왜 ShadowDOM이 생각보다 더 중요한가

발행: (2026년 3월 20일 AM 03:35 GMT+9)
12 분 소요
원문: Dev.to

Source: Dev.to

저는 예전에는 Shadow DOM이 아무도 원하지 않는 커스텀 엘리먼트를 만드는 사람들을 위한 틈새 기능이라고 생각했습니다. 그런데 임베드 가능한 위젯과 디자인‑시스템 컴포넌트를 만들기 시작하면서 Shadow DOM이 제 툴킷에서 가장 유용한 도구가 되었습니다. 여기서는 Shadow DOM이 받는 것보다 더 많은 주목을 받아야 하는 이유를 설명합니다.


Shadow DOM이란

Shadow DOM은 브라우저 네이티브 방식으로 캡슐화된 DOM 트리를 생성하는 방법입니다. 요소에 연결된 쉐도우 루트는 자체 스코프를 가지며, CSS가 안팎으로 새어나오지 않고, 메인 페이지의 JavaScript DOM 쿼리도 내부에 접근할 수 없습니다.

class MyWidget extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.innerHTML = `
      
        .container { padding: 16px; font-family: system-ui; }
        h2 { color: #333; margin: 0 0 8px; }
        p { color: #666; line-height: 1.5; }
      
      
        

Hello from the Shadow

These styles can’t be overridden by the host page.

  `;
  }
}
customElements.define('my-widget', MyWidget);

Drop “ on any page, and it works. No matter what CSS framework the host page uses—Tailwind, Bootstrap, or custom styles—nothing bleeds into your component.


진정한 빛: 스타일 블리드 방지

만약 서드‑파티 웹사이트에 삽입되는 컴포넌트를 만든 적이 있다면, 그 고통을 잘 알고 있을 겁니다:

  • 정성스럽게 스타일링한 버튼이 사이트마다 다르게 보입니다.
  • 호스트 페이지의 * { box-sizing: border-box; } 혹은 h2 { color: red; }가 레이아웃을 망칩니다.
  • !important를 여기저기 붙여보지만 스스로를 혐오하게 됩니다.

Shadow DOM이 이 모든 문제를 해결합니다. Shadow root 내부의 스타일은 스코프가 제한됩니다. 끝.

class PricingCard extends HTMLElement {
  connectedCallback() {
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.innerHTML = `
      
        /* These styles ONLY apply inside this shadow root */
        :host {
          display: block;
          max-width: 320px;
        }
        .card {
          border: 1px solid #e0e0e0;
          border-radius: 12px;
          padding: 24px;
          background: white;
        }
        .price {
          font-size: 2rem;
          font-weight: 700;
          color: #111;
        }
        button {
          width: 100%;
          padding: 12px;
          background: #2563eb;
          color: white;
          border: none;
          border-radius: 8px;
          cursor: pointer;
          font-size: 1rem;
        }
        button:hover { background: #1d4ed8; }
      
      
        
### ${this.getAttribute('plan') || 'Pro'}

        ${this.getAttribute('price') || '$29'}/mo
        Get Started
      
    `;
  }
}
customElements.define('pricing-card', PricingCard);

호스트 페이지에 button { background: pink; border-radius: 0; }가 있더라도, 여러분의 가격 카드가 설계된 그대로 표시됩니다.


:host::part 선택자

Shadow DOM은 벽이 아니라—제어된 스타일링 API를 제공합니다.

:host

섀도우 내부에서 커스텀 엘리먼트 자체를 스타일링합니다:

:host {
  display: block;
  margin: 16px 0;
}

:host([variant="dark"]) {
  background: #1a1a1a;
  color: white;
}

:host(:hover) {
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}

::part

호스트 페이지가 스타일을 적용할 수 있도록 특정 내부 요소를 노출합니다:

// Inside the component
shadow.innerHTML = `
  
    .header { padding: 16px; }
  
  
    
  
  
    
  
`;
/* Host page can now style these parts */
my-component::part(header) {
  background: navy;
  color: white;
}

