섀도 DOM 완벽 가이드: 진짜 캡슐화의 세계

shadow-domweb-componentscss-encapsulationdomfrontend

1. 전역 스코프의 한계와 새로운 프리미티브의 등장

CSS의 가장 큰 특징 중 하나는 모든 규칙이 기본적으로 전역 스코프에 적용된다는 점이다. 이로 인해 애플리케이션이 복잡해질수록 스타일 충돌 및 예측 불가능한 오버라이드 문제가 발생한다.

/* global.css */
.button { background: blue; color: white; }
<third-party-widget>
  <button class="button">Widget Button</button>
</third-party-widget>

BEM, CSS Modules, CSS-in-JS와 같은 방법론과 도구들은 이 문제를 해결하기 위한 효과적인 전략이다. 하지만 이들은 네이밍 컨벤션, 빌드 타임 클래스명 해싱(hashing), 혹은 JavaScript 런타임에 의존한다.

이러한 애플리케이션 레벨의 해결책과 달리, Shadow DOM은 브라우저 자체가 제공하는 네이티브 캡슐화(encapsulation) 기능이다. 이를 통해 외부로부터 완벽히 격리된 DOM 하위 트리를 생성하여 진정한 컴포넌트 캡슐화를 구현할 수 있다.

2. Shadow DOM의 핵심 개념과 구조

Shadow DOM은 일반적인 DOM(Light DOM) 요소에 연결되어 렌더링되지만, 메인 DOM 트리와는 독립적으로 구성되는 숨겨진 DOM 트리이다.

const hostElement = document.querySelector('#host');
 
// hostElement에 Shadow DOM을 연결하고 Shadow Root를 반환한다.
const shadowRoot = hostElement.attachShadow({ mode: 'open' });
 
shadowRoot.innerHTML = `
  <style> p { color: red; } </style>
  <p>이 텍스트는 Shadow DOM 내부에 있으므로 빨간색입니다.</p>
`;
 
document.body.innerHTML += `<p>이 텍스트는 Light DOM에 있으므로 영향을 받지 않습니다.</p>`;

주요 용어 정리

  • Shadow Host : Shadow DOM을 포함하는 일반 DOM 요소 (hostElement).
  • Shadow Root : Shadow DOM 트리의 최상위 노드. attachShadow() 메서드의 반환 값 (shadowRoot).
  • Shadow Tree : Shadow Root와 그 모든 자손으로 구성된 DOM 트리.
  • Shadow Boundary : Shadow DOM이 끝나고 일반 DOM이 시작되는 경계.

mode: 'open' vs mode: 'closed'

attachShadow() 메서드는 mode 옵션을 받는다.

  • mode: 'open': JavaScript를 통해 host.shadowRoot로 Shadow Root에 접근할 수 있다. 대부분의 경우 권장되는 방식이다.
  • mode: 'closed': host.shadowRoot가 null을 반환하여 외부에서의 접근을 차단한다. 이는 컴포넌트 사용자에게 거의 완벽한 블랙박스를 제공하지만, 디버깅이나 테스트가 매우 어려워지므로 특별한 보안 요구사항이 없는 한 사용이 권장되지 않는다.

3. 왜 Shadow DOM인가?

1. CSS 스코프 캡슐화 (Style Encapsulation)

Shadow DOM 내부의 <style> 태그에 정의된 규칙은 해당 Shadow Tree 내부에만 적용된다. 마찬가지로, 외부의 CSS 규칙은 Shadow DOM 내부의 요소에 영향을 주지 않는다 (CSS Custom Properties 등 일부 예외 제외). !important 규칙조차 이 경계를 넘을 수 없다.

2. DOM 구조 캡슐화 (DOM Encapsulation)

document.querySelector()와 같은 외부의 DOM API는 Shadow DOM 내부의 요소를 탐색할 수 없다. 내부 구조는 컴포넌트의 구현 상세로 숨겨지며, 이는 외부의 스크립트가 의도치 않게 컴포넌트 내부를 변경하는 것을 방지한다.

4. 컴포넌트 구성: 슬롯 <slot>과 스타일링

<slot>을 이용한 콘텐츠 프로젝션

<slot> 엘리먼트는 외부(Light DOM)에서 제공된 콘텐츠를 Shadow DOM 내부의 특정 위치에 렌더링할 수 있게 해주는 플레이스홀더이다.

// <custom-card>의 Shadow DOM
shadowRoot.innerHTML = `
  <header>
    <slot name="header">기본 헤더</slot>
  </header>
  <main>
    <slot></slot> </main>
`;
<custom-card>
  <h2 slot="header">커스텀 헤더</h2>
  <p>이 내용은 기본 슬롯으로 들어갑니다.</p>
</custom-card>

Shadow DOM 내부 스타일링

Shadow DOM 내부에서는 특별한 CSS 의사 클래스/요소를 사용하여 호스트와 슬롯을 스타일링할 수 있다.

  • :host : Shadow Host 요소 자체를 선택한다.
  • ::slotted(selector) : 슬롯에 삽입된 최상위 노드 중 selector와 일치하는 요소를 선택한다.
:host {
  display: block;
  border: 1px solid grey;
}
 
:host(:hover) {
  border-color: blue;
}
 
::slotted(h2) {
  font-size: 1.5rem;
  color: midnightblue;
}

5. 외부에서의 스타일 제어 기법

