JavaScript의 섀도우 DOM

JavaScript의 섀도우 DOM

이 글에서는 JavaScript의 섀도우 DOM에 대해 설명합니다.

YouTube Video

javascript-html-shadow-dom.html
  1<!DOCTYPE html>
  2<html lang="en">
  3<head>
  4  <meta charset="UTF-8">
  5  <title>JavaScript &amp; HTML</title>
  6  <style>
  7    * {
  8        box-sizing: border-box;
  9    }
 10
 11    body {
 12        margin: 0;
 13        padding: 1em;
 14        padding-bottom: 10em;
 15        font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
 16        background-color: #f7f9fc;
 17        color: #333;
 18        line-height: 1.6;
 19    }
 20
 21    .container {
 22        max-width: 800px;
 23        margin: 0 auto;
 24        padding: 1em;
 25        background-color: #ffffff;
 26        border: 1px solid #ccc;
 27        border-radius: 10px;
 28        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
 29    }
 30
 31    h1, h2 {
 32        font-size: 1.2rem;
 33        color: #007bff;
 34        margin-top: 0.5em;
 35        margin-bottom: 0.5em;
 36        border-left: 5px solid #007bff;
 37        padding-left: 0.6em;
 38        background-color: #e9f2ff;
 39    }
 40
 41    button {
 42        display: block;
 43        margin: 1em auto;
 44        padding: 0.75em 1.5em;
 45        font-size: 1rem;
 46        background-color: #007bff;
 47        color: white;
 48        border: none;
 49        border-radius: 6px;
 50        cursor: pointer;
 51        transition: background-color 0.3s ease;
 52    }
 53
 54    button:hover {
 55        background-color: #0056b3;
 56    }
 57
 58    #output {
 59        margin-top: 1em;
 60        background-color: #1e1e1e;
 61        color: #0f0;
 62        padding: 1em;
 63        border-radius: 8px;
 64        min-height: 200px;
 65        font-family: Consolas, monospace;
 66        font-size: 0.95rem;
 67        overflow-y: auto;
 68        white-space: pre-wrap;
 69    }
 70
 71    .highlight {
 72        outline: 3px solid #ffc107; /* yellow border */
 73        background-color: #fff8e1;  /* soft yellow background */
 74        transition: background-color 0.3s ease, outline 0.3s ease;
 75    }
 76
 77    .active {
 78        background-color: #28a745; /* green background */
 79        color: #fff;
 80        box-shadow: 0 0 10px rgba(40, 167, 69, 0.5);
 81        transition: background-color 0.3s ease, box-shadow 0.3s ease;
 82    }
 83  </style>
 84</head>
 85<body>
 86    <div class="container">
 87        <h2>HTML Sample</h2>
 88        <div id="content"></div>
 89        <div id="shadow-host">Shadow Root Element</div>
 90        <my-card></my-card>
 91    </div>
 92
 93    <div class="container">
 94        <h1>JavaScript Console</h1>
 95        <button id="executeBtn">Execute</button>
 96        <div id="output"></div>
 97    </div>
 98
 99    <div>
100        <h2>Slot Sample</h2>
101        <my-element>
102            <h3 slot="header">Header Content</h1>
103            <p slot="content">Main Content</p>
104        </my-element>
105    </div>
106
107    <script>
108        // Override console.log to display messages in the #output element
109        (function () {
110            const originalLog = console.log;
111            console.log = function (...args) {
112                originalLog.apply(console, args);
113                const output = document.getElementById('output');
114                output.textContent += args.map(String).join(' ') + '\n';
115            };
116        })();
117
118        document.getElementById('executeBtn').addEventListener('click', () => {
119            // Prevent multiple loads
120            if (document.getElementById('externalScript')) return;
121
122            const script = document.createElement('script');
123            script.src = 'javascript-html-shadow-dom.js';
124            script.id = 'externalScript';
125            //script.onload = () => console.log('javascript-html-shadow-dom.js loaded and executed.');
126            //script.onerror = () => console.log('Failed to load javascript-html-shadow-dom.js.');
127            document.body.appendChild(script);
128        });
129    </script>
130</body>
131</html>

