6.9 KiB
6.9 KiB
id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
| id | title | category | status | source_trust_level | verification_status | created_at | updated_at | tags | tech_stack | applied_in | aliases | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| frontend-web-components-deep | Web Components — Custom Element / Shadow DOM / Slots | Coding | draft | B | conceptual | 2026-05-09 | 2026-05-09 |
|
|
|
Web Components
표준 component (React 안 써도). Custom Element + Shadow DOM + Template + Slot. Lit 가 가장 ergonomic. Storybook / design system / framework-agnostic.
📖 핵심 개념
- Custom Element:
<my-button>같은 새 tag. - Shadow DOM: scoped DOM + style.
- Slot: composition.
- Declarative Shadow DOM: SSR.
💻 코드 패턴
가장 간단 Custom Element
class HelloWorld extends HTMLElement {
connectedCallback() {
this.innerHTML = '<p>Hello, World!</p>';
}
}
customElements.define('hello-world', HelloWorld);
// HTML
// <hello-world></hello-world>
Lifecycle callbacks
class MyEl extends HTMLElement {
connectedCallback() {
// DOM 에 붙음
}
disconnectedCallback() {
// DOM 에서 떼어짐 — cleanup
}
adoptedCallback() {
// 다른 document 로 이사 (iframe)
}
static observedAttributes = ['name'];
attributeChangedCallback(name: string, oldVal: string, newVal: string) {
// attribute 변경
}
}
Shadow DOM
class CardEl extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot!.innerHTML = `
<style>
:host { display: block; padding: 16px; border: 1px solid #ccc; }
h2 { color: blue; }
</style>
<h2>Title</h2>
<slot></slot>
`;
}
}
customElements.define('my-card', CardEl);
→ Style scoped — h2 가 외부 영향 X.
Slot (composition)
<my-card>
<p>This goes into the default slot</p>
<span slot="footer">Footer</span>
</my-card>
// Component
this.shadowRoot!.innerHTML = `
<slot></slot>
<hr>
<slot name="footer"></slot>
`;
CSS shadow parts
this.shadowRoot!.innerHTML = `
<style>
.badge { padding: 4px; background: blue; color: white; }
</style>
<span part="badge">${this.textContent}</span>
`;
/* 외부 */
my-component::part(badge) {
background: red;
}
→ Component 가 styling hook 노출.
CSS custom property (theming)
shadow.innerHTML = `
<style>
:host { color: var(--my-color, black); }
</style>
`;
my-element { --my-color: red; }
Lit (가장 인기 framework)
import { LitElement, html, css } from 'lit';
import { customElement, property } from 'lit/decorators.js';
@customElement('my-counter')
class MyCounter extends LitElement {
@property({ type: Number }) count = 0;
static styles = css`
button { font-size: 1rem; padding: 8px 16px; }
`;
render() {
return html`
<button @click=${() => this.count++}>+</button>
<span>${this.count}</span>
`;
}
}
→ React 비슷한 ergonomic + 표준 API.
Reactive properties (Lit)
@property({ type: String, reflect: true }) name = '';
// reflect: true → attribute 도 업데이트
@state() private _internal = 0;
// re-render 만, 외부 X
// Manually trigger
this.requestUpdate();
Events
// Component 안
this.dispatchEvent(new CustomEvent('change', {
detail: { value: this.value },
bubbles: true,
composed: true, // shadow boundary 통과
}));
// 사용 측
<my-input @change=${this.handleChange}></my-input>
Form-associated custom element
class MyInput extends HTMLElement {
static formAssociated = true;
internals_: ElementInternals;
constructor() {
super();
this.internals_ = this.attachInternals();
}
set value(v: string) {
this.internals_.setFormValue(v);
}
}
→ <form> 안에서 native input 처럼 동작.
Declarative Shadow DOM (SSR)
<my-card>
<template shadowrootmode="open">
<style>:host { padding: 8px }</style>
<h2>Card</h2>
<slot></slot>
</template>
<p>Content</p>
</my-card>
→ JS 없이 shadow DOM. SSR / SEO 친화.
Lit SSR
import { render } from '@lit-labs/ssr';
const html = await render(`<my-card>...</my-card>`);
React 안에서 Web Component
function App() {
return (
<my-card name="Alice"></my-card>
);
}
// JSX type augmentation
declare global {
namespace JSX {
interface IntrinsicElements {
'my-card': any;
}
}
}
→ React 19+ 가 web component property 자연 지원.
Vite Lit setup
// vite.config.ts
import { defineConfig } from 'vite';
export default defineConfig({
build: {
lib: {
entry: 'src/index.ts',
formats: ['es'],
},
},
});
Component library 출판
// package.json
{
"name": "@me/my-components",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"customElements": "custom-elements.json"
}
→ custom-elements.json (manifest) → Storybook / docs 자동.
Storybook
// my-card.stories.ts
export default { title: 'MyCard' };
export const Default = () => `
<my-card>Hello</my-card>
`;
→ Web Components storybook 가 framework-agnostic 의 큰 장점.
Use case
- Design system (Salesforce Lightning, Adobe Spectrum)
- Embeddable widgets (chat, analytics, payment)
- Cross-framework (React + Vue + Svelte 다 사용)
- Browser extension UI
- Edge / SSR friendly
Browser support
Custom Elements v1: Chrome, Firefox, Safari (모두 OK)
Shadow DOM: 모두 OK
Declarative Shadow: Chrome 90+, Safari 16.4+, FF 123+
Form-associated: 대부분 OK
Adoption examples
- GitHub: 작은 widget 일부 web components
- YouTube: 일부 player UI
- Apple Music Web: web components heavy
- Salesforce Lightning Web Components
🤔 의사결정 기준
| 상황 | 추천 |
|---|---|
| Cross-framework component | Web Component (Lit) |
| Design system | Lit / Stencil |
| 단일 React 앱 | React component |
| Embeddable widget | Web Component |
| SSR 중요 | Declarative Shadow DOM |
| 작은 단순 element | Vanilla custom element |
❌ 안티패턴
- Light DOM 만 사용 + scoped 가정: style leak.
composed: falseevent + parent 안 잡힘: shadow 막힘.- 모든 거 Web Component: 큰 앱 = framework 가 좋음.
- Lit 안 쓰고 vanilla 큰 앱: 보일러플레이트 폭발.
- CSS-in-shadow + custom prop 없음: theming 불가.
- Form integration 없음: form 깨짐.
- Slot 미지원 framework + Web Component: composition 깨짐.
🤖 LLM 활용 힌트
- Lit 가 Web Component 표준 framework.
- Shadow DOM = scoped style, slot = composition.
- Declarative shadow = SSR.
- Cross-framework / embeddable 가 강점.