React, Vue, Svelte 같은 프레임워크 없이도 재사용 가능한 UI 컴포넌트를 만들 수 있다는 사실, 알고 계셨나요? 바로 웹 컴포넌트(Web Components)를 활용하면 됩니다. 브라우저가 기본으로 지원하는 표준 기술이기 때문에 별도의 번들러나 라이브러리 없이도 동작합니다.

웹 컴포넌트는 세 가지 핵심 기술로 구성됩니다.

  • Custom Elements — 나만의 HTML 태그를 정의
  • Shadow DOM — 캡슐화된 독립적인 DOM 트리
  • HTML Templates — 렌더링 전까지 파싱되지 않는 마크업

Custom Elements

Custom Elements를 사용하면 <my-button>처럼 자신만의 HTML 요소를 만들 수 있습니다. HTMLElement를 상속받아 클래스를 정의하고, customElements.define()으로 등록합니다.

class MyButton extends HTMLElement {
  constructor() {
    super();
    this.addEventListener('click', () => {
      console.log('클릭됨!');
    });
  }

  // 요소가 DOM에 추가될 때 호출
  connectedCallback() {
    this.render();
  }

  render() {
    this.innerHTML = `<button>${this.getAttribute('label') || '클릭'}</button>`;
  }
}

customElements.define('my-button', MyButton);

이제 HTML에서 바로 <my-button label="저장"></my-button>처럼 사용할 수 있습니다.

Shadow DOM

Shadow DOM은 컴포넌트의 내부 DOM과 스타일을 외부로부터 완전히 격리합니다. 외부 CSS가 내부를 침범하지 못하고, 내부 스타일이 전역에 영향을 주지 않습니다.

class MyCard extends HTMLElement {
  constructor() {
    super();
    // Shadow root 생성 (mode: 'open'이면 외부에서 접근 가능)
    const shadow = this.attachShadow({ mode: 'open' });

    shadow.innerHTML = `
      <style>
        /* 이 스타일은 이 컴포넌트에만 적용됩니다 */
        .card {
          padding: 1rem;
          border: 1px solid #e3e6ea;
          border-radius: 0.75rem;
          background: white;
        }
        h2 { color: #6891f8; }
      </style>
      <div class="card">
        <h2><slot name="title">기본 제목</slot></h2>
        <p><slot>내용을 입력하세요.</slot></p>
      </div>
    `;
  }
}

customElements.define('my-card', MyCard);

<slot>을 사용하면 외부에서 내용을 주입할 수 있습니다.

<my-card>
  <span slot="title">웹 컴포넌트</span>
  Shadow DOM을 활용한 완전한 캡슐화를 지원합니다.
</my-card>

HTML Templates

<template> 태그 안의 내용은 페이지가 로드될 때 렌더링되지 않습니다. JavaScript로 복제(clone)해서 DOM에 추가할 때 비로소 화면에 나타납니다.

<template id="card-template">
  <div class="card">
    <h2 class="card-title"></h2>
    <p class="card-body"></p>
  </div>
</template>
const template = document.getElementById('card-template');
const clone = template.content.cloneNode(true);

clone.querySelector('.card-title').textContent = '제목';
clone.querySelector('.card-body').textContent = '본문 내용';

document.body.appendChild(clone);

세 기술을 합친 완성형 컴포넌트

실제로는 세 가지를 조합해서 사용합니다. 아래는 점심 메뉴 추천 컴포넌트의 예시입니다.

class LunchRecommender extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'open' });

    const template = document.createElement('template');
    template.innerHTML = `
      <style>
        :host { display: block; padding: 2rem; border-radius: 1rem; }
        button { background: #6891f8; color: white; border: none;
                 padding: 0.75rem 1.5rem; border-radius: 0.5rem; cursor: pointer; }
      </style>
      <div>
        <p id="result">버튼을 눌러보세요!</p>
        <button id="btn">추천받기</button>
      </div>
    `;

    shadow.appendChild(template.content.cloneNode(true));

    const menus = ['김치찌개', '비빔밥', '파스타', '초밥', '카레'];
    shadow.getElementById('btn').addEventListener('click', () => {
      shadow.getElementById('result').textContent =
        menus[Math.floor(Math.random() * menus.length)];
    });
  }
}

customElements.define('lunch-recommender', LunchRecommender);

마치며

핵심 정리
웹 컴포넌트는 프레임워크 없이도 재사용 가능한 UI를 만드는 브라우저 표준입니다. Custom Elements로 태그를 정의하고, Shadow DOM으로 캡슐화하며, HTML Templates로 마크업을 효율적으로 재사용하세요.

React나 Vue 같은 프레임워크와 함께 사용할 수도 있고, 단순한 위젯이나 디자인 시스템을 만들 때 특히 빛납니다. 브라우저 지원도 모든 모던 브라우저에서 완벽하게 동작하니 지금 바로 시작해보세요!

라이프사이클 콜백 완전 정복

Custom Element에는 4가지 라이프사이클 콜백이 있습니다. 각각의 호출 시점을 정확히 이해해야 메모리 누수 없는 컴포넌트를 만들 수 있습니다.

class MyWidget extends HTMLElement {
  // DOM에 연결될 때 — 초기화, 이벤트 리스너 등록
  connectedCallback() {
    this._render();
    this._button = this.shadowRoot.querySelector('button');
    this._button.addEventListener('click', this._handleClick);
  }

  // DOM에서 제거될 때 — 반드시 정리 작업!
  disconnectedCallback() {
    this._button?.removeEventListener('click', this._handleClick);
    clearInterval(this._timer); // 타이머 정리
  }