쉐도우 DOM 이해하기

쉐도우 DOM은 웹 컴포넌트 표준의 강력한 기능으로, 컴포넌트 내에서 스타일과 DOM 구조를 캡슐화할 수 있도록 합니다. 이 기능은 컴포넌트와 메인 문서 간의 스타일 및 스크립트 간섭을 방지합니다.

JavaScript의 섀도우 DOM

쉐도우 DOM은 일반 DOM 요소와 연결된 스코프 DOM 트리를 만드는 방법을 제공합니다. 이 쉐도우 트리는 전체 문서에서 격리되어 외부 스타일과 스크립트가 영향을 미치지 않으며, 내부 스타일 및 스크립트도 외부로 누출되지 않습니다.

예를 들어, 쉐도우 DOM을 사용하여 사용자 정의 버튼 컴포넌트를 만들면 해당 스타일이 페이지의 다른 요소에 영향을 미치지 않습니다. 마찬가지로 동일한 클래스 이름을 가진 요소들도 충돌하지 않습니다.

섀도우 DOM 외부의 일반 HTML 콘텐츠를 **라이트 DOM**이라고 부릅니다.

쉐도우 DOM의 장점

  1. 캡슐화

    • 섀도우 DOM은 스타일과 기능을 분리하여 전역 스타일 및 스크립트와의 충돌을 방지합니다.
  2. 재사용성

    • 섀도우 DOM으로 구성된 컴포넌트는 스타일 충돌 걱정 없이 여러 프로젝트에서 재사용할 수 있습니다.
  3. 유지보수성

    • 캡슐화 덕분에 컴포넌트의 로직과 스타일이 독립적으로 관리되어, 디버깅과 유지보수가 쉬워집니다.

쉐도우 DOM 생성하기

쉐도우 DOM을 사용하려면 HTML 요소에 쉐도우 루트를 첨부해야 합니다. 간단한 예시는 다음과 같습니다:.

1// Select the host element
2const hostElement = document.querySelector('#shadow-host');
3
4// Attach a shadow root
5const shadowRoot = hostElement.attachShadow({ mode: 'open' });

설명

