--- id: frontend-web-components title: Web Components β€” Custom Element / Shadow DOM 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 / HTML", applicable_to: ["Frontend"] } applied_in: [] aliases: [Web Components, Custom Element, Shadow DOM, slot, Lit, declarative shadow DOM] --- # Web Components > Browser native component. **Custom Element + Shadow DOM + Template + Slot**. Framework agnostic. Lit / Stencil κ°€ friendly. ## πŸ“– 핡심 κ°œλ… - Custom Element: `` μ •μ˜. - Shadow DOM: scoped CSS / DOM. - Template: μž¬μ‚¬μš© HTML. - Slot: child μ‚½μž… point. ## πŸ’» μ½”λ“œ νŒ¨ν„΄ ### Vanilla custom element ```ts class MyCard extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }); } connectedCallback() { this.shadowRoot!.innerHTML = `

${this.getAttribute('title') ?? ''}

`; } static observedAttributes = ['title']; attributeChangedCallback(name: string, old: string | null, value: string | null) { if (name === 'title' && this.shadowRoot) { const h2 = this.shadowRoot.querySelector('h2'); if (h2) h2.textContent = value ?? ''; } } } customElements.define('my-card', MyCard); ``` ```html

Card content

``` ### Lifecycle ``` connectedCallback: DOM 에 μΆ”κ°€ disconnectedCallback: 제거 attributeChangedCallback: attribute λ³€κ²½ adoptedCallback: λ‹€λ₯Έ document 둜 이동 ``` ### Shadow DOM (scoped CSS) ```ts this.attachShadow({ mode: 'open' }); // μ™ΈλΆ€ access OK this.attachShadow({ mode: 'closed' }); // μ™ΈλΆ€ access X (λ“œλ¬Όκ²Œ) this.shadowRoot.innerHTML = `

Scoped paragraph

`; ``` ### CSS λ³€μˆ˜ (theme) ```html ``` ```ts class MyButton extends HTMLElement { connectedCallback() { this.shadowRoot!.innerHTML = ` `; } } ``` β†’ CSS variable κ°€ Shadow boundary 톡과. ### Slot ```ts this.shadowRoot.innerHTML = `
`; ``` ```html

Title

Body content

``` ### Lit (modern WC framework) ```bash yarn add lit ``` ```ts import { LitElement, html, css } from 'lit'; import { customElement, property } from 'lit/decorators.js'; @customElement('my-card') export class MyCard extends LitElement { static styles = css` :host { display: block; padding: 16px; } h2 { margin: 0; } `; @property({ type: String }) cardTitle = ''; @property({ type: Number }) count = 0; render() { return html`

${this.cardTitle}

Count: ${this.count}

`; } } ``` ```html

Slotted content

``` β†’ React 같은 declarative + reactive. ### Lit + signals (modern) ```ts import { LitElement, html } from 'lit'; import { signal, SignalWatcher } from '@lit-labs/signals'; const count = signal(0); @customElement('my-counter') class MyCounter extends SignalWatcher(LitElement) { render() { return html``; } } ``` ### Stencil (큰 design system) ```bash npm init stencil ``` ```ts import { Component, Prop, State, h } from '@stencil/core'; @Component({ tag: 'my-card', styleUrl: 'my-card.css', shadow: true }) export class MyCard { @Prop() title: string; @State() count = 0; render() { return (

{this.title}

); } } ``` β†’ Compile-time. μž‘μ€ bundle. ### Declarative Shadow DOM (SSR) ```html

Slotted

``` β†’ Server κ°€ shadow DOM 직접 render. JS 없어도 styled. ### Form-associated (FACE) ```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 form value. ### React μ•ˆ μ‚¬μš© ```tsx import 'my-card.js'; function App() { return ( console.log('clicked')}>

