Shadow DOM 시작하기

Dominic Cooney
Dominic Cooney

소개

웹 구성요소는 다음과 같은 최첨단 표준입니다.

  1. 위젯을 빌드할 수 있게 함
  2. 안정적으로 재사용 가능
  3. 다음 버전의 구성요소가 내부 구현 세부정보를 변경해도 페이지가 중단되지 않습니다.

HTML/자바스크립트 사용 시기와 웹 구성요소 사용 시기를 결정해야 한다는 의미인가요? 아니요 HTML과 JavaScript는 대화형 시각적 콘텐츠를 만들 수 있습니다 위젯은 대화형 시각적 요소입니다. 위젯을 개발할 때 HTML 및 자바스크립트 기술을 활용하는 것이 좋습니다. 이를 위해 웹 구성요소 표준이 설계되었습니다.

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

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

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

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

안녕, 섀도우 월드

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>

이 시점에서 '밥'만 렌더링됩니다. 프레젠테이션 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단계: 프레젠테이션에서 콘텐츠 분리

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

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! 제 이름은')로, 일본어로는 '가장 먼저 되세요' 앞에 있습니다. 이러한 구분은 표시되는 이름을 업데이트하는 관점에서 의미상 의미가 없으므로 이름 업데이트 코드는 이러한 세부정보를 알 필요가 없습니다.

추가 크레딧: 고급 예측

위의 예에서 <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 시작하기

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

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