Shadow DOM 301

고급 개념 및 DOM API

이 글에서는 Shadow DOM으로 할 수 있는 놀라운 일들을 더 자세히 설명합니다! Shadow DOM 101Shadow DOM 201에서 설명한 개념을 기반으로 합니다.

여러 섀도 루트 사용

파티를 열 경우 모든 사람이 같은 방에 몰려있으면 답답함이 느껴집니다. 사용자 그룹을 여러 방에 분산하는 옵션을 원합니다. Shadow DOM을 호스팅하는 요소도 이 작업을 할 수 있습니다. 즉, 한 번에 둘 이상의 섀도우 루트를 호스팅할 수 있습니다.

호스트에 여러 섀도 루트를 연결하려고 하면 어떻게 되는지 살펴보겠습니다.

<div id="example1">Light DOM</div>
<script>
  var container = document.querySelector('#example1');
  var root1 = container.createShadowRoot();
  var root2 = container.createShadowRoot();
  root1.innerHTML = '<div>Root 1 FTW</div>';
  root2.innerHTML = '<div>Root 2 FTW</div>';
</script>

이미 섀도우 트리를 연결했음에도 불구하고 'Root 2 FTW'가 렌더링됩니다. 이는 호스트에 마지막으로 추가된 섀도 트리가 승리하기 때문입니다. 렌더링에 관한 한 LIFO 스택입니다 DevTools를 검사하면 이 동작을 확인할 수 있습니다.

그러면 마지막 그림자만 렌더링에 초대되는 경우 그림자를 여러 개 사용하는 것이 무슨 소용이 있을까요? 그림자 삽입 지점을 입력하세요.

그림자 삽입 포인트

'그림자 삽입점' (<shadow>)은 자리표시자라는 점에서 일반적인 삽입점 (<content>)과 유사합니다. 하지만 호스트의 콘텐츠의 자리표시자가 아니라 다른 섀도 트리의 호스트입니다. 바로 Shadow DOM Inception입니다!

상상할 수 있듯이, 토끼굴을 더 깊이 파고들수록 상황이 더 복잡해집니다. 따라서 여러 <shadow> 요소가 작동하면 어떻게 되는지 사양에서 매우 명확하게 설명합니다.

원래 예로 돌아보면 첫 번째 섀도 root1는 초대 목록에서 사라졌습니다. <shadow> 삽입점을 추가하면 다시 표시됩니다.

<div id="example2">Light DOM</div>
<script>
var container = document.querySelector('#example2');
var root1 = container.createShadowRoot();
var root2 = container.createShadowRoot();
root1.innerHTML = '<div>Root 1 FTW</div><content></content>';
**root2.innerHTML = '<div>Root 2 FTW</div><shadow></shadow>';**
</script>

이 예에는 몇 가지 흥미로운 점이 있습니다.

  1. 'Root 2 FTW'는 여전히 'Root 1 FTW' 위에서 렌더링됩니다. 이는 <shadow> 삽입점을 배치한 위치 때문입니다. 반전시키려면 삽입점(root2.innerHTML = '<shadow></shadow><div>Root 2 FTW</div>';)을 이동합니다.
  2. 이제 root1에 <content> 삽입 지점이 있습니다. 그러면 렌더링을 위해 텍스트 노드 'Light DOM'이 사용됩니다.

<shadow>에서 렌더링되는 결과

<shadow>에서 렌더링되고 있는 이전 섀도우 트리를 아는 것이 유용할 때가 있습니다. .olderShadowRoot를 통해 이 트리에 관한 참조를 가져올 수 있습니다.

**root2.olderShadowRoot** === root1 //true

호스트의 섀도 루트 가져오기

요소가 Shadow DOM을 호스팅하는 경우 .shadowRoot를 사용하여 가장 작은 섀도우 루트에 액세스할 수 있습니다.

var root = host.createShadowRoot();
console.log(host.shadowRoot === root); // true
console.log(document.body.shadowRoot); // null

그림자에 사람이 들어갈까 걱정된다면 .shadowRoot를 null로 재정의합니다.

Object.defineProperty(host, 'shadowRoot', {
  get: function() { return null; },
  set: function(value) { }
});

약간의 해킹이 있었지만 잘 작동됩니다. 결론적으로, 놀랍도록 훌륭하지만 Shadow DOM은 보안 기능으로 설계되지 않았습니다. 콘텐츠를 완전히 격리하기 위해 이 도구에 의존하지 마세요.

JS에서 Shadow DOM 빌드

DOM을 JS로 빌드하려는 경우 HTMLContentElementHTMLShadowElement에 이를 위한 인터페이스가 있습니다.

<div id="example3">
  <span>Light DOM</span>
</div>
<script>
var container = document.querySelector('#example3');
var root1 = container.createShadowRoot();
var root2 = container.createShadowRoot();

var div = document.createElement('div');
div.textContent = 'Root 1 FTW';
root1.appendChild(div);

 // HTMLContentElement
var content = document.createElement('content');
content.select = 'span'; // selects any spans the host node contains
root1.appendChild(content);

var div = document.createElement('div');
div.textContent = 'Root 2 FTW';
root2.appendChild(div);

// HTMLShadowElement
var shadow = document.createElement('shadow');
root2.appendChild(shadow);
</script>

이 예는 이전 섹션의 예와 거의 동일합니다. 유일한 차이점은 이제 select를 사용하여 새로 추가된 <span>를 가져온다는 것입니다.

삽입점 사용하기

호스트 요소에서 선택되고 섀도우 트리에 '분산'되는 노드를 분산 노드라고 합니다. 삽입 지점으로 초대할 때 섀도우 경계를 넘을 수 있습니다.

