[Part 1 ]Web Components
Web Components의 개념과 등장 배경, 4가지 핵심 기술(Custom Elements, Shadow DOM, Template, ES Modules)을 정리하고 첫 커스텀 태그를 만들어봅니다.
Web Components 완벽 정리 [Part 1] 개념과 등장 배경
Web Components란
브라우저에 내장된 표준 기술로, 캡슐화된 재사용 가능한 커스텀 HTML 태그를 만드는 API의 집합입니다. 라이브러리나 프레임워크가 아니며, 별도 설치 없이 모던 브라우저에서 바로 동작합니다.
<my-button variant="primary">눌러보세요</my-button>
<my-modal open>안녕하세요</my-modal>
<my-toast type="success">저장되었습니다</my-toast>위 태그들은 표준 HTML에 존재하지 않는 사용자 정의 태그이며, 내부 스타일과 동작이 외부와 완전히 격리됩니다.
등장 배경
1. CSS 전역 충돌
는 기본적으로 전역 스코프입니다. 동일한 클래스명이 여러 파일에 존재하면 의도치 않게 스타일이 덮어쓰여집니다.
css/* A 파일 */
.button { background: blue; }
/* B 파일 */
.button { background: red; }BEM, SMACSS, CSS Modules 같은 방법론은 컨벤션이지 브라우저가 강제하는 격리가 아닙니다. Shadow DOM은 브라우저 차원에서 스타일을 물리적으로 격리시킵니다.
2. 재사용 가능한 UI의 부재
<select> 같은 표준 태그는 드롭다운 동작, 키보드 네비게이션, 호버 효과가 내장되어 한 줄로 사용 가능합니다. 그러나 모달, 토스트, 탭 같은 커스텀 UI는 매번 <div>와 클래스, JavaScript 이벤트를 조합해 새로 만들어야 합니다. Web Components는 사용자 정의 UI도 표준 태그처럼 한 줄로 사용 가능하게 만듭니다.
3. 프레임워크 종속성
React 컴포넌트는 Vue 프로젝트에서 사용할 수 없습니다. 각 프레임워크가 컴포넌트를 정의하는 방식이 다르기 때문입니다. Web Components는 표준이므로 React, Vue, vanilla HTML, 서버 사이드 렌더링 환경 등 어디서든 동일하게 동작합니다.
React/Vue와의 관계
대체 관계가 아닌 보완 관계입니다. 항목Web ComponentsReact/Vue종류브라우저 표준외부 라이브러리캡슐화Shadow DOM (브라우저 강제)scoped CSS, CSS Modules상태 관리직접 구현 또는 라이브러리내장빌드 도구선택사항사실상 필수추가 런타임0 KB프레임워크 코드 필요 Web Components 적합 사례
디자인 시스템 (여러 프로젝트에서 공통 사용) 위젯, 임베드 (외부 사이트에 삽입) 마이크로 프론트엔드 레거시 프로젝트(PHP, jQuery)의 점진적 모던화
프레임워크 적합 사례
복잡한 라우팅이 필요한 SPA 대규모 상태 관리 빠른 개발 속도가 우선인 프로젝트
실무 패턴: Web Components로 디자인 시스템(저수준 UI)을 구축하고, 이를 React/Vue 안에서 사용. Adobe, Microsoft, GitHub, Salesforce가 이 방식을 채택하고 있습니다.
Web Components의 4가지 핵심 기술
1. Custom Elements
새로운 HTML 태그를 정의하는 API입니다.
class MyButton extends HTMLElement {
connectedCallback() {
this.textContent = '안녕하세요';
}
}
customElements.define('my-button', MyButton);규칙: 태그 이름에는 반드시 하이픈(-)이 포함되어야 합니다. 단일 단어 태그는 미래의 표준 HTML 태그와 충돌할 수 있어 금지됩니다.
2. Shadow DOM
컴포넌트 내부의 DOM과 CSS를 외부로부터 격리시키는 기술입니다.
class MyButton extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
button { background: blue; color: white; }
</style>
<button>안녕하세요</button>
`;
}
}Shadow DOM 내부의 스타일은 외부로 누출되지 않으며, 외부 페이지의 스타일도 내부로 침투하지 못합니다. !important로도 뚫을 수 없습니다.
3. HTML Templates와 Slot
<template>은 렌더링되지 않는 HTML 설계도이며, <slot>은 외부에서 내용을 주입받는 자리입니다. React의 props.children과 유사한 개념입니다.
<template id="card-template">
<div class="card">
<h2><slot name="title"></slot></h2>
<p><slot></slot></p>
</div>
</template>
<my-card>
<span slot="title">제목입니다</span>본문 내용입니다 </my-card>
4. ES Modules
의 모듈 시스템입니다. 컴포넌트를 import/export로 관리할 수 있습니다.
javascript// my-button.js
export class MyButton extends HTMLElement { /* ... */ }
customElements.define('my-button', MyButton);
html<script type="module" src="./my-button.js"></script>
<my-button></my-button>브라우저 지원
2026년 현재 모든 모던 브라우저(Chrome, Edge, Firefox, Safari)가 완벽 지원합니다.
Custom Elements v1: 2018년부터 Shadow DOM v1: 2018년부터 <template>: 그 이전부터 ES Modules: 2018년부터
폴리필 불필요. Internet Explorer는 Microsoft의 지원 종료로 고려 대상이 아닙니다.
첫 컴포넌트 만들기 (Hello World)
index.html 파일을 만들고 아래 코드를 그대로 입력 후 브라우저에서 열면 동작합니다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>첫 Web Component</title>
</head>
<body>
<hello-world></hello-world>
<hello-world name="제노"></hello-world>
<hello-world name="개발자"></hello-world>
<script>
class HelloWorld extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
const name = this.getAttribute('name') || '세상';
this.shadowRoot.innerHTML = `
<style>
.box {
padding: 16px;
background: #f0f4ff;
border: 2px solid #4f46e5;
border-radius: 8px;
font-family: sans-serif;
margin: 8px 0;
}
.name {
color: #4f46e5;
font-weight: bold;
}
</style>
<div class="box">
안녕하세요, <span class="name">${name}</span>!
</div>
`;
}
}
customElements.define('hello-world', HelloWorld);
</script>
</body>
</html>코드 분석
class HelloWorld extends HTMLElement 모든 HTML 요소의 부모 클래스인 HTMLElement를 상속받습니다. 이로써 클래스가 HTML 태그로 동작할 수 있는 자격을 갖춥니다. super() 부모 클래스의 생성자를 호출합니다. extends를 사용한 클래스의 constructor 내부에서 필수입니다. this.attachShadow({ mode: 'open' }) 요소에 Shadow DOM을 부착합니다. mode: 'open'은 외부 JavaScript에서 element.shadowRoot로 접근 가능함을 의미합니다. closed로 설정하면 shadowRoot가 null을 반환합니다. 실무에서는 거의 항상 open을 사용합니다. connectedCallback() 요소가 DOM에 삽입될 때 자동 실행되는 라이프사이클 콜백입니다. 초기 렌더링, 이벤트 등록, 데이터 fetching의 표준 위치입니다. this.getAttribute('name')
속성 값을 읽습니다. 값이 없으면 null을 반환하므로 || '세상'으로 기본값을 지정합니다.
this.shadowRoot.innerHTML = ...Shadow DOM 내부에 HTML과 CSS를 삽입합니다. 이 안의 <style>은 외부로 누출되지 않습니다. customElements.define('hello-world', HelloWorld) 브라우저의 CustomElementRegistry에 태그를 등록합니다. 이 시점부터 <hello-world> 태그가 페이지에서 동작합니다.
격리 검증
페이지에 아래 CSS를 추가해도 컴포넌트는 영향받지 않습니다.
<style>
.box { background: red !important; }
.name { color: black !important; }
</style>이는 Shadow DOM이 외부 CSS의 영향을 차단하기 때문입니다. 개발자 도구의 Elements 탭에서 <hello-world>를 펼치면 #shadow-root (open) 영역에 격리된 DOM 트리가 표시됩니다.
다음 글 예고
Part 2에서는 <template>과 <slot>을 다룹니다. Named slot, slotchange 이벤트, Light DOM과 Shadow DOM의 관계, React children과의 차이점을 포함합니다.