개요

Web Components는 재사용 가능한 커스텀 HTML 요소를 만들 수 있는 웹 표준 기술의 집합입니다. Custom Elements, Shadow DOM, HTML Templates 세 가지 핵심 스펙으로 구성되며, 프레임워크에 독립적인 컴포넌트를 만들 수 있다는 것이 가장 큰 장점입니다.

2025년 현재 Web Components는 모든 주요 브라우저에서 완벽하게 지원되며, Declarative Shadow DOM과 같은 새로운 기능이 추가되면서 SSR(서버 사이드 렌더링)과의 호환성도 크게 개선되었습니다. React, Vue, Angular 등 어떤 프레임워크와도 함께 사용할 수 있어 디자인 시스템 구축에 특히 적합합니다.

핵심 개념

Web Components의 세 가지 핵심 기술을 이해해야 합니다.

  • Custom Elements: customElements.define()로 새로운 HTML 태그를 등록합니다. 반드시 하이픈(-)을 포함한 이름이어야 합니다.
  • Shadow DOM: 컴포넌트의 DOM과 스타일을 외부로부터 격리합니다. attachShadow({ mode: 'open' })으로 생성합니다.
  • HTML Templates: <template><slot>으로 재사용 가능한 마크업 구조를 정의합니다.
  • Declarative Shadow DOM: <template shadowrootmode="open">으로 JavaScript 없이 Shadow DOM을 선언합니다. SSR에 필수입니다.
  • 라이프사이클 콜백: connectedCallback, disconnectedCallback, attributeChangedCallback 등으로 컴포넌트 생명주기를 관리합니다.

실전 예제

실무에서 바로 사용할 수 있는 알림 카드 Web Component 예제입니다.

<!-- 사용 예시 -->
<alert-card type="warning" dismissible>
  <strong>주의:</strong> 이 작업은 되돌릴 수 없습니다.
</alert-card>

<alert-card type="success">
  저장이 완료되었습니다.
</alert-card>

<!-- Declarative Shadow DOM (SSR 호환) -->
<alert-card type="info">
  <template shadowrootmode="open">
    <style>
      :host { display: block; }
      .alert { padding: 1rem; border-radius: 8px; }
    </style>
    <div class="alert">
      <slot></slot>
    </div>
  </template>
  서버에서 렌더링된 알림입니다.
</alert-card>
<script>
class AlertCard extends HTMLElement {
  static observedAttributes = ['type', 'dismissible'];

  // 타입별 스타일 맵
  static styles = {
    info:    { bg: '#eff6ff', border: '#3b82f6', icon: 'info' },
    success: { bg: '#f0fdf4', border: '#22c55e', icon: 'check_circle' },
    warning: { bg: '#fffbeb', border: '#f59e0b', icon: 'warning' },
    error:   { bg: '#fef2f2', border: '#ef4444', icon: 'error' }
  };

  constructor() {
    super();
    // Declarative Shadow DOM이 없으면 imperative로 생성
    if (!this.shadowRoot) {
      this.attachShadow({ mode: 'open' });
    }
  }

  connectedCallback() {
    this.render();
  }

  attributeChangedCallback(name, oldVal, newVal) {
    if (oldVal !== newVal) this.render();
  }

  get type() {
    return this.getAttribute('type') || 'info';
  }

  get dismissible() {
    return this.hasAttribute('dismissible');
  }

  render() {
    const style = AlertCard.styles[this.type] || AlertCard.styles.info;

    this.shadowRoot.innerHTML = `
      <style>
        :host {
          display: block;
          margin: 1rem 0;
        }
        :host([hidden]) { display: none; }

        .alert {
          display: flex;
          align-items: flex-start;
          gap: 0.75rem;
          padding: 1rem 1.25rem;
          border-radius: 8px;
          border-left: 4px solid ${style.border};
          background: ${style.bg};
          font-family: inherit;
          line-height: 1.6;
        }

        .close-btn {
          margin-left: auto;
          background: none;
          border: none;
          cursor: pointer;
          font-size: 1.25rem;
          opacity: 0.6;
          padding: 0;
        }
        .close-btn:hover { opacity: 1; }

        /* 외부 폰트 상속 */
        ::slotted(*) {
          margin: 0;
        }
      </style>
      <div class="alert" role="alert">
        <div class="content">
          <slot></slot>
        </div>
        ${this.dismissible ? '<button class="close-btn" aria-label="닫기">x</button>' : ''}
      </div>
    `;

    if (this.dismissible) {
      this.shadowRoot.querySelector('.close-btn')
        ?.addEventListener('click', () => {
          this.dispatchEvent(new CustomEvent('dismiss'));
          this.remove();
        });
    }
  }
}

customElements.define('alert-card', AlertCard);
</script>

활용 팁

  • 디자인 시스템에 최적: React, Vue, Angular 등 어떤 프레임워크에서든 사용할 수 있으므로, 조직의 디자인 시스템 컴포넌트로 적합합니다.
  • CSS Custom Properties로 테마 적용: Shadow DOM은 스타일을 격리하지만, CSS 변수(custom properties)는 경계를 넘어 전달됩니다. 이를 활용해 테마를 적용하세요.
  • ::part()과 exportparts로 Shadow DOM 내부 요소에 외부에서 제한적으로 스타일을 적용할 수 있습니다.
  • Lit 프레임워크 검토: 순수 Web Components가 번거롭다면 Google의 Lit을 사용하면 간결한 문법으로 동일한 웹 컴포넌트를 만들 수 있습니다.
  • 폼 연동: ElementInternals API를 사용하면 커스텀 요소가 네이티브 폼에 참여할 수 있습니다(Form-Associated Custom Elements).

마무리

Web Components는 프레임워크 전쟁에서 벗어나 웹 표준에 기반한 컴포넌트를 만들 수 있게 해줍니다. Shadow DOM의 스타일 격리, Custom Elements의 재사용성, Declarative Shadow DOM의 SSR 호환성이 결합되어, 이제 프로덕션에서도 충분히 활용할 수 있는 수준에 도달했습니다. 특히 여러 프레임워크가 혼재하는 환경이나 디자인 시스템 구축에 Web Components를 적극 고려해 보시기 바랍니다.