이 코드는 다음과 같은 요소를 포함합니다:.

  1. 호스트 요소

    • 섀도우 루트가 연결되는 일반 DOM 요소입니다 (여기서는 #shadow-host).
  2. 섀도우 루트

    • attachShadow로 생성된 섀도우 트리의 루트입니다.
  3. 모드

    • open 모드에서는 외부 JavaScript가 element.shadowRoot를 통해 섀도우 루트에 접근할 수 있습니다. 반면에, closed 모드는 접근을 허용하지 않습니다.

쉐도우 DOM 내에서 스타일링하기

Shadow DOM은 자체 스타일 범위를 가지고 있습니다. Shadow 트리 내에서 정의된 스타일은 해당 트리 내의 요소에만 적용됩니다. 다음은 예제입니다:.

1// Add content to the shadow root
2shadowRoot.innerHTML = `
3    <style>
4        p {
5            color: green;
6        }
7    </style>
8    <p>Scoped style inside Shadow DOM.</p>
9`;

메인 문서에 충돌하는 스타일이 있더라도, 섀도우 트리 내부의 단락에는 영향을 주지 않습니다.

1const content = document.getElementById('content');
2content.innerHTML = `
3    <style>
4        p {
5            color: red;
6        }
7    </style>
8    <p>This is in the main DOM.</p>
9`;
  • 섀도우 DOM 내의 단락은 초록색을 유지하며, 외부 단락은 빨간색이 됩니다.

Shadow DOM 내에서의 이벤트

Shadow DOM 내의 이벤트는 일반 DOM 이벤트와 유사하지만 캡슐화로 인해 전파 방식이 다를 수 있습니다.

다음은 예제입니다:.

 1// Add an event listener inside Shadow DOM
 2shadowRoot.innerHTML = `
 3    <div id="button-container">
 4        <button id="shadow-button">Click Me</button>
 5    </div>
 6`;
 7
 8shadowRoot.querySelector('#shadow-button').addEventListener('click', (event) => {
 9    console.log('Button : Button clicked inside Shadow DOM');
10    console.log(event.target);
11});
12
13shadowRoot.querySelector('#button-container').addEventListener('click', (event) => {
14    console.log('Container : Button clicked inside Shadow DOM');
15    console.log(event.target);
16});
17
18hostElement.addEventListener('click', (event) => {
19    console.log('Event bubbled to the host element');
20    console.log(event.target);
21});
22
23document.addEventListener('click', (event) => {
24    console.log('Document listener');
25    console.log(event.target);
26});
  • 버튼이 클릭되면 두 리스너가 모두 작동하여 이벤트 버블링의 동작을 보여줍니다.
  • Shadow DOM 내부에서 발생한 이벤트가 Light DOM으로 버블링될 때, 이벤트의 target은 원래의 소스 대신 호스트 요소로 변경됩니다.
    • 이 예시에서 Shadow DOM 내부의 event.target은 실제 button 요소이지만, Shadow DOM 외부에서는 호스트 div 요소로 대체됩니다.

Shadow DOM과 커스텀 요소

Shadow DOM과 커스텀 요소는 Web Components의 핵심 구성 요소입니다. 이 기술들은 재사용 가능하고 캡슐화된 UI 컴포넌트를 만들기 위해 사용됩니다.

 1class MyCard extends HTMLElement {
 2    constructor() {
 3        super();
 4        const shadow = this.attachShadow({ mode: 'open' });
 5        shadow.innerHTML = `
 6        <style>
 7            p {
 8                color: blue;
 9            }
10        </style>
11        <p>I'm inside shadow DOM</p>
12        `;
13    }
14}
15
16customElements.define('my-card', MyCard);

메인 문서는 다음과 같은 HTML을 포함합니다:.

1<my-card></my-card>
  • 커스텀 요소 내부에서 Shadow DOM을 사용하면 스타일 충돌에 강한 재사용 가능한 컴포넌트를 만들 수 있습니다. 이 코드에서는 my-card라는 태그가 생성되어 MyCard 클래스와 연결됩니다. my-card 내부의 <p> 요소는 외부 스타일의 영향을 받지 않으며 항상 파란색으로 표시됩니다.

슬롯: 라이트 DOM 콘텐츠 배포

슬롯을 사용하면 라이트 DOM 콘텐츠를 섀도우 DOM으로 투영할 수 있습니다. 다음은 예제입니다:.

 1class MyElement extends HTMLElement {
 2    constructor() {
 3        super();
 4        const shadow = this.attachShadow({ mode: 'open' });
 5
 6        shadow.innerHTML = `
 7        <style>
 8            .container {
 9                border: 2px solid #ccc;
10                padding: 16px;
11                border-radius: 8px;
12                font-family: sans-serif;
13            }
14            .header {
15                font-size: 1.2em;
16                color: darkblue;
17                margin-bottom: 8px;
18            }
19            .content {
20                font-size: 1em;
21                color: #333;
22            }
23        </style>
24        <div class="container">
25            <div class="header">
26                <slot name="header"></slot>
27            </div>
28            <div class="content">
29                <slot name="content"></slot>
30            </div>
31        </div>
32        `;
33    }
34}
35
36customElements.define('my-element', MyElement);

메인 문서는 다음과 같은 HTML을 포함합니다:.

1<my-element>
2    <h3 slot="header">Header Content</h1>
3    <p slot="content">Main Content</p>
4</my-element>
  • 섀도우 DOM 내의 slot 요소는 해당 slot 속성이 있는 라이트 DOM 콘텐츠를 표시합니다.

결론

Shadow DOM은 강력하고 재사용 가능하며 유지보수하기 쉬운 웹 컴포넌트를 만들기 위한 중요한 도구입니다. 스타일과 기능을 캡슐화함으로써 충돌 가능성을 줄이고 코드베이스 관리를 단순화합니다.

위의 기사를 보면서 Visual Studio Code를 사용해 우리 유튜브 채널에서 함께 따라할 수 있습니다. 유튜브 채널도 확인해 주세요.

YouTube Video