notes
Shadow DOM이란?
Shadow DOM의 캡슐화·스타일 격리·컴포넌트 구조와 사용 메모.
- Shadow DOM 은 컴포넌트 하나를 바깥 페이지와 분리된 작은 문서처럼 만든다.
- 밖에서 스타일을 바꾸려면 컴포넌트가 열어 둔 통로만 쓸 수 있다.
- 요즘은 JS 없이 HTML 만으로도 만들 수 있어서 SSR 에서도 쓸 수 있다.
Shadow DOM 이란
일반 DOM 은 페이지 전체가 하나의 트리다. 어느 CSS 든 어느 요소든 건드릴 수 있다.
Shadow DOM 은 어떤 요소(shadow host) 아래에 별도 트리(shadow root)를 붙인다. 이 안쪽은 바깥 트리와 경계가 있어서, document.querySelector 로도 바로 못 찾고 바깥 CSS 선택자도 안 먹힌다.
<video> 태그의 재생 버튼이나 <input type="range"> 의 슬라이더가 대표적인 예다. 브라우저가 내부를 Shadow DOM 으로 그려서, 우리 CSS 로 내부를 마음대로 못 바꾼다.
웹 컴포넌트를 만들 때 주로 쓴다. Custom Element 와 같이 쓰면 어떤 페이지에 넣어도 스타일이 깨지지 않는 컴포넌트가 된다.
class MyCard extends HTMLElement {
constructor() {
super();
const root = this.attachShadow({ mode: 'open' });
root.innerHTML = `
<style>
:host { display: block; border: 1px solid #ddd; border-radius: 8px; }
p { color: var(--card-color, #333); }
</style>
<p><slot></slot></p>
`;
}
}
customElements.define('my-card', MyCard);안에 쓴 p { ... } 는 이 컴포넌트 안의 p 에만 먹는다. 페이지의 다른 p 는 영향을 안 받는다.
경계를 넘는 것과 못 넘는 것
캡슐화라고 해서 완전히 막힌 건 아니다. 컴포넌트가 열어 둔 통로로만 들어간다.
<style>
my-card { --card-color: tomato; color: navy; }
my-card::part(title) { font-weight: 700; }
</style>
<my-card>
<span slot="title">제목</span>
본문은 기본 slot 으로 들어간다
</my-card>이벤트도 비슷하다. 안에서 난 클릭은 밖으로 버블링되지만, 밖에서 보면 event.target 이 host 요소로 바뀌어 보인다. 커스텀 이벤트를 밖으로 내보내려면 composed: true 를 줘야 한다.
open, closed 그리고 Declarative Shadow DOM
mode: 'open' 이면 밖에서 element.shadowRoot 로 안쪽에 접근할 수 있다. 'closed' 면 null 이 나온다.
closed 가 보안 기능은 아니다. 개발자 도구에서는 다 보이고, 테스트나 디버깅만 불편해져서 보통 open 을 쓴다.
예전에는 Shadow DOM 을 JS 로만 만들 수 있어서, 서버에서 렌더링한 HTML 에는 넣을 수가 없었다. 지금은 Declarative Shadow DOM 이 Chrome, Edge, Safari, Firefox 에 다 들어와서 HTML 만으로 만들 수 있다.
<my-card>
<template shadowrootmode="open">
<style>
:host { display: block; }
</style>
<p><slot></slot></p>
</template>
본문
</my-card>파서가 template 을 보고 바로 shadow root 를 붙이기 때문에, JS 가 로드되기 전에도 스타일이 적용된 채로 보인다. SSR 하는 웹 컴포넌트에서 깜빡임을 없애는 방법이다.