Shadow DOM

Shadow DOM은 무엇일까?

DOM(Document Object Model)은 HTML 문서의 구조화된 표현입니다. 이것은 브라우저가 페이지에 무엇을 렌더링 할지 결정하기 위해, 혹은 자바스크립트 프로그램이 페이지의 콘텐츠 및 구조, 스타일을 수정하기 위해 사용됩니다.

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
<title>My first web page</title>
</head>
<body>
<h1>Hello, world!</h1>
<p>How are you?</p>
</body>
</html>

위의 HTML 문서는 다음과 같은 DOM 트리를 생성합니다.

DOM 트리 구조

지난 몇 년 동안 Shadow DOMVirtual DOM이라는 용어를 들어보셨을 겁니다. 이들은 DOM과 관련이 있지만 매우 다른 개념을 가리킵니다.
이 문서에서는 shadow DOM이 무엇인지, 그리고 기존의 DOM과 어떻게 다른지에 대해 다루도록 하겠습니다.

HTML 문서의 모든 요소와 스타일로 이루어진 DOM은 하나의 큰 글로벌 범위 내에 있습니다.
페이지의 요소가 문서 내에 깊이 중첩되어 있거나 어디에 배치되어있는지 상관없이 document.querySelector() 메서드를 사용하여 접근이 가능합니다. 마찬가지로, CSS 스타일 또한 글로벌 범위 내의 어떤 요소든 선택이 가능합니다.

문서 전체에 스타일을 일괄 적용하고 싶을 때 이러한 방식은 매우 유용합니다. 예를 들어, box-sizing 속성을 사용한 한 줄의 코드를 통해 페이지에 있는 모든 단일 요소를 선택할 수 있습니다.

1
* { box-sizing: border-box }

반면에 어떤 요소는 완전한 캡슐화를 필요로 하는 경우가 있고, 이것이 글로벌 스타일에 영향을 받는 것을 원하지 않을 수 있습니다.
이에 대한 좋은 예는 트위터의 “follow” 버튼과 같이 외부에서 가져온 위젯을 들 수 있습니다.

1
<iframe scrolling="no" frameborder="0" allowtransparency="true" src="https://platform.twitter.com/widgets/follow_button.d30011b0f5ce05b98f24b01d3331b3c1.en.html#dnt=false&amp;id=twitter-widget-0&amp;lang=en&amp;screen_name=ireaderinokun&amp;show_count=false&amp;show_screen_name=true&amp;size=m&amp;time=1546395468133"></iframe>

Javascript를 활성화하고 요소를 검사한다고 가정할 때 이 버튼이 <iframe> 요소라는 것을 알 수 있는데, 이 요소는 실제로 보이는 스타일 버튼과 함께 작은 문서를 로드합니다.

iframe 내부 소스코드

<iframe> 은 트위터의 위젯이 호스팅 문서의 전역 CSS에 영향을 받지 않고 의도된 스타일을 보장할 수 있는 방법입니다. 같은 결과를 얻기 위해 캐스케이드를 이용할 수 있지만, 다른 방법으로는 <iframe> 과 같은 보장이 주어지지 않으며 이상적인 방법은 아닙니다.

Shadow DOM은 <iframe>과 같은 도구에 의존할 필요 없이, 웹 플랫폼에서 기본적으로 캡슐화와 구성요소화를 허용하기 위해 만들어졌습니다.

A DOM within a DOM

Shadow DOM을 “DOM 내의 DOM”으로 생각할 수도 있지만, 원래의 DOM 트리에서 완전히 분리된 고유의 요소와 스타일을 가진 DOM 트리입니다.

Shadow DOM은 웹 작성자가 사용하도록 최근에 지정되었지만, 사용자 에이전트에서 폼 요소와 같이 복잡한 구성요소를 만들고 스타일을 입히기 위해 수년 동안 사용되어 왔습니다.
예를 들어 범위 입력 요소를 살펴보겠습니다. 페이지에 해당 요소를 생성하기 위해서는 아래의 코드를 추가해야 합니다.

