[Daily morning study] Shadow DOM๊ณผ Web Components

#daily morning study

Image


Web Components๋ž€

Web Components๋Š” ๋ธŒ๋ผ์šฐ์ € ํ‘œ์ค€ ๊ธฐ์ˆ ๋งŒ์œผ๋กœ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ปค์Šคํ…€ UI ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“œ๋Š” ๊ธฐ์ˆ ์ด๋‹ค. React๋‚˜ Vue ๊ฐ™์€ ํ”„๋ ˆ์ž„์›Œํฌ ์—†์ด๋„ ์บก์Аํ™”๋œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.

ํ•ต์‹ฌ ๊ธฐ์ˆ ์€ ์„ธ ๊ฐ€์ง€๋‹ค.

๊ธฐ์ˆ ์—ญํ• 
Custom Elements์ปค์Šคํ…€ HTML ํƒœ๊ทธ ์ •์˜
Shadow DOM๋‚ด๋ถ€ DOM์„ ์บก์Аํ™”ํ•ด ์™ธ๋ถ€ ์Šคํƒ€์ผ/์Šคํฌ๋ฆฝํŠธ ๊ฒฉ๋ฆฌ
HTML Templates<template>, <slot> ํƒœ๊ทธ๋กœ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋งˆํฌ์—… ์ •์˜

Custom Elements

customElements.define()์œผ๋กœ HTML ํƒœ๊ทธ๋ฅผ ์ง์ ‘ ์ •์˜ํ•œ๋‹ค.

class MyButton extends HTMLElement {
  connectedCallback() {
    this.innerHTML = `<button>ํด๋ฆญ</button>`;
  }
}

customElements.define('my-button', MyButton);

์ดํ›„ HTML์—์„œ <my-button></my-button> ์œผ๋กœ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๋‹ค.

๋ผ์ดํ”„์‚ฌ์ดํด ์ฝœ๋ฐฑ

class MyCard extends HTMLElement {
  connectedCallback()    { /* DOM์— ์—ฐ๊ฒฐ๋  ๋•Œ */ }
  disconnectedCallback() { /* DOM์—์„œ ์ œ๊ฑฐ๋  ๋•Œ */ }
  attributeChangedCallback(name, oldVal, newVal) { /* ์†์„ฑ ๋ณ€๊ฒฝ ์‹œ */ }

  static get observedAttributes() {
    return ['title', 'disabled']; // ๊ฐ์ง€ํ•  ์†์„ฑ ๋ชฉ๋ก
  }
}

Shadow DOM

Shadow DOM์€ ์š”์†Œ ๋‚ด๋ถ€์— ๋…๋ฆฝ๋œ DOM ํŠธ๋ฆฌ๋ฅผ ์ƒ์„ฑํ•ด ์™ธ๋ถ€ ์Šคํƒ€์ผ์ด ์นจ๋ฒ”ํ•˜์ง€ ๋ชปํ•˜๋„๋ก ๊ฒฉ๋ฆฌํ•œ๋‹ค.

class MyCard extends HTMLElement {
  connectedCallback() {
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.innerHTML = `
      <style>
        p { color: red; } /* ์ด ์Šคํƒ€์ผ์€ shadow ๋‚ด๋ถ€์—๋งŒ ์ ์šฉ */
      </style>
      <p>๊ฒฉ๋ฆฌ๋œ ์ปดํฌ๋„ŒํŠธ</p>
    `;
  }
}

mode: 'open'์ด๋ฉด element.shadowRoot๋กœ ์™ธ๋ถ€์—์„œ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•˜๊ณ , 'closed'๋ฉด ์ ‘๊ทผ์ด ์ฐจ๋‹จ๋œ๋‹ค.

