template · slot — 웹 컴포넌트 마크업 청사진

HTML template 요소로 재사용 가능한 마크업을 정의하고, slot으로 Light DOM 콘텐츠를 Shadow DOM 내에 투영하는 웹 컴포넌트 합성 패턴을 정리합니다.

· 4 min read · PALDYN Team

지난 글에서 Custom Elements와 Shadow DOM으로 캡슐화된 컴포넌트를 만드는 방법을 살펴봤습니다. 이번에는 웹 컴포넌트 합성의 핵심인 <template><slot>을 다룹니다.


<template> 요소

<template>은 HTML 파서가 내용을 파싱하지만 화면에 렌더링하지 않는 요소입니다. 스크립트를 실행하지 않고, 이미지를 로드하지 않으며, CSS를 적용하지 않습니다. .content 프로퍼티가 DocumentFragment로, cloneNode(true)로 복사해 사용합니다.

<template id="row-tpl">
  <tr>
    <td class="name"></td>
    <td class="score"></td>
  </tr>
</template>
const tpl = document.querySelector('#row-tpl');

data.forEach(item => {
  const row = tpl.content.cloneNode(true);
  row.querySelector('.name').textContent  = item.name;
  row.querySelector('.score').textContent = item.score;
  tbody.appendChild(row);
});

<template>은 Shadow DOM 없이도 사용할 수 있습니다. 단순한 테이블 행 반복 렌더링에도 유용합니다.


<slot> — 콘텐츠 투영

<slot>은 Shadow DOM 내에서 Light DOM 콘텐츠가 나타날 위치를 지정하는 구멍입니다. 콘텐츠가 Shadow 트리로 실제 이동하지 않고, 렌더링 시 해당 위치에 투영됩니다.

slot 투영 구조

기본 슬롯

이름 없는 <slot>slot 속성이 없는 모든 자식 노드를 받습니다.

<!-- Shadow DOM 내부 -->
<div class="card">
  <slot></slot>  <!-- Light DOM 자식이 여기에 투영 -->
</div>

<!-- 사용처 -->
<my-card>
  <p>이 단락이 기본 슬롯에 투영됩니다.</p>
</my-card>

이름 있는 슬롯

name 속성으로 여러 슬롯을 구분합니다.

<!-- Shadow DOM 내부 -->
<header><slot name="title">기본 제목</slot></header>
<main><slot></slot></main>
<footer><slot name="actions"></slot></footer>

<!-- 사용처 -->
<my-layout>
  <h1 slot="title">페이지 제목</h1>
  <article>본문 콘텐츠</article>
  <button slot="actions">저장</button>
</my-layout>

slot="name" 속성이 있는 요소는 해당 이름의 슬롯에, 없는 요소는 기본 슬롯에 배분됩니다.

슬롯 폴백 콘텐츠

슬롯에 배분되는 노드가 없으면 슬롯 내부의 콘텐츠(폴백)가 표시됩니다.

<slot name="icon">★</slot>  <!-- slot="icon"이 없으면 ★ 표시 -->

::slotted() 가상 클래스

Shadow DOM 내 CSS에서 투영된 Light DOM 요소를 선택합니다.

/* Shadow 내부 style */
::slotted(p) {
  color: #333;
  margin-bottom: 1rem;
}

::slotted(*) {
  box-sizing: border-box;
}

직계 자식만 선택됩니다. ::slotted(p span) 같은 자손 선택자는 동작하지 않습니다.


template + slot 통합 패턴

template + slot 코드 패턴

class DialogEl extends HTMLElement {
  connectedCallback() {
    const tmpl = document.querySelector('#dialog-tpl');
    const root = this.attachShadow({ mode: 'open' });
    root.appendChild(tmpl.content.cloneNode(true));

    // slotchange 이벤트로 배분 변화 감지
    root.querySelector('slot').addEventListener('slotchange', e => {
      const nodes = e.target.assignedNodes();
      console.log('배분된 노드:', nodes);
    });
  }
}
customElements.define('dialog-el', DialogEl);

슬롯 API

const slot = shadowRoot.querySelector('slot[name="content"]');

// 배분된 노드 목록 (텍스트 포함)
slot.assignedNodes();

// 배분된 요소만
slot.assignedElements();

// flatten: true → 폴백 포함
slot.assignedNodes({ flatten: true });

Declarative Shadow DOM

서버 사이드 렌더링(SSR)을 위한 선언형 Shadow DOM은 HTML에서 직접 Shadow DOM을 정의합니다.

<my-card>
  <template shadowrootmode="open">
    <style>:host { display: block; }</style>
    <slot></slot>
  </template>
  <p>콘텐츠</p>
</my-card>

JS 없이 서버에서 Shadow DOM을 포함한 HTML을 전송할 수 있어 초기 렌더링에 유리합니다. Chrome 90+, Firefox 123+, Safari 16.4+에서 지원됩니다.


<template>은 “마크업 청사진”, <slot>은 “콘텐츠 구멍”입니다. 두 기술을 함께 사용하면 Shadow DOM 컴포넌트가 외부 콘텐츠를 유연하게 받아들이면서도 내부 구조를 보호할 수 있습니다.


지난 글: Shadow DOM · Custom Elements — 웹 컴포넌트 기초

다음 글: 이벤트 위임 — 효율적인 이벤트 리스너 관리


읽어주셔서 감사합니다. 😊