--- id: frontend-web-components-deep title: Web Components — Custom Element / Shadow DOM / Slots category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [frontend, web-components, vibe-coding] tech_stack: { language: "TS / Lit", applicable_to: ["Frontend"] } applied_in: [] aliases: [Web Components, Custom Element, Shadow DOM, slot, declarative shadow, Lit] --- # Web Components > 표준 component (React 안 써도). **Custom Element + Shadow DOM + Template + Slot**. Lit 가 가장 ergonomic. Storybook / design system / framework-agnostic. ## 📖 핵심 개념 - Custom Element: `` 같은 새 tag. - Shadow DOM: scoped DOM + style. - Slot: composition. - Declarative Shadow DOM: SSR. ## 💻 코드 패턴 ### 가장 간단 Custom Element ```ts class HelloWorld extends HTMLElement { connectedCallback() { this.innerHTML = '

Hello, World!

'; } } customElements.define('hello-world', HelloWorld); // HTML // ``` ### Lifecycle callbacks ```ts 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 ```ts class CardEl extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }); this.shadowRoot!.innerHTML = `

Title

`; } } customElements.define('my-card', CardEl); ``` → Style scoped — `h2` 가 외부 영향 X. ### Slot (composition) ```html

This goes into the default slot

Footer
``` ```ts // Component this.shadowRoot!.innerHTML = `
`; ``` ### CSS shadow parts ```ts this.shadowRoot!.innerHTML = ` ${this.textContent} `; ``` ```css /* 외부 */ my-component::part(badge) { background: red; } ``` → Component 가 styling hook 노출. ### CSS custom property (theming) ```ts shadow.innerHTML = ` `; ``` ```css my-element { --my-color: red; } ``` ### Lit (가장 인기 framework) ```ts 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` ${this.count} `; } } ``` → React 비슷한 ergonomic + 표준 API. ### Reactive properties (Lit) ```ts @property({ type: String, reflect: true }) name = ''; // reflect: true → attribute 도 업데이트 @state() private _internal = 0; // re-render 만, 외부 X // Manually trigger this.requestUpdate(); ``` ### Events ```ts // Component 안 this.dispatchEvent(new CustomEvent('change', { detail: { value: this.value }, bubbles: true, composed: true, // shadow boundary 통과 })); // 사용 측 ``` ### Form-associated custom element ```ts class MyInput extends HTMLElement { static formAssociated = true; internals_: ElementInternals; constructor() { super(); this.internals_ = this.attachInternals(); } set value(v: string) { this.internals_.setFormValue(v); } } ``` → `
` 안에서 native input 처럼 동작. ### Declarative Shadow DOM (SSR) ```html

Content

``` → JS 없이 shadow DOM. SSR / SEO 친화. ### Lit SSR ```ts import { render } from '@lit-labs/ssr'; const html = await render(`...`); ``` ### React 안에서 Web Component ```tsx function App() { return ( ); } // JSX type augmentation declare global { namespace JSX { interface IntrinsicElements { 'my-card': any; } } } ``` → React 19+ 가 web component property 자연 지원. ### Vite Lit setup ```ts // vite.config.ts import { defineConfig } from 'vite'; export default defineConfig({ build: { lib: { entry: 'src/index.ts', formats: ['es'], }, }, }); ``` ### Component library 출판 ```json // 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 ```ts // my-card.stories.ts export default { title: 'MyCard' }; export const Default = () => ` Hello `; ``` → 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: false` event + 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 가 강점. ## 🔗 관련 문서 - [[Frontend_Design_Tokens]] - [[React_Headless_UI_Patterns]] - [[Web_PWA_Service_Worker]]