왜 ShadowDOM이 생각보다 더 중요한가
I’m sorry, but I can’t access external websites. If you provide the text you’d like translated (excluding any code blocks or URLs you want to keep unchanged), I’ll be happy to translate it into Korean while preserving the original formatting.
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
이 스타일은 호스트 페이지에서 재정의될 수 없습니다.
class MyWidget extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
:host {
all: initial;
display: block;
}
/* Your component styles here */
</style>
<div>Hello from the Shadow DOM!</div>
`;
}
}
customElements.define('my-widget', MyWidget);어떤 페이지에든 “를 삽입하면 작동합니다. 호스트 페이지가 Tailwind, Bootstrap 또는 사용자 정의 스타일 등 어떤 CSS 프레임워크를 사용하든, 스타일이 컴포넌트에 침투하지 않습니다.
Source: …
진정한 빛: 스타일 블리드 방지
타사 웹사이트에 삽입되는 컴포넌트를 만든 적이 있다면, 다음과 같은 고통을 겪어봤을 것입니다:
- 신중하게 스타일링한 버튼이 사이트마다 다르게 보인다.
- 호스트 페이지의
* { box-sizing: border-box; }혹은h2 { color: red; }때문에 레이아웃이 망가진다. !important를 여기저기 붙이고 스스로를 원망한다.
Shadow DOM이 이 모든 문제를 해결합니다. 쉐도우 루트 내부의 스타일은 스코프가 제한됩니다. 끝.
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 컴포넌트의 경우.
리스트 효율적으로 렌더링하기
Don’t 각 리스트 항목마다 별도의 shadow‑DOM 컴포넌트를 만들지 마세요. 전체 리스트를 단일 shadow root 안에 렌더링하세요.
Style Duplication
각 컴포넌트 인스턴스가 자체 “ 블록을 포함하면, 브라우저는 동일한 CSS를 반복해서 파싱합니다.
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개의 컴포넌트가 하나의 파싱된 스타일시트를 사용하므로 스타일 재계산 작업이 크게 감소합니다.
이벤트 재타깃팅
그림자 루트 내부에서 발생한 이벤트는 재타깃팅되어 외부에서 관찰될 때 다음과 같이 보입니다:
// 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()를 사용하면 그림자 경계 너머까지 포함된 전체 전파 경로를 확인할 수 있습니다.
Slots – Passing Content In
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>Slots는 소비자가 사용자 정의 마크업(예: 아이콘)을 삽입할 수 있게 하면서, 컴포넌트는 자체 스타일링을 제어합니다.
Real‑World Use‑Case
많은 인증 제공업체는 호스트 애플리케이션과의 CSS 충돌을 피하기 위해 로그인 모달을 Shadow DOM에 삽입합니다. 모달은 호스트의 CSS 프레임워크와 관계없이 일관된 모습을 유지하며, 호스트 페이지가 의도치 않게(또는 악의적으로) 비밀번호 필드의 스타일을 변경할 수 없습니다.
Shadow DOM을 사용하면 안 되는 경우
| 시나리오 | 이유 |
|---|---|
| 블로그 콘텐츠 / CMS‑렌더링 HTML | 전역 스타일을 적용하고 싶을 때 |
| 단순 유틸리티 컴포넌트 | 캡슐화 오버헤드가 이점보다 클 때 |
| SSR‑중심 앱 | 선언적 Shadow DOM이 일반 SSR보다 아직 덜 성숙함 |
| 깊은 CSS 커스터마이징이 필요할 때 | 캡슐화가 소비자 오버라이드를 차단할 수 있음 |
Browser Support (2026)
- Shadow DOM v1: Chrome, Firefox, Safari, Edge (데스크톱 및 모바일) – 완전 지원.
- Constructable Stylesheets & Declarative Shadow DOM: 광범위한 지원, 2년 전과 비교해 개발자 경험이 크게 향상됨.
“The IE‑only” 변명은 이제 구시대적이다.
요약
Shadow DOM은 실제 문제를 해결합니다: CSS 및 DOM 캡슐화는 다양한 상황에서 구성되고, 삽입되며, 재사용되는 컴포넌트를 위해서입니다. 만약 여러분이 다음을 구축하고 있다면:
- 디자인 시스템
- 삽입 가능한 위젯
- 마이크로 프론트엔드
…Shadow DOM은 기본 선택이어야 합니다. 웹 플랫폼은 수년간 이를 지원해 왔으며, 도구들도 이제 마침내 따라잡았습니다. 더 이상 무시하지 마세요.