[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,327 @@
|
||||
---
|
||||
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]]
|
||||
Reference in New Issue
Block a user