이렇게 하면 기본적으로 캡슐화되고, 원하는 부분에서는 커스터마이징이 가능합니다.

설득력 있는 사용 사례: 디자인‑시스템 컴포넌트

디자인‑시스템 컴포넌트가 Shadow DOM을 사용할 경우, 팀은 React, Vue, Svelte 또는 일반 HTML 프로젝트에서 스타일 충돌을 걱정하지 않고 사용할 수 있습니다.

class DsButton extends HTMLElement {
  static get observedAttributes() {
    return ['variant', 'size', 'disabled'];
  }

  connectedCallback() {
    const shadow = this.attachShadow({ mode: 'open' });
    this.render(shadow);
  }

  attributeChangedCallback() {
    if (this.shadowRoot) this.render(this.shadowRoot);
  }

  render(shadow) {
    const variant = this.getAttribute('variant') || 'primary';
    const size = this.getAttribute('size') || 'medium';

    shadow.innerHTML = `
      
        button {
          font-family: inherit;
          border: none;
          border-radius: 6px;
          cursor: pointer;
          font-weight: 500;
          transition: all 0.15s ease;
        }
        button[data-variant="primary"] {
          background: #2563eb; color: white;
        }
        button[data-variant="secondary"] {
          background: #f3f4f6; color: #374151;
        }
        button[data-size="small"]  { padding: 6px 12px;  font-size: 0.875rem; }
        button[data-size="medium"] { padding: 10px 20px; font-size: 1rem; }
        button[data-size="large"]  { padding: 14px 28px; font-size: 1.125rem; }
      
      
        
      
    `;
  }
}
customElements.define('ds-button', DsButton);

모든 프레임워크에서 사용하기

<ds-button variant="primary" size="large">Submit</ds-button>

Shadow DOM이 빛날 때

  • 채팅 위젯, 피드백 폼, 결제 모달, 인증 다이얼로그 – 다른 사람 사이트에 삽입하는 모든 것이 크게 이점을 얻습니다.
  • 마이크로 프런트엔드 – 서로 다른 팀이 페이지의 서로 다른 부분을 담당합니다; Shadow DOM은 팀 경계 사이의 CSS 충돌을 방지합니다. 각 마이크로 프런트엔드는 다른 팀에 영향을 주지 않으면서 원하는 CSS 방법론을 사용할 수 있습니다.

트레이드오프: 알아야 할 점

  • DOM 크기 – 각 섀도우 루트는 별개의 DOM 트리입니다. 페이지에 수백 개의 섀도우 루트가 있으면 메모리와 렌더링 성능에 영향을 줄 수 있습니다.
  • 스타일링 제한 – 전역 CSS 변수는 작동하지만, 파트를 노출하지 않으면 호스트 페이지에서 섀도우 트리 내부에 접근할 수 없습니다.
  • 툴링 지원 – 일부 구형 브라우저는 전체 Shadow DOM 지원이 부족합니다(하지만 폴리필이 존재합니다).

핵심 요점: 캡슐화 이점이 비용보다 일반적으로 더 큽니다, 특히 재사용 가능하고 임베드 가능한 UI 컴포넌트의 경우.

리스트 효율적으로 렌더링하기

하지 말아야 할 것 각 리스트 항목마다 별도의 shadow‑DOM 컴포넌트를 만들지 마세요. 전체 리스트를 단일 shadow root 안에 렌더링하세요.

Style Duplication

각 컴포넌트 인스턴스가 자체 “ 블록을 포함하면 브라우저가 동일한 CSS를 반복해서 파싱합니다.

Example: 50 인스턴스 × 동일한 “ → 50개의 별도 스타일 계산.

Mitigation with Constructable Stylesheets

// Create a shared stylesheet once
const sheet = new CSSStyleSheet();
sheet.replaceSync(`
  .container { padding: 16px; }
  button { background: blue; color: white; }
`);

