[Daily morning study] Shadow DOM๊ณผ Web Components
#daily morning study
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 Components | React/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 ์ปดํฌ๋ํธ ์ ์์ ์ ํฉํ๋ค