테마
08. Web Components: Custom Elements·Shadow DOM
Web Components는 "프레임워크를 버리자"는 구호가 아니라, 브라우저 수준에서 재사용 가능한 UI 경계를 만들 수 있다는 점에서 가치가 있다.
학습 목표
- Custom Elements, Shadow DOM,
<template>,<slot>의 역할을 설명할 수 있다. - 커스텀 엘리먼트 생명주기와 이벤트 경계를 이해한다.
ElementInternals를 통한 폼 연동 개념을 이해한다.- Web Components가 잘 맞는 경우와 그렇지 않은 경우를 구분할 수 있다.
1. Web Components는 세 가지 축으로 이해하면 된다
이 네 가지를 한꺼번에 세세히 외우기보다, "브라우저 기본 기능만으로도 재사용 컴포넌트를 만들 수 있다"는 큰 그림을 먼저 잡는 것이 좋다.
2. Custom Elements는 새 HTML 태그를 정의하는 방법이다
js
class AppBadge extends HTMLElement {
connectedCallback() {
this.textContent = 'New'
}
}
customElements.define('app-badge', AppBadge)핵심 규칙:
- 이름에 하이픈이 들어가야 한다
- 보통
HTMLElement를 상속받는다 - DOM에 연결될 때
connectedCallback()이 호출된다
대표 생명주기는 아래 정도를 먼저 알면 충분하다.
connectedCallback()disconnectedCallback()attributeChangedCallback()
3. customized built-in 요소는 실전 기본값이 아니다
원문은 기존 요소 확장 방식도 다루지만, 현재는 주의가 필요하다.
<button is="fancy-button">같은 customized built-in 요소는 브라우저 이식성이 매끄럽지 않다- 특히 Safari 호환성 이슈 때문에 범용 UI 라이브러리 기본 선택지로 보기 어렵다
그래서 학습용 문서에서는 자율 커스텀 엘리먼트(autonomous custom elements) 를 기본 경로로 두는 편이 현실적이다.
4. Shadow DOM은 구조와 스타일 경계를 만든다
js
const shadowRoot = this.attachShadow({ mode: 'open' })
shadowRoot.innerHTML = `
<style>
:host { display: inline-block; }
</style>
<button><slot></slot></button>
`Shadow DOM의 핵심은 다음 두 가지다.
- 내부 마크업과 스타일을 외부 문서와 어느 정도 분리
- 컴포넌트의 공용 API를 더 명확하게 만들 수 있음
open과 closed
| 모드 | 의미 |
|---|---|
open | 외부에서 element.shadowRoot로 접근 가능 |
closed | 외부에서 직접 접근 불가 |
실무에서는 디버깅과 협업 편의 때문에 open을 더 자주 본다.
5. 스타일링은 완전 차단이 아니라 경계 설계다
Shadow DOM을 쓰면 내부 스타일 충돌이 줄어든다.
다만 "외부에서 절대 건드릴 수 없다" 수준으로만 보면 오해가 생긴다.
같이 알아 두면 좋은 선택지:
:host:host(...)::slotted(...)::part(...)
즉, 캡슐화는 폐쇄가 아니라 "어떤 면을 외부에 열어 둘 것인가"를 설계하는 작업이다.
6. <template>와 <slot>은 재사용 구조의 기본이다
<template>
- 로드 시 바로 렌더링되지 않는 비활성 마크업
- 복제해서 여러 컴포넌트의 기반 구조로 사용 가능
<slot>
- 외부에서 전달한 자식을 Shadow DOM 내부 특정 위치에 투영
- 기본 슬롯과 이름 있는 슬롯을 지원
html
<app-card>
<h2 slot="title">제목</h2>
<p>본문</p>
</app-card>이 구조를 알면 "내부 구조는 내가 통제하고, 일부 내용만 외부에서 넣게 하겠다"는 설계가 쉬워진다.
7. slotchange와 이벤트 경계
슬롯에 들어가는 콘텐츠가 바뀌면 slotchange로 감지할 수 있다.
또 Shadow DOM에서는 이벤트가 경계를 넘는 방식도 중요하다.
- 어떤 이벤트는 Shadow DOM 밖으로 전파된다
- 이때
event.composed와event.composedPath()를 보면 경로를 더 정확히 이해할 수 있다 open과closed모드에 따라 외부에서 보이는 경로 정보가 달라질 수 있다
즉, Web Components에서는 DOM 구조뿐 아니라 이벤트 모델도 같이 바뀐다.
8. ElementInternals는 폼과 상태를 연결한다
원문 후반의 고급 주제 중 하나가 이 부분이다.
- 커스텀 엘리먼트를 폼 제출에 참여시킬 수 있다
setFormValue()로 전송 값을 설정할 수 있다- 내부 상태와 접근성 정보를 브라우저와 연결할 수 있다
이 기능 덕분에 Shadow DOM 내부 구현을 유지하면서도, 외부에서는 일반 폼 컨트롤처럼 다루는 길이 열린다.
다만 이건 기본 DOM 입문 주제가 아니라, 이미 폼과 커스텀 엘리먼트 기본기를 갖춘 뒤 들어가는 고급 영역으로 보는 편이 맞다.
9. Declarative Shadow DOM도 알아 둘 가치가 있다
서버 렌더링과 Web Components를 함께 쓸 때 중요한 최신 흐름이 선언적 Shadow DOM이다.
<template shadowrootmode="open">형태로 서버 HTML에 Shadow DOM 구조를 표현- 초기 렌더링과 하이드레이션 사이의 간극을 줄이는 데 도움이 됨
이 기능 하나만으로 Web Components를 선택할 이유가 생기지는 않지만, "브라우저 수준 컴포넌트도 SSR과 연결할 길이 열리고 있다"는 점은 기억할 만하다.
10. Web Components는 언제 잘 맞을까?
잘 맞는 경우
- 프레임워크를 넘나드는 공용 디자인 시스템
- 외부 사이트에 삽입하는 위젯
- 마이크로프론트엔드 경계의 공용 UI
덜 맞는 경우
- 단일 프레임워크 기반 대형 SPA 전체를 모두 대체하려는 경우
- 팀이 Shadow DOM과 생명주기 모델에 익숙하지 않은 경우
- 라이브러리 생태계의 도움을 많이 받아야 하는 복합 앱
즉, Web Components는 만능 대체재가 아니라 브라우저 표준 기반의 강한 경계가 필요할 때 빛난다.
11. 흔한 안티패턴
| 안티패턴 | 문제점 | 더 나은 방식 |
|---|---|---|
| 모든 컴포넌트를 WC로 재작성하려는 시도 | 팀 생산성 저하 가능 | 경계가 필요한 부분에 선택적으로 사용 |
| customized built-in을 기본 전략으로 채택 | 이식성 문제 | 자율 커스텀 엘리먼트 우선 |
| Shadow DOM을 완전 차단 수단으로만 이해 | 외부 API 설계가 빈약해짐 | slot, part, 이벤트 계약까지 설계 |
disconnectedCallback() cleanup 누락 | 리스너와 observer 누수 | 생명주기마다 정리 코드 포함 |
12. PR 리뷰 체크리스트
- 이 컴포넌트는 정말 브라우저 수준 경계가 필요한가
- 공개 API가 속성, 이벤트, 슬롯 구조로 명확히 설계되어 있는가
- Shadow DOM 내부 리스너와 observer cleanup이 보장되는가
- customized built-in 대신 더 이식성 높은 선택을 했는가
- 폼 연동이 필요하다면
ElementInternals가 적절한지 검토했는가
핵심 정리
- Web Components는 Custom Elements, Shadow DOM, template/slot을 묶어서 이해하면 된다
- 장점은 재사용과 캡슐화이지만, 모든 UI의 만능 해법은 아니다
- 현재는 자율 커스텀 엘리먼트, Shadow DOM 경계,
ElementInternals, 선언적 Shadow DOM까지 함께 보는 편이 현실적이다