섀도 DOM, 템플릿, 슬롯

웹 기능: Shadow DOM <slot>

섀도 루트는 호스트 요소에 연결된 캡슐화된 DOM 하위 트리입니다. 내부 스타일은 외부로 유출되지 않으며 페이지 스타일은 내부로 유출되지 않습니다 (상속된 속성 제외). 템플릿은 트리를 효율적으로 구성하고, 슬롯은 Light DOM 콘텐츠를 트리에 투영합니다.

섀도우 루트 연결

this.attachShadow({ mode: 'open', delegatesFocus: true });

delegatesFocus: true는 호스트에 포커스를 맞추면 포커스를 섀도 트리의 첫 번째 포커스 가능 요소로 이동하고, :focus 스타일 지정을 호스트로 라우팅하고, 양식 연결 맞춤 요소 (static formAssociated = true)에서 <label> 클릭을 내부 컨트롤로 전달합니다. 구성요소가 포커스 가능한 컨트롤을 래핑할 때마다 설정합니다. 거의 모든 경우에 mode: 'open'를 사용하여 개발 도구, 페이지 스크립트, 서버 하이드레이션이 element.shadowRoot에 도달할 수 있도록 합니다(웹 구성요소 방향 참고).

템플릿: 효율적으로 트리 구성

<template>를 한 번 (모듈 범위 또는 정적 필드) 정의하고 인스턴스별로 클론합니다. 파싱된 프래그먼트를 클론하는 것이 구성할 때마다 innerHTML 문자열을 다시 파싱하는 것보다 훨씬 빠릅니다.

const template = document.createElement('template');
template.innerHTML = `
  <div class="card">
    <slot name="header"></slot>
    <div class="body"><slot></slot></div>
    <slot name="footer"></slot>
  </div>
`;

class MyCard extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    // MANDATORY: deep-clone the template content so each instance gets its own nodes.
    this.shadowRoot.append(template.content.cloneNode(true));
  }
}
customElements.define('my-card', MyCard);

템플릿을 간결하게 유지합니다. 깊게 중첩된 'div soup'와 여러 수준의 중첩된 shadow root는 모두 측정 가능한 스타일 해상도와 레이아웃 비용을 추가하므로 대규모 성능에 큰 영향을 미칩니다. 평면 구조와 시맨틱 요소를 사용하는 것이 좋습니다.

슬롯: 사람들을 놀라게 하는 부분

기본 슬롯과 이름이 지정된 슬롯은 문서에 설명된 대로 작동합니다. 다음은 문제가 되는 동작입니다.

  • 공백도 포함됩니다. 요소 사이의 텍스트 노드 (순수 공백 포함)는 기본 슬롯에 할당되므로 기본 슬롯이 실제로 '비어 있는' 경우는 거의 없습니다.
  • 슬롯 순서가 아닌 DOM 순서가 유지됩니다. 프로젝션된 노드는 채우는 슬롯과 관계없이 :first-child/:last-child 목적으로 원래 Light DOM 순서를 유지합니다. ::slotted(*:first-child) 규칙은 해당 슬롯의 첫 번째 하위 요소가 아닌 첫 번째 DOM 하위 요소를 타겟팅하므로 예상과 일치하지 않는 경우가 많습니다.
  • slotchange은 업그레이드 시점 신호입니다. 할당된 노드가 변경될 때 (초기 할당 포함) 실행되며, connectedCallback 중에 존재하지 않았을 수 있는 Light DOM 하위 요소가 업그레이드 후에 도착했는지 확인하는 신뢰할 수 있는 방법입니다.
this.shadowRoot.querySelector('slot[name="header"]')
  .addEventListener('slotchange', (e) => {
    const nodes = e.target.assignedElements();
    this.toggleAttribute('has-header', nodes.length > 0);
  });

섀도우 루트에 스타일시트 로드

메서드 장점 단점
템플릿의 <style> 간단하고 완전히 캡슐화되어 있으며 JS 없이 작동합니다. 인스턴스별로 중복되어 대규모로 DOM을 비대하게 만듭니다.
<link rel="stylesheet"> 캐시 가능한 외부 파일입니다. 추가 요청, 스타일이 지정되지 않은 콘텐츠가 깜박일 위험이 있습니다.
adoptedStyleSheets 모든 인스턴스에서 공유되는 CSSStyleSheet 1개, 대규모에서 가장 빠름 JS가 필요합니다. 구성된 시트는 @import를 지원하지 않습니다.

하나의 구성요소에 여러 인스턴스가 있는 경우 공유 구성 스타일시트를 사용하는 것이 좋습니다.

const sheet = new CSSStyleSheet();
sheet.replaceSync(`:host { display: block; }`);

class MyCard extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    // The same sheet object is reused by every instance; no per-instance parse.
    this.shadowRoot.adoptedStyleSheets = [sheet];
  }
}