Shadow DOM 시작하기

Dominic Cooney
Dominic Cooney

소개

Web Components는 다음과 같은 최신 표준 집합입니다.

  1. 위젯 빌드 가능
  2. …신뢰성 있게 재사용할 수 있습니다.
  3. 다음 버전의 구성요소에서 내부 구현 세부정보가 변경되더라도 페이지가 손상되지 않습니다.

즉, HTML/JavaScript를 사용해야 하는 경우와 웹 구성요소를 사용해야 하는 경우를 결정해야 하나요? 아니요 HTML과 JavaScript를 사용하면 양방향 시각적 요소를 만들 수 있습니다. 위젯은 대화형 시각적 요소입니다. 위젯을 개발할 때는 HTML 및 JavaScript 기술을 활용하는 것이 좋습니다. Web Components 표준은 이를 지원하도록 설계되었습니다.

하지만 HTML과 JavaScript로 빌드된 위젯을 사용하기 어렵게 만드는 근본적인 문제가 있습니다. 위젯 내부의 DOM 트리가 페이지의 나머지 부분과 캡슐화되지 않습니다. 캡슐화가 이루어지지 않으면 문서 스타일시트가 실수로 위젯 내부에 있는 부분에 적용될 수 있고, JavaScript가 실수로 위젯 내부의 부분을 수정할 수 있으며, ID가 위젯 내부의 ID와 겹칠 수 있습니다.

웹 구성요소는 다음 세 부분으로 구성됩니다.

  1. 템플릿
  2. Shadow DOM
  3. 맞춤 요소

Shadow DOM은 DOM 트리 캡슐화 문제를 해결합니다. 웹 구성요소의 네 부분은 함께 작동하도록 설계되었지만 사용할 웹 구성요소의 부분을 선택할 수도 있습니다. 이 튜토리얼에서는 섀도우 DOM을 사용하는 방법을 보여줍니다.

Hello, Shadow World

Shadow DOM을 사용하면 요소와 연결된 새로운 종류의 노드를 가져올 수 있습니다. 이러한 새로운 종류의 노드를 섀도우 루트라고 합니다. 섀도우 루트가 연결된 요소를 섀도우 호스트라고 합니다. 그림 호스트의 콘텐츠는 렌더링되지 않으며 대신 그림 루트의 콘텐츠가 렌더링됩니다.

예를 들어 다음과 같은 마크업이 있다고 가정해 보겠습니다.

<button>Hello, world!</button>
<script>
var host = document.querySelector('button');
var root = host.createShadowRoot();
root.textContent = 'こんにちは、影の世界!';
</script>

대신

<button id="ex1a">Hello, world!</button>
<script>
function remove(selector) {
  Array.prototype.forEach.call(
      document.querySelectorAll(selector),
      function (node) { node.parentNode.removeChild(node); });
}

if (!HTMLElement.prototype.createShadowRoot) {
  remove('#ex1a');
  document.write('<img src="SS1.png" alt="Screenshot of a button with \'Hello, world!\' on it.">');
}
</script>

페이지가 다음과 같이 표시됩니다.

<button id="ex1b">Hello, world!</button>
<script>
(function () {
  if (!HTMLElement.prototype.createShadowRoot) {
    remove('#ex1b');
    document.write('<img src="SS2.png" alt="Screenshot of a button with \'Hello, shadow world!\' in Japanese on it.">');
    return;
  }
  var host = document.querySelector('#ex1b');
  var root = host.createShadowRoot();
  root.textContent = 'こんにちは、影の世界!';
})();
</script>

뿐만 아니라 페이지의 JavaScript가 버튼의 textContent를 묻는 경우 섀도우 루트 아래의 DOM 하위 트리가 캡슐화되어 있으므로 'こんにちは、影の世界!'가 아닌 'Hello, world!'가 표시됩니다.

콘텐츠와 프레젠테이션 분리

이제 Shadow DOM을 사용하여 콘텐츠를 프레젠테이션과 분리하는 방법을 살펴보겠습니다. 다음과 같은 이름표가 있다고 가정해 보겠습니다.

