본 번역문은 원작자의 동의하에 번역 및 게시되었습니다.

브라우저에서 제공하는 <video> 요소의 재생 버튼, 타임라인은 어떻게 생성되는 걸까요?
얼마 전 번역한 DOM 아티클 작성자가 shadow DOM을 주제로 What is the Shadow DOM? 글을 작성했습니다. 꽤 오래된 개념이지만 이번 기회에 shadow DOM에 대해 자세히 알아보고자 이 글을 번역해보았습니다.
원문에서 다루지 않는 “slot” 개념은 다른 기술 문서를 참고하여 내용 하단부에 추가하였습니다.

Shadow DOM은 무엇일까?

몇 주 전에 what exactly the DOM is 라는 아티클을 작성했습니다. 요약하자면, DOM(Document Object Model)은 HTML 문서의 구조화된 표현입니다. 이것은 브라우저가 페이지에 무엇을 렌더링 할지 결정하기 위해, 혹은 자바스크립트 프로그램이 페이지의 콘텐츠 및 구조, 스타일을 수정하기 위해 사용됩니다.

<!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 트리를 생성합니다.

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

Everything is global 👍🏾! Wait, everything is global 👎🏾

HTML 문서의 모든 요소와 스타일로 이루어진 DOM은 하나의 큰 글로벌 범위 내에 있습니다.
페이지의 요소가 문서 내에 깊이 중첩되어 있거나 어디에 배치되어있는지 상관없이 document.querySelector() 메서드를 사용하여 접근이 가능합니다. 마찬가지로, CSS 스타일 또한 글로벌 범위 내의 어떤 요소든 선택이 가능합니다.
문서 전체에 스타일을 일괄 적용하고 싶을 때 이러한 방식은 매우 유용합니다. 예를 들어, box-sizing 속성을 사용한 한 줄의 코드를 통해 페이지에 있는 모든 단일 요소를 선택할 수 있습니다.

* { box-sizing: border-box }

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

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

<iframe> 은 트위터의 위젯이 호스팅 문서의 전역 CSS에 영향을 받지 않고 의도된 스타일을 보장할 수 있는 방법입니다. 같은 결과를 얻기 위해 캐스케이드를 이용할 수 있지만, 다른 방법으로는 <iframe> 과 같은 보장이 주어지지 않으며 이상적인 방법은 아닙니다.
Shadow DOM은 <iframe>과 같은 도구에 의존할 필요 없이, 웹 플랫폼에서 기본적으로 캡슐화와 구성요소화를 허용하기 위해 만들어졌습니다.

A DOM within a DOM

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

<input type="range">

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

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

이처럼 Shadow DOM을 사용하여 위와 같은 결과를 얻을 수 있습니다. 호스트 HTML 문서에는 단순한 <input> 요소가 노출되지만, 그 내부에는 DOM의 글로벌 범위에 포함되지 않는 HTML 요소와 스타일 구성 요소들이 있습니다.

How the shadow DOM works

Shadow DOM이 어떻게 작동하는지 설명하기 위해 <iframe> 대신 shadow DOM을 사용하여 트위터의 “follow” 버튼을 만들어 보겠습니다.
먼저 shadow host로 시작합니다.
shadow host는 새로운 shadow DOM을 붙일 원본 DOM의 일반 HTML 요소를 사용합니다. Follow 버튼과 같은 구성요소의 경우, 페이지에 Javascript가 활성화되지 않았거나 shadow DOM이 지원되지 않을 경우 표시할 폴백 요소를 포함할 수 있습니다.

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

주로 상호 작용하는 특정 요소들은 shadow host가 될 수 없기 때문에, 단순히 <a> 요소를 shadow host로 사용할 수 없습니다.
호스트에 shadow DOM을 붙이기 위해, attachShadow() 메서드를 사용합니다.

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

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

일반 HTML 자식 요소는 검사기에서 확인될지라도 shadow root가 차지하면서 더 이상 페이지에 보이지 않게 됩니다.
다음으로, 새로운 shadow tree를 만들기 위해 콘텐츠를 생성해야 합니다. shadow tree는 DOM tree와 비슷하지만 일반 DOM 대신 shadow DOM을 사용합니다.
follow 버튼을 생성하기 위해서는 이미 가지고 있는 폴백 링크와 거의 동일하지만 아이콘이 있는 새로운 <a> 요소가 필요합니다.

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에 새로운 요소를 추가합니다.

shadow.appendChild(link);

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

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

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);

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

The DOM vs the shadow DOM

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


추가 내용

원문에서는 shadow DOM의 슬롯에 대한 언급이 없었기 때문에 추가적으로 간단히 다뤄보겠습니다.

slot

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

<!-- 렌더링할 템플릿 선언 -->
<template id="my-template">
    <style>
        p { color: green; }
    </style>
    <p>Hello, Shadow DOM!</p>
</template>
<!-- 사용자 커스텀 요소 사용 -->
<my-template></my-template>
// 사용자 커스텀 요소를 정의하고 준비한 템플릿 코드를 가져와 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로 지정되어 내부 스타일을 가질 수 있습니다.
하지만 템플릿은 단순히 작성된 요소만 화면에 표시하기 때문에 유연하지 않습니다. 슬롯은 이러한 템플릿 코드에 유연성을 제공합니다.
슬롯을 사용한 템플릿 코드 예시를 살펴보겠습니다.

<!-- 빈 슬롯이 추가된 템플릿 선언 -->
<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>

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

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

named slot

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

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

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

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

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

스타일 지정

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

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

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

<!-- 템플릿 선언 -->
<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>

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

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

참고 자료


5개의 댓글

Nobody · 2019년 11월 22일 10:54 오전

좋은 글 감사합니다

noone · 2020년 4월 22일 11:05 오전

좋은 글이네요.. 잘 읽었습니다 감사합니다.

ㅇㅇ · 2021년 1월 29일 4:23 오후

잘 읽었습니다! 감사합니다.

잘읽었습니다 · 2021년 3월 24일 10:18 오전

개발은 알면 알수록 신기한 세계,
끝이란 없고 없어서 아름다운 것

allman · 2021년 8월 19일 10:48 오후

알면 알 수록 내가 어디까지 알고있는건지 모르겠네요…ㅎㅎ
감사합니다. 오늘도 반성합니다.

답글 남기기

아바타 플레이스홀더

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다