Content

); } ``` β†’ React κ°€ unknown element κ·ΈλŒ€λ‘œ render. 단 event κ°€ camelCase 좩돌 β€” wrapper. ```tsx // React wrapper import { createComponent } from '@lit/react'; import { MyCard } from './my-card'; const MyCardReact = createComponent({ tagName: 'my-card', elementClass: MyCard, react: React, events: { onChange: 'change' }, }); ``` ### Vue / Svelte / Solid μ•ˆ μ‚¬μš© ```vue ``` β†’ 거의 λ‹€ native ν˜Έν™˜. ### Bundle / size ``` Lit: ~5 KB Stencil: 컴파일 μ‹œ μž‘μŒ Vanilla: 0 dependency β†’ μž‘μ€ widget = vanilla / Lit. ``` ### Distribution ```ts // npm package "main": "dist/my-card.js", "types": "dist/my-card.d.ts", "customElements": "dist/custom-elements.json" // CDN ``` β†’ μ–΄λ””μ„œλ‚˜ μ‚¬μš©. ### Use cases ``` βœ… Design system (cross-team / cross-framework) βœ… Embeddable widget (3rd party) βœ… Shadow DOM 의 isolation ν•„μš” βœ… Long-lived component (framework migration μ•ˆμ „) ❌ Heavy app component (React/Solid κ°€ 빠름) ❌ Frequent re-render ❌ Complex state (better with framework) ``` ### 함정 ``` 1. Style 격리 β€” μ™ΈλΆ€ CSS μ•ˆ 영ν–₯ X (μ˜λ„). 2. SSR 지원 약함 (Declarative Shadow DOM κ°€ ν•΄κ²° 쀑). 3. Form integration 어렀움 (FACE κ°€ ν•΄κ²°). 4. A11y β€” 직접 ARIA μΆ”κ°€. 5. Bundle 더 큼 (ν•œ component 의 ν‘œμ€€ < 큰 framework). ``` ### Polyfill ``` Modern browser = native 지원. μ˜› IE11 = 큰 polyfill. β†’ λ¬΄μ‹œ. ``` ### vs React component ``` Web Component: + Framework agnostic + Browser native + Long-lived - Less ecosystem React component: + Familiar + 큰 ecosystem + Server / streaming - React 만 ``` ### Atomic / Compound ```html Home About ``` ```ts class MyTabs extends LitElement { @state() activeTab = ''; firstUpdated() { const tabs = this.querySelectorAll('my-tab'); this.activeTab = tabs[0]?.getAttribute('name') ?? ''; } // ... } ``` ### Custom event ```ts this.dispatchEvent(new CustomEvent('select', { detail: { id: '...' }, bubbles: true, composed: true, // shadow DOM 톡과 })); ``` ```ts document.querySelector('my-card').addEventListener('select', (e) => { console.log(e.detail.id); }); ``` ### shadow vs light DOM ``` Shadow: scoped, encapsulated. Light: 일반 child β€” μ™ΈλΆ€ CSS 영ν–₯. β†’ Slot κ°€ light 의 일뢀. ::slotted(p) { color: red; } β€” slotted content 일뢀 styling. ``` ### Adoption ``` Apple Music web app: Web Components GitHub: λ§Žμ€ web component (custom element) Microsoft FAST: UI library Salesforce LWC: Large-scale web components Google Material: web component ν˜•νƒœ ``` β†’ 큰 νšŒμ‚¬ κ°€ design system 으둜 μ‚¬μš©. ## πŸ€” μ˜μ‚¬κ²°μ • κΈ°μ€€ | 상황 | μΆ”μ²œ | |---|---| | Cross-framework design system | Web Components (Lit) | | Embeddable widget | Web Components | | Single framework app | React / Solid λ“± native | | Shadow DOM 격리 critical | Web Components | | μž‘μ€ widget | Lit | | 큰 design system | Stencil | ## ❌ μ•ˆν‹°νŒ¨ν„΄ - **λͺ¨λ“  κ±° web component κ°•μ œ**: μž‘μ€ app κ°€ 의미 μ—†μŒ. - **A11y μ•ˆ μ‹ κ²½**: native 보닀 더 λ§Žμ€ 일. - **CSS κ°€ μ™ΈλΆ€ λͺ» customize**: design token / part API. - **Lifecycle 잘λͺ»**: connectedCallback κ°€ λ§€ attach. - **No SSR**: 큰 site = 빈 page first paint. - **Bundle 큰 framework + μž‘μ€ widget**: vanilla λ˜λŠ” Lit. ## πŸ€– LLM ν™œμš© 힌트 - Lit = modern + light. - Cross-framework design system 의 λ‹΅. - Declarative Shadow DOM = SSR. - Custom event + composed: true. ## πŸ”— κ΄€λ ¨ λ¬Έμ„œ - [[Frontend_Tailwind_Architecture]] - [[Frontend_Design_Tokens]] - [[React_Headless_UI_Patterns]]