<style>
.ex2a.outer {
  border: 2px solid brown;
  border-radius: 1em;
  background: red;
  font-size: 20pt;
  width: 12em;
  height: 7em;
  text-align: center;
}
.ex2a .boilerplate {
  color: white;
  font-family: sans-serif;
  padding: 0.5em;
}
.ex2a .name {
  color: black;
  background: white;
  font-family: "Marker Felt", cursive;
  font-size: 45pt;
  padding-top: 0.2em;
}
</style>
<div class="ex2a outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    Bob
  </div>
</div>

마크업은 다음과 같습니다. 다음은 오늘 작성할 내용입니다. Shadow DOM을 사용하지 않습니다.

<style>
.outer {
  border: 2px solid brown;
  border-radius: 1em;
  background: red;
  font-size: 20pt;
  width: 12em;
  height: 7em;
  text-align: center;
}
.boilerplate {
  color: white;
  font-family: sans-serif;
  padding: 0.5em;
}
.name {
  color: black;
  background: white;
  font-family: "Marker Felt", cursive;
  font-size: 45pt;
  padding-top: 0.2em;
}
</style>
<div class="outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    Bob
  </div>
</div>

DOM 트리에는 캡슐화가 없으므로 이름 태그의 전체 구조가 문서에 노출됩니다. 페이지의 다른 요소가 실수로 스타일 지정이나 스크립트에 동일한 클래스 이름을 사용하면 문제가 발생합니다.

불편을 끼쳐 드려 죄송합니다.

1단계: 프레젠테이션 세부정보 숨기기

의미론적으로는 다음 사항만 중요하게 생각합니다.

  • 이름표입니다.
  • 이름은 '밥'입니다.

먼저 원하는 실제 시맨틱에 더 가까운 마크업을 작성합니다.

<div id="nameTag">Bob</div>

그런 다음 프레젠테이션에 사용된 모든 스타일과 div를 <template> 요소에 배치합니다.