완벽한 캡슐화는 장점이지만, 때로는 컴포넌트의 스타일을 외부에서 커스터마이징해야 한다. Shadow DOM은 이를 위한 공식적인 통로를 제공한다.

  1. CSS Custom Properties (CSS 변수) : Custom Properties는 Shadow Boundary를 통과한다. 이는 컴포넌트의 테마(theme)를 외부에서 제어하는 가장 강력한 방법이다.
/* 외부 CSS */
custom-card {
  --card-padding: 20px;
  --header-color: #f06;
}
 
/* Shadow DOM 내부 CSS */
:host {
  padding: var(--card-padding, 16px);
}
::slotted(h2) {
  color: var(--header-color, #333);
}
  1. ::part Pseudo-element : 컴포넌트 개발자는 내부의 특정 요소에 part 속성을 부여하여 외부에 스타일링을 허용할 수 있다.
<button part="action-button">클릭</button>
/* 외부 CSS */
custom-card::part(action-button) {
  background-color: green;
}
  1. 상속 가능한 CSS 속성 : color, font-family, line-height 등 상속 가능한 CSS 속성들은 기본적으로 Shadow Boundary를 통과하여 내부 요소에 적용된다.

6. 이벤트 모델: 재타겟팅과 전파

Shadow DOM 내부에서 발생한 버블링되는 이벤트가 경계를 넘어 전파될 때, 이벤트의 target이 Shadow Host로 재설정된다. 이는 외부에는 컴포넌트의 내부 구조를 숨기기 위함이다.

// 외부 document에 부착된 리스너
document.addEventListener('click', (e) => {
  console.log(e.target); // <my-component> (내부 버튼이 아님)
});

컴포넌트 내부의 특정 이벤트를 외부에 알리려면, composed: true 옵션을 가진 커스텀 이벤트를 사용해야 한다.

  • composed: false (기본값) : 이벤트가 Shadow Boundary를 넘어 전파되지 않는다.
  • composed: true : 이벤트가 경계를 넘어 Light DOM으로 전파된다.
this.dispatchEvent(new CustomEvent('value-changed', {
  detail: { value: this.newValue },
  bubbles: true,
  composed: true // 외부 리스너가 감지할 수 있도록 설정
}));

7. 주요 아키텍처적 고려사항

Declarative Shadow DOM

attachShadow()는 클라이언트 사이드 API이므로, 서버 사이드 렌더링(SSR) 환경에서는 사용할 수 없다. 이 문제를 해결하기 위해 Declarative Shadow DOM이 표준화되었다. 서버는 <template> 태그와 shadowrootmode 속성을 이용해 Shadow DOM 구조를 포함한 HTML을 생성할 수 있다.

<my-component>
  <template shadowrootmode="open">
    <style> p { color: red; } </style>
    <p>서버에서 렌더링된 Shadow DOM 콘텐츠</p>
  </template>
</my-component>

포커스 관리

기본적으로 Shadow DOM은 문서의 순차적인 포커스 탐색(Tab 키)에 포함된다. attachShadow({ delegatesFocus: true }) 옵션을 사용하면, Shadow Host에 포커스가 갔을 때 내부의 첫 번째 포커스 가능한 요소로 포커스를 위임할 수 있어 접근성을 향상시킬 수 있다.

8. 사용 시나리오와 트레이드오프 분석

적합한 사용 사례

  • 독립적인 위젯: 외부 스타일에 전혀 영향을 받지 않는 날짜 선택기, 결제 모듈, 비디오 플레이어.
  • 서드파티 콘텐츠 삽입: 광고, 소셜 미디어 위젯, 댓글 시스템 등 외부 콘텐츠를 안전하게 격리.
  • 프레임워크 독립적인 디자인 시스템: 어떤 프레임워크와도 함께 사용할 수 있는 재사용 가능한 UI 컴포넌트 라이브러리.

고려사항

  • 스타일링 복잡성: 외부에서 내부를 스타일링하는 것은 CSS 변수나 ::part를 통해 명시적으로 허용해야 하므로, 간단한 CSS 오버라이드보다 복잡할 수 있습니다.
  • SEO: 현대적인 검색 엔진 크롤러(Googlebot 등)는 Shadow DOM 콘텐츠를 렌더링하고 인덱싱할 수 있습니다. 그러나 일부 오래된 크롤러나 소셜 미디어 링크 파서는 콘텐츠를 누락할 수 있으므로, 핵심적인 SEO 콘텐츠는 Light DOM에 두는 것이 여전히 안전한 전략일 수 있습니다.
  • 브라우저 호환성: 모든 모던 브라우저에서 네이티브로 지원됩니다. IE11과 같은 레거시 브라우저 지원이 필요하다면 폴리필이 필요하지만, 현재시점으로, 대부분의 프로젝트에서는 고려 대상이 아니다.

9. 결론: 캡슐화를 위한 브라우저의 표준

Shadow DOM은 CSS Modules나 CSS-in-JS와 경쟁하는 기술이라기보다는, 이들이 해결하려던 '스코프' 문제를 브라우저 네이티브 레벨에서 해결하는 근본적인 웹 표준 기술이다.

React/Vue와 같은 프레임워크가 제공하는 개발 경험과 생태계는 매우 강력하지만, 프레임워크에 종속되지 않고 재사용 가능하며, 예측 가능하고 견고한 컴포넌트를 만들어야 하는 경우, Shadow DOM은 가장 신뢰할 수 있는 표준 기술이다. 이는 웹 개발의 복잡성을 관리하고, 더 안정적인 대규모 애플리케이션을 구축하기 위한 중요한 아키텍처적 도구인 것이다.