class EfficientComponent extends HTMLElement {
  connectedCallback() {
    const shadow = this.attachShadow({ mode: 'open' });
    // Share the stylesheet across all instances
    shadow.adoptedStyleSheets = [sheet];
    shadow.innerHTML = `
      
        
      `;
  }
}

Why it helps – 이제 모든 50개의 컴포넌트가 one 파싱된 스타일시트를 사용하므로 스타일‑재계산 작업이 크게 감소합니다.

이벤트 리타게팅

섀도우 루트 내부에서 발생한 이벤트는 외부에서 관찰할 때 리타게팅됩니다:

// Inside shadow: click on <button> in <my-widget>
// Outside observer sees:
document.addEventListener('click', e => {
  console.log(e.target);          // → <my-widget>
  console.log(e.composedPath());   // → actual element chain
});

팁: event.composedPath()를 사용하면 섀도우 경계 너머의 전체 전파 경로를 확인할 수 있습니다.


슬롯 – 콘텐츠 전달하기

class AlertBox extends HTMLElement {
  connectedCallback() {
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.innerHTML = `
      
        .alert { padding: 16px; border-radius: 8px; border-left: 4px solid; }
        :host([type="warning"]) .alert { background:#fef3c7; border-color:#f59e0b; }
        :host([type="error"])   .alert { background:#fee2e2; border-color:#ef4444; }
        :host([type="info"])    .alert { background:#dbeafe; border-color:#3b82f6; }
      
      
        
        
      `;
  }
}
customElements.define('alert-box', AlertBox);
<alert-box type="warning">
  <span slot="icon">⚠️</span>
  Please verify your email address.
</alert-box>

슬롯을 사용하면 소비자가 커스텀 마크업(예: 아이콘)을 삽입할 수 있고, 컴포넌트는 자체 스타일을 제어합니다.


실제 사용 사례

많은 인증 제공자는 로그인 모달을 Shadow DOM에 삽입하여 호스트 애플리케이션과 CSS 충돌을 방지합니다. 모달은 호스트의 CSS 프레임워크와 관계없이 일관된 모습을 유지하며, 호스트 페이지가 비의도적으로(또는 악의적으로) 비밀번호 필드의 스타일을 변경할 수 없습니다.

Shadow DOM을 사용하면 안 되는 경우

시나리오이유
블로그 콘텐츠 / CMS‑렌더링 HTML전역 스타일을 적용하고 싶을 때
간단한 유틸리티 컴포넌트캡슐화 오버헤드가 이점을 초과할 때
SSR‑중심 앱선언적 Shadow DOM은 아직 일반 SSR보다 성숙도가 낮음
깊은 CSS 커스터마이징이 필요할 때캡슐화가 소비자 오버라이드를 차단할 수 있음

브라우저 지원 (2026)

  • Shadow DOM v1: Chrome, Firefox, Safari, Edge (데스크톱 및 모바일) – 완전 지원.
  • Constructable Stylesheets & Declarative Shadow DOM: 폭넓은 지원, 2년 전과 비교해 개발자 경험이 크게 향상되었습니다.

“The IE‑only” 변명은 이제 구시대적입니다.


요약

Shadow DOM는 실제 문제를 해결합니다: CSS와 DOM 캡슐화를 제공하여 다양한 상황에서 구성, 임베드, 재사용되는 컴포넌트들을 지원합니다. 만약 다음과 같은 것을 구축한다면:

  • 디자인 시스템
  • 임베드 가능한 위젯
  • 마이크로 프론트엔드

…Shadow DOM은 기본 선택이어야 합니다. 웹 플랫폼은 수년간 이를 지원해 왔으며, 도구들도 이제 따라잡았습니다. 더 이상 무시하지 마세요.

0 조회
Back to Blog

관련 글

더 보기 »

스크롤 시 텍스트 표시

이 comment를 숨기시겠습니까? post에서는 숨겨지지만 comment의 permalink를 통해 여전히 볼 수 있습니다....