삽입점의 개념상 이상한 점은 DOM을 물리적으로 이동시키지 않는다는 점입니다. 호스트 노드는 그대로 유지됩니다. 삽입 지점은 단순히 호스트에서 섀도우 트리로 노드를 다시 투영할 뿐입니다. 프레젠테이션/렌더링입니다. '이 노드를 여기로 이동' '노드를 이 위치에 렌더링하세요.'

예를 들면 다음과 같습니다.

<div><h2>Light DOM</h2></div>
<script>
var root = document.querySelector('div').createShadowRoot();
root.innerHTML = '<content select="h2"></content>';

var h2 = document.querySelector('h2');
console.log(root.querySelector('content[select="h2"] h2')); // null;
console.log(root.querySelector('content').contains(h2)); // false
</script>

그러면 h2는 Shadow DOM의 하위 요소가 아닙니다. 이것으로 또 하나의 정보를 얻을 수 있습니다.

Element.getDistributedNodes()

<content>로 순회할 수 없지만 .getDistributedNodes() API를 사용하면 삽입 지점에서 분산 노드를 쿼리할 수 있습니다.

<div id="example4">
  <h2>Eric</h2>
  <h2>Bidelman</h2>
  <div>Digital Jedi</div>
  <h4>footer text</h4>
</div>

<template id="sdom">
  <header>
    <content select="h2"></content>
  </header>
  <section>
    <content select="div"></content>
  </section>
  <footer>
    <content select="h4:first-of-type"></content>
  </footer>
</template>

<script>
var container = document.querySelector('#example4');

var root = container.createShadowRoot();

var t = document.querySelector('#sdom');
var clone = document.importNode(t.content, true);
root.appendChild(clone);

var html = [];
[].forEach.call(root.querySelectorAll('content'), function(el) {
  html.push(el.outerHTML + ': ');
  var nodes = el.getDistributedNodes();
  [].forEach.call(nodes, function(node) {
    html.push(node.outerHTML);
  });
  html.push('\n');
});
</script>

Element.getDestinationInsertionPoints()

.getDistributedNodes()와 마찬가지로 .getDestinationInsertionPoints()를 호출하여 노드가 배포된 삽입 지점을 확인할 수 있습니다.

<div id="host">
  <h2>Light DOM
</div>

<script>
  var container = document.querySelector('div');

  var root1 = container.createShadowRoot();
  var root2 = container.createShadowRoot();
  root1.innerHTML = '<content select="h2"></content>';
  root2.innerHTML = '<shadow></shadow>';

  var h2 = document.querySelector('#host h2');
  var insertionPoints = h2.getDestinationInsertionPoints();
  [].forEach.call(insertionPoints, function(contentEl) {
    console.log(contentEl);
  });
</script>

도구: Shadow DOM Visualizer

Shadow DOM인 블랙 마법을 이해하는 것은 어렵습니다. 처음으로 머리를 감았던 기억이 납니다.

Shadow DOM 렌더링의 작동 방식을 시각화하기 위해 d3.js를 사용하여 도구를 빌드했습니다. 왼쪽의 마크업 상자는 모두 수정할 수 있습니다. 자유롭게 자체 마크업을 붙여넣고 작동 방식을 살펴보고 삽입 지점이 호스트 노드를 섀도우 트리에 재구성할 수 있습니다.

Shadow DOM 비주얼라이저
Shadow DOM Visualizer 실행

사용해 보시고 의견을 알려주세요.

이벤트 모델

섀도우 경계를 넘는 이벤트도 있고 그렇지 않은 이벤트도 있습니다. 이벤트가 경계를 넘는 경우 섀도우 루트의 상한값이 제공하는 캡슐화를 유지하기 위해 이벤트 타겟이 조정됩니다. 즉, 내부 요소가 아닌 호스트 요소에서 Shadow DOM으로 온 것처럼 이벤트가 다시 타겟팅됩니다.

액션 1 플레이

  • 재미있군요. 호스트 요소 (<div data-host>)에서 파란색 노드까지의 mouseout가 표시됩니다. 분산 노드이지만 여전히 ShadowDOM이 아닌 호스트에 있습니다. 더 아래쪽에서 노란색으로 마우스를 다시 가져가면 파란색 노드에 mouseout가 발생합니다.

Play 액션 2

  • 호스트 (맨 끝에)에 하나의 mouseout가 표시됩니다. 일반적으로 모든 노란색 블록에서 mouseout 이벤트가 트리거됩니다. 그러나 이 경우 이러한 요소는 Shadow DOM 내부에 있으며 이벤트가 상한 경계를 통과하지 않습니다.

Play 액션 3

  • 입력을 클릭하면 focusin가 입력에 표시되지 않고 호스트 노드 자체에 표시됩니다. 다시 표적이 되었어요!

항상 중지된 이벤트

다음 이벤트는 섀도우 경계를 넘지 않습니다.

  • 취소
  • error
  • select
  • 변경
  • load
  • 재설정
  • resize
  • scroll
  • CANNOT TRANSLATE

결론

Shadow DOM은 놀라울 정도로 강력합니다. 처음으로 <iframe> 또는 다른 기존 기법의 추가 수하물 없이 적절한 캡슐화를 얻었습니다.

Shadow DOM은 확실히 복잡한 짐이지만 웹 플랫폼에 추가할 가치가 있습니다. 시간을 내서 사용해 보세요. 알아보기 질문하기.

자세한 내용은 Dominic의 소개 자료 Shadow DOM 101Shadow DOM 201: CSS & Styling 문서를 참고하세요.