1
<input type="range">

이 요소로 인해 다음과 같은 구성 요소가 생성됩니다.

input type=range 요소

더 깊게 파고들면, <input> 요소가 실제로 여러 작은 <div> 요소로 구성되어 트랙과 슬라이더를 자체적으로 제어하는 것을 볼 수 있습니다.

input type=range - Shadow DOM

웹브라우저의 네이티브 Shadow DOM 구조는 Chrome 브라우저 개발자 도구 > Settings > Preferences > Elements > Show user agent shadow DOM 체크박스 옵션을 활성화 해야 확인할 수 있습니다.

chrome 개발자도구 - agent shadow DOM 옵션 활성화

How the shadow DOM works

Shadow DOM이 어떻게 작동하는지 설명하기 위해 <iframe> 대신 shadow DOM을 사용하여 트위터의 “follow” 버튼을 만들어 보겠습니다.

먼저 shadow host로 시작합니다.

shadow host는 새로운 shadow DOM을 붙일 원본 DOM의 일반 HTML 요소를 사용합니다. Follow 버튼과 같은 구성요소의 경우, 페이지에 Javascript가 활성화되지 않았거나 shadow DOM이 지원되지 않을 경우 표시할 폴백 요소를 포함할 수 있습니다.

1
2
3
4
5
<span class="shadow-host">
<a href="https://twitter.com/ireaderinokun">
Follow @ireaderinokun
</a>
</span>

주로 상호 작용하는 특정 요소들은 shadow host가 될 수 없기 때문에, 단순히 요소를 shadow host로 사용할 수 없습니다.

호스트에 shadow DOM을 붙이기 위해, attachShadow() 메서드를 사용합니다.

1
2
const shadowEl = document.querySelector('.shadow-host');
const shadow = shadowEl.attachShadow({mode: 'open'});

이 코드는 shadow host의 자식 요소인 빈 shadow root를 생성합니다. <html> 요소가 DOM의 시작인 것처럼 shadow root는 shadow DOM의 시작점 역할을 합니다.

shadow root 생성

일반 HTML 자식 요소는 검사기에서 확인될지라도 shadow root가 차지하면서 더 이상 페이지에 보이지 않게 됩니다.

다음으로, 새로운 shadow tree를 만들기 위해 콘텐츠를 생성해야 합니다. shadow tree는 DOM tree와 비슷하지만 일반 DOM 대신 shadow DOM을 사용합니다.
follow 버튼을 생성하기 위해서는 이미 가지고 있는 폴백 링크와 거의 동일하지만 아이콘이 있는 새로운 <a> 요소가 필요합니다.

1
2
3
4
5
6
7
const link = document.createElement('a');

link.href = shadowEl.querySelector('a').href;
link.innerHTML = `
<span aria-label="Twitter icon"></span>
${shadowEl.querySelector('a').textContent}
`;

일반적인 방법과 동일하게 appendChild() 메서드를 사용하여 shadow DOM에 새로운 요소를 추가합니다.

1
shadow.appendChild(link);

이 시점에서 해당 요소는 아래와 같습니다.

shadow root 적용

마지막으로 <style> 요소를 만들고 shadow root에 추가함으로써 몇가지 스타일을 적용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
const styles = document.createElement('style');

