HTML은 모두 DOM으로 변환된다. DOM은 자바스크립트로 엑세스하고 CSS로 스타일링 할 수 있다. Shadow DOM은 요소 내부에 Shadow DOM 이라는 자체 DOM을 생성할 수 있다.
웹 사이트의 소스를 보다가 위와 같은 것을 발견한다면 그것이 쉐도우 돔이 적용된 것이다.
Shadow DOM을 사용하는 이유?
- 페이지의 다른 자바스크립트나 CSS로 부터 컴포넌트를 보호
- 컴포넌트에 대한 캡슐화된 스타일을 만들 수 있음
- 컴포넌트 내부 요소에 안전하게 ID를 사용할 수 있음
Shadow DOM 생성
const shadow = document.getElementById('shadow');
const shadowRoot = shadow.attachShadow({ mode: 'closed' });
쉐도우 돔은 위와 같이 간단하게 생성할 수 있다. mode는 open
, closed
2가지 값으로 설정할 수 있는데 두 옵션의 차이는 다음과 같다.
// open
const shadowRoot = shadow.attachShadow({ mode: 'open' });
shadow.shadowRoot // 접근 가능 (= shadowRoot)
// closed
const shadowRoot = shadow.attachShadow({ mode: 'open' });
shadow.shadowRoot // 접근 불가 (= null)
Sadow DOM 스타일링
쉐도우 돔은 기본적으로 외부 스타일에 영향을 받지 않는다. 예를들어 아래와 같은 코드가 있다고 가정하면
<style>
span {
color: red;
}
</style>
<span>Light DOM Content</span>
<div id="shadow"></div>
<script>
const shadow = document.getElementById('shadow');
const shadowRoot = shadow.attachShadow({ mode: 'closed' });
shadowRoot.innerHTML = `
<span>Shadow DOM Content</span>
`;
</script>
결과는 위와 같이 출력된다. 다만 일부 상황에서는 쉐도우 돔과 라이트 돔이 스타일을 공유하거나 영향을 줄 수 있다. 먼저 라이트 돔의 스타일이 쉐도우 돔의 영향을 주는 케이스다.
라이트 -> 쉐도우
1. 스타일 상속
Light DOM에서 Shadow DOM이 생성된 엘리먼트(Host)로 스타일이 상속될 경우 Shadow DOM에도 스타일이 반영된다.
<style>
body {
color: blue;
}
span {
color: red;
}
</style>
<span>Light DOM Content</span>
<div id="shadow"></div>
<script>
const shadow = document.getElementById('shadow');
const shadowRoot = shadow.attachShadow({ mode: 'closed' });
shadowRoot.innerHTML = `
<span>Shadow DOM Content</span>
`;
</script>
body의 스타일은 Shadow DOM의 호스트에도 상속되기 때문에 결과는 아래와 같아진다.
Shadow DOM에서 이러한 상속을 모두 제한하려는 경우 아래와 같이 호스트의 상속을 막을 수 있다.
shadowRoot.innerHTML = `
<style> :host { all: initial; } </style>
<span>Shadow DOM Content</span>
`;
그럼 다시 결과는 원점으로 돌아간다.
2. 사용자 정의 속성
:root
에 정의된 사용자 정의 속성(variant)은 Shadow DOM에서도 사용할 수 있다. 상속으로 스타일을 주입하는 것 보다 훨씬 더 예측 가능하고 사이드 이펙트가 적다.
<style>
:root {
--my-favorite-color: #735af2;
}
span {
color: var(--my-favorite-color);
}
</style>
<span>Light DOM Content</span>
<div id="shadow"></div>
<script>
const shadow = document.getElementById('shadow');
const shadowRoot = shadow.attachShadow({ mode: 'closed' });
shadowRoot.innerHTML = `
<style>
span {
color: var(--my-favorite-color);
}
</style>
<span>Shadow DOM Content</span>
`;
</script>
3, 의사 요소
part
속성을 이용해서 외부에서 Shadow DOM에 스타일을 주입할 수 있도록 할 수 있다.
<style>
span {
color: #735af2;
}
#shadow::part(shadow-content) {
color: #735af2;
}
</style>
<span>Light DOM Content</span>
<div id="shadow"></div>
<script>
const shadow = document.getElementById('shadow');
const shadowRoot = shadow.attachShadow({ mode: 'closed' });
shadowRoot.innerHTML = `
<span part="shadow-content">Shadow DOM Content</span>
`;
</script>
4. 템플릿 슬롯
커스텀 엘리먼트를 선언할 때 사용하는 슬롯을 사용할 때 스타일은 의도한 것과 다르게 들어가므로 유의해야 한다. Shadow DOM 내부에서 스타일이 주입되는 것이 아니라 외부에서 스타일이 주입되는 것에 유의해야 한다.
<style>
p {
color: blue;
}
</style>
<template id="my-component-template">
<style>
p {
color: red;
}
</style>
<div>
<slot></slot>
</div>
</template>
<my-component>
<p>External content</p>
</my-component>
<script>
class MyComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
const template = document.querySelector('#my-component-template');
this.shadowRoot.appendChild(template.content.cloneNode(true));
}
}
customElements.define('my-component', MyComponent);
</script>
쉐도우
1. 템플릿 슬롯
템플릿 내부에서 slot의 스타일을 지정하려는 경우, ::slotted
선택자를 사용하여 스타일을 적용할 수 있다.
<style>
p {
color: blue;
}
</style>
<template id="my-component-template">
<style>
::slotted(p) {
color: red !important;
}
</style>
<div>
<slot></slot>
</div>
</template>
<my-component>
<p>External content</p>
</my-component>
2. 호스트
위에서 잠시 언급이 되었지만 :host
선택자를 사용하면 쉐도우 돔을 렌더하고 있는 호스트의 스타일링을 해줄 수 있다.
<style>
span {
color: red;
}
</style>
<span>Light DOM Content</span>
<div id="shadow"></div>
<script>
const shadow = document.getElementById('shadow');
const shadowRoot = shadow.attachShadow({ mode: 'closed' });
shadowRoot.innerHTML = `
<style>
:host {
color: blue;
}
</style>
<span>Shadow DOM Content</span>
`;
</script>
:host-context
선택자를 사용하면 호스트의 부모 요소의 적용된 클래스를 참조하여 스타일링 할 수 있다. 예를들어 다크 모드와 같은 기능을 만들 때 특히 유용할 수 있다. 참고로 해당 글 작성일 기준으로 사파리와 파이어폭스에서는 동작하지 않는 기능이다.
<style>
body.dark {
background: black;
}
span {
color: red;
}
</style>
<span>Light DOM Content</span>
<div id="shadow"></div>
<script>
document.body.classList.add('dark');
const shadow = document.getElementById('shadow');
const shadowRoot = shadow.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `
<style>
:host-context(body.dark) span {
color: white;
}
</style>
<span>Shadow DOM Content</span>
`;
</script>
3. 스타일 임포트
Light DOM에서 Global로 사용하는 스타일이 있는 경우 간단하게 import 구문을 사용할 수 있다. 별도의 라이브러리나 별도의 처리 없이 브라우저 네이티브로 동작하며, 이미 다운로드된 스타일 시트의 경우 캐시되어 불필요한 리소스가 낭비되지 않는다.
<template id="my-component-template">
<style>
@import '/assets/style/normalize.css';
</style>
<div>
<slot></slot>
</div>
</template>
4. 생성 가능한 스타일 시트
Constructable Stylesheets는 Shadow DOM에서 공유 가능한 스타일시트를 제공하여, 재사용성과 성능을 극대화할 수 있다. 또한 엔드 유저에게 별도의 스타일이 노출되지 않는다.
<style>
span {
color: red;
}
</style>
<span>Light DOM Content</span>
<div id="shadow"></div>
<script>
const shadow = document.getElementById('shadow');
const shadowRoot = shadow.attachShadow({ mode: 'open' });
const styleSheet = new CSSStyleSheet();
styleSheet.replace(`
span {
color: blue;
}
`)
shadowRoot.adoptedStyleSheets = [styleSheet];
shadowRoot.innerHTML = `
<span>Shadow DOM Content</span>
`;
</script>
Ghost