<div id="nameTag">Bob</div>
<template id="nameTagTemplate">
<span class="unchanged"><style>
.outer {
  border: 2px solid brown;

  … same as above …

</style>
<div class="outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    Bob
  </div>
</div></span>
</template>

이 시점에서 렌더링되는 것은 'Bob'뿐입니다. 프레젠테이션 DOM 요소를 <template> 요소 내부로 이동했으므로 렌더링되지는 않지만 JavaScript에서 액세스할 수 있습니다. 이제 그림자 루트를 채우기 위해 다음을 실행합니다.

<script>
var shadow = document.querySelector('#nameTag').createShadowRoot();
var template = document.querySelector('#nameTagTemplate');
var clone = document.importNode(template.content, true);
shadow.appendChild(clone);

이제 그림자 루트를 설정했으므로 이름 태그가 다시 렌더링됩니다. 이름 태그를 마우스 오른쪽 버튼으로 클릭하고 요소를 검사하면 멋진 시맨틱 마크업이 표시됩니다.

<div id="nameTag">Bob</div>

이는 Shadow DOM을 사용하여 문서에서 이름 태그의 프레젠테이션 세부정보를 숨겼음을 보여줍니다. 프레젠테이션 세부정보는 Shadow DOM에 캡슐화됩니다.

2단계: 콘텐츠를 프레젠테이션에서 분리하기

이제 이름 태그가 페이지에서 프레젠테이션 세부정보를 숨기지만 실제로는 프레젠테이션을 콘텐츠와 구분하지 않습니다. 콘텐츠 (이름 '밥')가 페이지에 있지만 렌더링되는 이름은 섀도우 루트로 복사된 이름이기 때문입니다. 이름표의 이름을 변경하려면 두 곳에서 변경해야 하며 동기화되지 않을 수 있습니다.

HTML 요소는 구성적입니다. 예를 들어 테이블 내에 버튼을 배치할 수 있습니다. 여기서는 구성이 필요합니다. 이름 태그는 빨간색 배경, 'Hi!' 텍스트, 이름 태그에 있는 콘텐츠의 구성이어야 합니다.

구성요소 작성자는 <content>라는 새 요소를 사용하여 위젯에서 컴포지션이 작동하는 방식을 정의합니다. 이렇게 하면 위젯 프레젠테이션에 삽입 지점이 생성되고 삽입 지점은 해당 지점에 표시할 섀도우 호스트의 콘텐츠를 선별합니다.

Shadow DOM의 마크업을 다음과 같이 변경하면

<span class="unchanged"><template id="nameTagTemplate">
<style>
  …
</style></span>
<div class="outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    <content></content>
  </div>
</div>
<span class="unchanged"></template></span>

이름 태그가 렌더링되면 그림자 호스트의 콘텐츠가 <content> 요소가 표시되는 지점에 투사됩니다.

이제 이름이 문서 한 곳에만 있으므로 문서의 구조가 더 간단해졌습니다. 페이지에서 사용자 이름을 업데이트해야 하는 경우 다음과 같이 작성하면 됩니다.

document.querySelector('#nameTag').textContent = 'Shellie';

그러면 끝입니다. 이름 태그의 콘텐츠를 <content>프로젝션하므로 이름 태그의 렌더링은 브라우저에서 자동으로 업데이트됩니다.

<div id="ex2b">

이제 콘텐츠와 프레젠테이션을 분리했습니다. 콘텐츠는 문서에 있고 프레젠테이션은 Shadow DOM에 있습니다. 렌더링할 때 브라우저에 의해 자동으로 동기화됩니다.

3단계: 수익 창출

콘텐츠와 프레젠테이션을 분리하면 콘텐츠를 조작하는 코드를 간소화할 수 있습니다. 이름 태그 예시에서 코드는 여러 개가 아닌 하나의 <div>가 포함된 간단한 구조만 처리하면 됩니다.

이제 프레젠테이션을 변경해도 코드를 변경할 필요가 없습니다.

예를 들어 이름 태그를 현지화하려고 한다고 가정해 보겠습니다. 여전히 이름 태그이므로 문서의 시맨틱 콘텐츠는 변경되지 않습니다.

<div id="nameTag">Bob</div>

그림자 루트 설정 코드는 동일하게 유지됩니다. 그림자 루트 변경사항에 추가되는 항목은 다음과 같습니다.

<template id="nameTagTemplate">
<style>
.outer {
  border: 2px solid pink;
  border-radius: 1em;
  background: url(sakura.jpg);
  font-size: 20pt;
  width: 12em;
  height: 7em;
  text-align: center;
  font-family: sans-serif;
  font-weight: bold;
}
.name {
  font-size: 45pt;
  font-weight: normal;
  margin-top: 0.8em;
  padding-top: 0.2em;
}
</style>
<div class="outer">
  <div class="name">
    <content></content>
  </div>
  と申します。
</div>
</template>

이는 오늘날 웹의 상황을 크게 개선한 것입니다. 이름 업데이트 코드가 간단하고 일관된 구성요소의 구조에 종속될 수 있기 때문입니다. 이름 업데이트 코드는 렌더링에 사용되는 구조를 알 필요가 없습니다. 렌더링되는 내용을 고려하면 이름은 'Hi! My name is”)이지만 먼저 일본어로('と申します' 전에) 표시됩니다. 이 구분은 표시되는 이름을 업데이트하는 관점에서 의미상 의미가 없으므로 이름 업데이트 코드는 이 세부정보를 알 필요가 없습니다.

추가 크레딧: 고급 프로젝션

위의 예에서 <content> 요소는 그림자 호스트의 모든 콘텐츠를 선별합니다. select 속성을 사용하여 콘텐츠 요소가 투사하는 항목을 제어할 수 있습니다. 콘텐츠 요소를 여러 개 사용할 수도 있습니다.

예를 들어 다음과 같은 문서가 있다고 가정해 보겠습니다.

<div id="nameTag">
  <div class="first">Bob</div>
  <div>B. Love</div>
  <div class="email">bob@</div>
</div>

CSS 선택자를 사용하여 특정 콘텐츠를 선택하는 그림자 루트:

<div style="background: purple; padding: 1em;">
  <div style="color: red;">
    <content **select=".first"**></content>
  </div>
  <div style="color: yellow;">
    <content **select="div"**></content>
  </div>
  <div style="color: blue;">
    <content **select=".email">**</content>
  </div>
</div>

<div class="email"> 요소는 <content select="div"><content select=".email"> 요소와 모두 일치합니다. 밥의 이메일 주소가 몇 번 표시되며 어떤 색상으로 표시되나요?

답은 밥의 이메일 주소가 한 번 노란색으로 표시된다는 것입니다.

그 이유는 Shadow DOM을 해킹하는 사람들이 알다시피 화면에 실제로 렌더링되는 항목의 트리를 구성하는 것이 거대한 파티와 같기 때문입니다. 콘텐츠 요소는 문서의 콘텐츠를 백스테이지 Shadow DOM 렌더링 파티로 가져올 수 있는 초대입니다. 이러한 초대장은 순서대로 전송됩니다. 초대장을 받는 대상은 초대 대상자 (select 속성)에 따라 다릅니다. 콘텐츠는 초대를 받으면 항상 초대를 수락하고 (누가 수락하지 않겠어요?) 실행됩니다. 그 주소로 후속 초대가 다시 전송되면 아무도 집에 없으므로 파티에 참석하지 못하게 됩니다.

위 예에서 <div class="email">div 선택자와 .email 선택자 모두와 일치하지만 div 선택자가 있는 콘텐츠 요소가 문서에서 더 먼저 나오므로 <div class="email">는 노란색 파티로 이동하고 아무도 파란색 파티로 이동할 수 없습니다. (그렇기 때문에 하늘이 그렇게 푸른 이유일 수도 있지만, 고통은 동료를 좋아하는 법이므로 알 수 없습니다.)

없음 파티에 초대된 항목은 전혀 렌더링되지 않습니다. 이것이 바로 첫 번째 예시의 'Hello, world' 텍스트에 발생한 일입니다. 이는 완전히 다른 렌더링을 실행하려는 경우에 유용합니다. 문서에 시맨틱 모델을 작성합니다. 이 모델은 페이지의 스크립트에서 액세스할 수 있지만 렌더링 목적으로는 숨기고 JavaScript를 사용하여 Shadow DOM의 완전히 다른 렌더링 모델에 연결합니다.

예를 들어 HTML에는 멋진 날짜 선택 도구가 있습니다. <input type="date">를 작성하면 깔끔한 팝업 캘린더가 표시됩니다. 하지만 사용자가 디저트로 섬 휴가를 즐기기 위해 며칠 동안 머물지 결정하도록 하려면 어떻게 해야 할까요? 빨간 포도나무로 만든 해먹이 있는 섬 휴가 말이죠. 다음과 같이 문서를 설정합니다.

<div class="dateRangePicker">
  <label for="start">Start:</label>
  <input type="date" name="startDate" id="start">
  <br>
  <label for="end">End:</label>
  <input type="date" name="endDate" id="end">
</div>

테이블을 사용하여 날짜 범위 등을 강조 표시하는 멋진 달력을 만드는 Shadow DOM을 만듭니다. 사용자가 캘린더의 날짜를 클릭하면 구성요소가 startDate 및 endDate 입력의 상태를 업데이트합니다. 사용자가 양식을 제출하면 이러한 입력 요소의 값이 제출됩니다.

라벨이 렌더링되지 않을 예정인데 문서에 라벨을 포함한 이유는 무엇인가요? 이는 사용자가 Shadow DOM을 지원하지 않는 브라우저로 양식을 보더라도 양식을 사용할 수 있기 때문입니다. 다만 뷰가 예쁘지 않을 뿐입니다. 사용자에게 다음과 같은 내용이 표시됩니다.

<div class="dateRangePicker">
  <label for="start">Start:</label>
  <input type="date" name="startDate" id="start">
  <br>
  <label for="end">End:</label>
  <input type="date" name="endDate" id="end">
</div>

Shadow DOM 101을 통과했습니다.

이것이 Shadow DOM의 기본사항입니다. Shadow DOM 101을 통과했습니다. Shadow DOM을 사용하면 더 많은 작업을 할 수 있습니다. 예를 들어 하나의 Shadow 호스트에서 여러 Shadow를 사용하거나 캡슐화를 위해 중첩된 Shadow를 사용하거나 모델 기반 뷰 (MDV) 및 Shadow DOM을 사용하여 페이지를 설계할 수 있습니다. 웹 구성요소는 Shadow DOM 그 이상입니다.

이 내용은 이후 게시물에서 설명합니다.