์Šคํƒ€์ผ ๊ฒฉ๋ฆฌ ์›๋ฆฌ

  • Light DOM: ์ผ๋ฐ˜์ ์ธ document DOM. ์ „์—ญ CSS๊ฐ€ ๋ชจ๋‘ ์ ์šฉ๋œ๋‹ค.
  • Shadow DOM: ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€ ์ „์šฉ DOM. ์™ธ๋ถ€ CSS๊ฐ€ ์Šค๋ฉฐ๋“ค์ง€ ์•Š๋Š”๋‹ค.
  • :host ์„ ํƒ์ž๋กœ shadow root ์ž์‹ (=์ปดํฌ๋„ŒํŠธ ํƒœ๊ทธ)์— ์Šคํƒ€์ผ์„ ์ง€์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.
:host {
  display: block;
  border: 1px solid #ccc;
}

:host([disabled]) {
  opacity: 0.5;
}

HTML Templates์™€ Slot

<template> ํƒœ๊ทธ ์•ˆ์— ์ •์˜๋œ ๋‚ด์šฉ์€ ํŒŒ์‹ฑ์€ ๋˜์ง€๋งŒ ๋ Œ๋”๋ง๋˜์ง€ ์•Š๋Š”๋‹ค. JS๋กœ cloneํ•ด์„œ ์‚ฌ์šฉํ•œ๋‹ค.

<template id="card-template">
  <style>
    .card { padding: 16px; border-radius: 8px; }
  </style>
  <div class="card">
    <slot name="title"></slot>
    <slot></slot>
  </div>
</template>
class MyCard extends HTMLElement {
  connectedCallback() {
    const template = document.getElementById('card-template');
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.appendChild(template.content.cloneNode(true));
  }
}

์‚ฌ์šฉ ์ธก์—์„œ ์Šฌ๋กฏ์— ์ฝ˜ํ…์ธ ๋ฅผ ์ฃผ์ž…ํ•œ๋‹ค.

<my-card>
  <span slot="title">์นด๋“œ ์ œ๋ชฉ</span>
  <p>์นด๋“œ ๋ณธ๋ฌธ ๋‚ด์šฉ</p>
</my-card>

<slot>์€ ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€์— ๊ตฌ๋ฉ์„ ๋šซ์–ด๋‘๋Š” ๊ฐœ๋…์ด๋‹ค. ์ด๋ฆ„ ์žˆ๋Š” ์Šฌ๋กฏ(slot="title")์€ ์ง€์ • ์œ„์น˜์— ์‚ฝ์ž…๋˜๊ณ , ๊ธฐ๋ณธ ์Šฌ๋กฏ์€ ๋‚˜๋จธ์ง€ ๋ชจ๋“  ์ž์‹์„ ๋ฐ›๋Š”๋‹ค.


CSS ์ปค์Šคํ…€ ํ”„๋กœํผํ‹ฐ๋ฅผ ํ†ตํ•œ ํ…Œ๋งˆ ์ฃผ์ž…

Shadow DOM์€ ์™ธ๋ถ€ CSS๋ฅผ ๋ง‰์ง€๋งŒ, CSS ๋ณ€์ˆ˜(custom properties)๋Š” shadow boundary๋ฅผ ํ†ต๊ณผํ•œ๋‹ค. ์ด๋ฅผ ์ด์šฉํ•ด ํ…Œ๋งˆ๋ฅผ ์™ธ๋ถ€์—์„œ ์ œ์–ดํ•  ์ˆ˜ ์žˆ๋‹ค.

/* ์™ธ๋ถ€ CSS */
my-card {
  --card-bg: #f0f4ff;
  --card-radius: 12px;
}
/* shadow ๋‚ด๋ถ€ */
.card {
  background: var(--card-bg, white);
  border-radius: var(--card-radius, 4px);
}

React/Vue์™€์˜ ๋น„๊ต