styles.textContent = `
a, span {
vertical-align: top;
display: inline-block;
box-sizing: border-box;
}

a {
height: 20px;
padding: 1px 8px 1px 6px;
background-color: #1b95e0;
color: #fff;
border-radius: 3px;
font-weight: 500;
font-size: 11px;
font-family:'Helvetica Neue', Arial, sans-serif;
line-height: 18px;
text-decoration: none;
}

a:hover {
background-color: #0c7abf;
}

span {
position: relative;
top: 2px;
width: 14px;
height: 14px;
margin-right: 3px;
background: transparent 0 0 no-repeat;
background-image: url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2072%2072%22%3E%3Cpath%20fill%3D%22none%22%20d%3D%22M0%200h72v72H0z%22%2F%3E%3Cpath%20class%3D%22icon%22%20fill%3D%22%23fff%22%20d%3D%22M68.812%2015.14c-2.348%201.04-4.87%201.744-7.52%202.06%202.704-1.62%204.78-4.186%205.757-7.243-2.53%201.5-5.33%202.592-8.314%203.176C56.35%2010.59%2052.948%209%2049.182%209c-7.23%200-13.092%205.86-13.092%2013.093%200%201.026.118%202.02.338%202.98C25.543%2024.527%2015.9%2019.318%209.44%2011.396c-1.125%201.936-1.77%204.184-1.77%206.58%200%204.543%202.312%208.552%205.824%2010.9-2.146-.07-4.165-.658-5.93-1.64-.002.056-.002.11-.002.163%200%206.345%204.513%2011.638%2010.504%2012.84-1.1.298-2.256.457-3.45.457-.845%200-1.666-.078-2.464-.23%201.667%205.2%206.5%208.985%2012.23%209.09-4.482%203.51-10.13%205.605-16.26%205.605-1.055%200-2.096-.06-3.122-.184%205.794%203.717%2012.676%205.882%2020.067%205.882%2024.083%200%2037.25-19.95%2037.25-37.25%200-.565-.013-1.133-.038-1.693%202.558-1.847%204.778-4.15%206.532-6.774z%22%2F%3E%3C%2Fsvg%3E);
}
`;

shadow.appendChild(styles);

이렇게 생성된 요소는 다음과 같습니다.

shadow root 결과화면


The DOM vs the shadow DOM

어떤 면에서 shadow DOM은 DOM의 “lite” 버전입니다.
DOM과 같이 HTML 요소의 구조화된 표현이며, 페이지에 무엇을 표시할지 결정하고 요소의 수정을 가능하게 합니다. 하지만 DOM과 다르게 완전한 독립 문서를 기반으로 하지 않습니다.
이름에서 알 수 있듯이 shadow DOM은 항상 일반 DOM 내의 요소에 부착됩니다. DOM이 없으면 shadow DOM도 존재하지 않습니다.


slot

슬롯은 사용자가 컴포넌트 내부에 원하는 마크업을 채울 수 있도록 미리 선언해놓은 자리 표시자입니다.
주로 사용자 커스텀 요소를 생성할 때 유용합니다. 사용자 커스텀 요소에 필요한 최소한의 마크업만 제공하고 작성자가 원하는 대로 그룹화하고 스타일을 적용하여 사용할 수 있습니다.

슬롯을 설명하기 전에 <template>을 사용한 마크업 예시를 먼저 살펴보겠습니다.

1
2
3
4
5
6
7
8
9
10
<!-- 렌더링할 템플릿 선언 -->
<template id="my-template">
<style>
p { color: green; }
</style>
<p>Hello, Shadow DOM!</p>
</template>

<!-- 사용자 커스텀 요소 사용 -->
<my-template></my-template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 사용자 커스텀 요소를 정의하고 준비한 템플릿 코드를 가져와 shadow DOM을 생성합니다.
// shadow DOM으로 인해 템플릿 내의 코드는 캡슐화됩니다.
class myTemplate extends HTMLElement {
constructor() {
super();
let template = document.getElementById('my-template');
let templateContent = template.content;

const shadowRoot = this.attachShadow({mode: 'open'})
.appendChild(templateContent.cloneNode(true));
}
}

customElements.define('my-template', myTemplate);

템플릿 요소는 마크업 조각 형태로 이루어집니다. 이는 페이지 로딩 시 렌더링 되지 않으며 자바스크립트를 이용해 런타임 시 인스턴스화할 수 있습니다.
따라서 자주 사용되는 마크업 조각들을 템플릿 요소에 추가하고 복제함으로써 재사용성을 증가시킵니다. 또한 템플릿 요소가 shadow host로 지정되어 내부 스타일을 가질 수 있습니다.