  // 관찰 중인 attribute가 변경될 때
  attributeChangedCallback(name, oldValue, newValue) {
    if (oldValue !== newValue) this._render();
  }

  // 관찰할 attribute 목록
  static get observedAttributes() {
    return ['label', 'disabled'];
  }
}

disconnectedCallback에서 이벤트 리스너와 타이머를 반드시 정리해야 합니다. 이를 빠뜨리면 컴포넌트가 DOM에서 제거된 후에도 클로저가 메모리에 남아 메모리 누수가 발생합니다.

Web Components vs React/Vue — 언제 무엇을 쓸까

항목Web ComponentsReact/Vue
프레임워크 의존성없음 (순수 브라우저 API)프레임워크 필수
재사용성모든 프레임워크에서 사용 가능같은 프레임워크 내에서만
상태 관리직접 구현 필요풍부한 생태계 제공
SSR 지원제한적우수 (Next.js, Nuxt 등)
적합한 용도디자인 시스템, 위젯, 마이크로 프론트엔드SPA, 복잡한 앱

Lit — 경량 Web Components 라이브러리

Google이 만든 Lit은 Web Components를 더 쉽게 개발할 수 있는 경량 라이브러리입니다. 번들 크기가 약 5KB(gzip)이며 반응형 속성과 템플릿 업데이트를 자동으로 처리합니다.

import { LitElement, html, css } from 'lit';
import { property } from 'lit/decorators.js';

class CounterElement extends LitElement {
  static styles = css`
    button { padding: 8px 16px; font-size: 1rem; cursor: pointer; }
    span { margin: 0 12px; font-weight: bold; }
  `;

  @property({ type: Number }) count = 0;

  render() {
    return html`
      <button @click=${() => this.count--}>-</button>
      <span>${this.count}</span>
      <button @click=${() => this.count++}>+</button>
    `;
  }
}
customElements.define('my-counter', CounterElement);

프레임워크 없이 인터랙티브 컴포넌트를 만들어야 한다면 Lit이 첫 번째 선택지입니다. TypeScript와 데코레이터를 완벽히 지원하고, 모든 주요 프레임워크와 함께 사용할 수 있습니다.

Web Components로 디자인 시스템 구축하기

Web Components의 가장 강력한 활용 사례는 여러 프로젝트에서 공유하는 디자인 시스템 구축입니다. 한 번 만든 컴포넌트를 React 프로젝트, Vue 프로젝트, 순수 HTML 페이지 어디서든 동일하게 사용할 수 있기 때문입니다.

Google의 Material Web Components, Adobe의 Spectrum Web Components가 대표적인 사례입니다. 이들은 모두 Web Components 표준을 기반으로 만들어져 어떤 프레임워크 환경에서도 동작합니다. 국내에서도 대형 IT 기업들이 내부 디자인 시스템을 Web Components로 전환하는 사례가 늘고 있습니다.

디자인 시스템을 Web Components로 구축할 때는 몇 가지 핵심 원칙이 있습니다. 첫째, CSS 커스텀 속성(CSS Variables)을 통해 외부에서 스타일을 재정의할 수 있게 해야 합니다. Shadow DOM이 스타일을 캡슐화하지만, :host와 CSS 변수를 활용하면 테마 변경이 가능합니다. 둘째, ARIA 속성을 처음부터 설계에 반영해 접근성을 보장해야 합니다. 셋째, Storybook 같은 컴포넌트 문서화 도구와 연동해 팀원 모두가 쉽게 컴포넌트를 찾고 사용할 수 있도록 해야 합니다.

Web Components는 마이크로 프론트엔드 아키텍처에서도 각광받고 있습니다. 팀별로 독립적으로 개발한 기능 모듈을 Web Components로 패키징하면, 다른 팀의 앱에 프레임워크 충돌 없이 통합할 수 있습니다. 대규모 조직에서 여러 팀이 각자의 기술 스택으로 개발하면서도 일관된 사용자 경험을 제공해야 할 때 특히 유용합니다.

Web Components를 언제 선택하고 언제 피해야 하는가

Web Components가 강력한 도구이지만, 모든 상황에 적합한 것은 아닙니다. Web Components가 가장 빛을 발하는 상황은 다음과 같습니다. 여러 프레임워크나 기술 스택을 사용하는 팀들이 공통 UI 컴포넌트를 공유해야 할 때, 프레임워크에 종속되지 않는 독립적인 위젯을 배포해야 할 때(지도 위젯, 차트, 임베드 가능한 폼), 또는 수년간 유지해야 하는 컴포넌트 라이브러리를 만들 때 Web Components 표준이 특정 프레임워크보다 더 오랜 수명을 보장합니다.

반면 Web Components를 피해야 하는 경우도 있습니다. 단일 React나 Vue 프로젝트 내에서만 사용할 컴포넌트라면, 해당 프레임워크의 네이티브 컴포넌트가 더 나은 개발 경험과 도구 지원을 제공합니다. 서버 사이드 렌더링(SSR)이 중요한 SEO 페이지라면 Web Components의 Shadow DOM이 SSR과 완벽하게 통합되지 않을 수 있어 주의가 필요합니다. 또한 팀의 학습 비용도 고려해야 합니다. 생명주기, 슬롯, Shadow DOM의 개념을 익히는 데 시간이 필요하므로, 작은 프로젝트에서 학습 비용 대비 얻는 이점이 충분한지 판단해야 합니다. 결국 Web Components는 프레임워크를 대체하는 도구가 아니라, 프레임워크를 초월한 상호 운용성이 필요할 때 사용하는 도구입니다.