---
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: `<my-button>` 같은 새 tag.
- Shadow DOM: scoped DOM + style.
- Slot: composition.
- Declarative Shadow DOM: SSR.

## 💻 코드 패턴

### 가장 간단 Custom Element
```ts
class HelloWorld extends HTMLElement {
  connectedCallback() {
    this.innerHTML = '<p>Hello, World!</p>';
  }
}
customElements.define('hello-world', HelloWorld);

// HTML
// <hello-world></hello-world>
```

### 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 = `
      <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)
```html
<my-card>
  <p>This goes into the default slot</p>
  <span slot="footer">Footer</span>
</my-card>
```

```ts
// Component
this.shadowRoot!.innerHTML = `
  <slot></slot>
  <hr>
  <slot name="footer"></slot>
`;
```

### CSS shadow parts
```ts
this.shadowRoot!.innerHTML = `
  <style>
    .badge { padding: 4px; background: blue; color: white; }
  </style>
  <span part="badge">${this.textContent}</span>
`;
```

```css
/* 외부 */
my-component::part(badge) {
  background: red;
}
```

→ Component 가 styling hook 노출.

### CSS custom property (theming)
```ts
shadow.innerHTML = `
  <style>
    :host { color: var(--my-color, black); }
  </style>
`;
```

```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`
      <button @click=${() => this.count++}>+</button>
      <span>${this.count}</span>
    `;
  }
}
```

→ 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 통과
}));

// 사용 측
<my-input @change=${this.handleChange}></my-input>
```

### 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);
  }
}
```

→ `<form>` 안에서 native input 처럼 동작.

### Declarative Shadow DOM (SSR)
```html
<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
```ts
import { render } from '@lit-labs/ssr';
const html = await render(`<my-card>...</my-card>`);
```

### React 안에서 Web Component
```tsx
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
```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 = () => `
  <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: 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]]