ํ•ญ๋ชฉWeb ComponentsReact/Vue
ํ‘œ์ค€ ์—ฌ๋ถ€๋ธŒ๋ผ์šฐ์ € ํ‘œ์ค€๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
ํ”„๋ ˆ์ž„์›Œํฌ ์˜์กด์—†์Œ์žˆ์Œ
๋ฐ˜์‘ํ˜• ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ์ง์ ‘ ๊ตฌํ˜„ ํ•„์š”๋‚ด์žฅ
์ƒํƒœ๊ณ„/ํˆด๋ง๋นˆ์•ฝํ’๋ถ€
์Šคํƒ€์ผ ๊ฒฉ๋ฆฌShadow DOM์œผ๋กœ ์™„์ „ ๊ฒฉ๋ฆฌCSS Modules, scoped ๋“ฑ
SSR ์ง€์›์ œํ•œ์ ์ž˜ ์ง€์›๋จ

Web Components ์ž์ฒด๋Š” ๋ฐ˜์‘ํ˜• ์ƒํƒœ ๊ด€๋ฆฌ ๊ธฐ๋Šฅ์ด ์—†์–ด ๋ณต์žกํ•œ ์•ฑ์„ ๋งŒ๋“ค๊ธฐ ์–ด๋ ต๋‹ค. ๊ทธ๋ž˜์„œ ๋””์ž์ธ ์‹œ์Šคํ…œ์˜ leaf ์ปดํฌ๋„ŒํŠธ(๋ฒ„ํŠผ, ์ž…๋ ฅ ํ•„๋“œ ๋“ฑ)๋ฅผ ๋งŒ๋“ค๊ฑฐ๋‚˜, ๋‹ค์–‘ํ•œ ํ”„๋ ˆ์ž„์›Œํฌ์—์„œ ๊ณตํ†ต์œผ๋กœ ์“ฐ์ผ UI ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ๊ตฌ์„ฑํ•  ๋•Œ ์ฃผ๋กœ ํ™œ์šฉ๋œ๋‹ค.


์‹ค์ œ ํ™œ์šฉ ์‚ฌ๋ก€

  • Google: <google-map>, <paper-button> ๋“ฑ Polymer ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๊ธฐ๋ฐ˜
  • YouTube: ํ”Œ๋ ˆ์ด์–ด ์ปดํฌ๋„ŒํŠธ ์ผ๋ถ€๊ฐ€ Shadow DOM์œผ๋กœ ๊ตฌํ˜„๋จ
  • GitHub: ์ผ๋ถ€ UI ์š”์†Œ
  • ๋””์ž์ธ ์‹œ์Šคํ…œ: Adobe Spectrum, FAST (Microsoft) ๋“ฑ์€ Web Components ๊ธฐ๋ฐ˜

์ •๋ฆฌ

  • Web Components = Custom Elements + Shadow DOM + HTML Templates ์„ธ ๊ธฐ์ˆ ์˜ ์กฐํ•ฉ
  • Shadow DOM์€ CSS/DOM ์บก์Аํ™”๋ฅผ ๋ธŒ๋ผ์šฐ์ € ์ˆ˜์ค€์—์„œ ๋ณด์žฅํ•œ๋‹ค
  • <slot>์œผ๋กœ ์ปดํฌ๋„ŒํŠธ ์™ธ๋ถ€์—์„œ ๋‚ด์šฉ์„ ์‚ฝ์ž…ํ•  ์ˆ˜ ์žˆ๋‹ค
  • CSS ๋ณ€์ˆ˜๋Š” shadow boundary๋ฅผ ํ†ต๊ณผํ•ด ํ…Œ๋งˆ ์ฃผ์ž…์ด ๊ฐ€๋Šฅํ•˜๋‹ค
  • ๋ณต์žกํ•œ ์•ฑ๋ณด๋‹ค๋Š” ํ”„๋ ˆ์ž„์›Œํฌ ๋…๋ฆฝ์ ์ธ ๊ณตํ†ต UI ์ปดํฌ๋„ŒํŠธ ์ œ์ž‘์— ์ ํ•ฉํ•˜๋‹ค