하지만 템플릿은 단순히 작성된 요소만 화면에 표시하기 때문에 유연하지 않습니다. 슬롯은 이러한 템플릿 코드에 유연성을 제공합니다.
슬롯을 사용한 템플릿 코드 예시를 살펴보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- 빈 슬롯이 추가된 템플릿 선언 -->
<template id="my-template">
<style>
:host { color: green; }
</style>
<slot></slot>
</template>

<!-- 각각의 사용자 커스텀 요소마다 다른 요소를 삽입 -->
<my-template>
<h1>Hello Shadow DOM!</h1>
</my-template>
<my-template>
<p>Hello, Shadow DOM!</p>
</my-template>

하나의 슬롯을 사용했지만 결과적으로는 다른 두 요소를 렌더링 합니다.

slot 결과화면

슬롯은 shadow DOM에서 사용됩니다. 즉, shadow root에 추가되는 템플릿 코드 내에 슬롯을 작성해야 합니다.
빈 슬롯을 추가한 템플릿을 생성한 후, 사용자 커스텀 요소에서 해당 슬롯에 배치하고 싶은 요소를 추가하여 사용할 수 있습니다. 슬롯을 통해 다양한 요소들이 하나의 템플릿에서 구현 가능하므로 매우 유용합니다.

named slot

다양한 콘텐츠로 이루어진 복잡한 요소는 명명된 슬롯을 사용하여 쉽게 생성할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
<!-- 템플릿 선언 -->
<template id="my-template">
<slot name="title"></slot>
<hr>
<slot></slot>
</template>

<!-- 사용자 커스텀 요소 -->
<my-template>
<h1 slot="title">제목</h1>
<p>이 텍스트는 이름 없는 빈 슬롯에 들어가게 됩니다.</p>
</my-template>

출력된 결과는 아래와 같습니다.

named slot 결과화면

참고: <my-template> 내의 <h1>, <p>과 같은 자식 요소들을 Light DOM이라고 합니다. 이들은 템플릿 코드에 있는 지정된 slot을 찾아갑니다.

슬롯 요소에는 name 속성을 사용합니다. 그리고 원하는 슬롯에 배치할 light DOM 요소에는 slot 속성을 사용하며, 해당 속성값으로 슬롯의 name 값을 지정해줍니다.

스타일 지정

웹 구성 요소와 shadow DOM 내부 요소에 스타일을 지정하는 다양한 방법이 있습니다.

  • :host: shadow root로 지정된 웹 구성 요소에 스타일을 적용합니다.
  • :host-context(<selector>): 웹 구성 요소 혹은 상위 요소의 선택자가 <selector>와 일치하면, 웹 구성 요소의 자식 요소에 스타일을 적용합니다.
  • ::slotted(<compound-selector>): 지정한 복합 선택자와 일치하는 슬롯 콘텐츠에 스타일을 적용합니다.

간단한 예시를 살펴보시면,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<!-- 템플릿 선언 -->
<template id="my-template">
<style>
:host {
all: initial;
display: block;
contain: content;
color: green;
}
:host(:hover) {
border: 1px solid blue;
}

:host-context(.orange-theme) {
color: orange;
}

::slotted(a) {
color: red;
text-decoration: none;
}
</style>
<slot></slot>
</template>

<!-- 사용자 커스텀 요소 -->
<my-template>
<h1>Hello Shadow DOM!</h1>
</my-template>

<my-template class="orange-theme">
<div>
<span>text 1</span>
<span>text 2</span>
<span>text 3</span>
</div>
</my-template>

<my-template>
<a href="#">Hello, Shadow DOM!</a>
</my-template>

아래와 같이 사용자 커스텀 요소에 지정된 스타일이 적용됩니다.

shadow DOM 내부 스타일

보다 자세한 스타일 지정 방식은 https://developers.google.com/web/fundamentals/web-components/shadowdom#styling 에서 확인하실 수 있습니다.

참조

공유하기