React, Vue, Svelte 같은 프레임워크 없이도 재사용 가능한 UI 컴포넌트를 만들 수 있다는 사실, 알고 계셨나요? 바로 웹 컴포넌트(Web Components)를 활용하면 됩니다. 브라우저가 기본으로 지원하는 표준 기술이기 때문에 별도의 번들러나 라이브러리 없이도 동작합니다.
웹 컴포넌트는 세 가지 핵심 기술로 구성됩니다.
- Custom Elements — 나만의 HTML 태그를 정의
- Shadow DOM — 캡슐화된 독립적인 DOM 트리
- HTML Templates — 렌더링 전까지 파싱되지 않는 마크업
Custom Elements
Custom Elements를 사용하면 <my-button>처럼 자신만의 HTML 요소를 만들 수 있습니다. HTMLElement를 상속받아 클래스를 정의하고, customElements.define()으로 등록합니다.
class MyButton extends HTMLElement {
constructor() {
super();
this.addEventListener('click', () => {
console.log('클릭됨!');
});
}
// 요소가 DOM에 추가될 때 호출
connectedCallback() {
this.render();
}
render() {
this.innerHTML = `<button>${this.getAttribute('label') || '클릭'}</button>`;
}
}
customElements.define('my-button', MyButton);
이제 HTML에서 바로 <my-button label="저장"></my-button>처럼 사용할 수 있습니다.
Shadow DOM
Shadow DOM은 컴포넌트의 내부 DOM과 스타일을 외부로부터 완전히 격리합니다. 외부 CSS가 내부를 침범하지 못하고, 내부 스타일이 전역에 영향을 주지 않습니다.
class MyCard extends HTMLElement {
constructor() {
super();
// Shadow root 생성 (mode: 'open'이면 외부에서 접근 가능)
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
/* 이 스타일은 이 컴포넌트에만 적용됩니다 */
.card {
padding: 1rem;
border: 1px solid #e3e6ea;
border-radius: 0.75rem;
background: white;
}
h2 { color: #6891f8; }
</style>
<div class="card">
<h2><slot name="title">기본 제목</slot></h2>
<p><slot>내용을 입력하세요.</slot></p>
</div>
`;
}
}
customElements.define('my-card', MyCard);
<slot>을 사용하면 외부에서 내용을 주입할 수 있습니다.
<my-card>
<span slot="title">웹 컴포넌트</span>
Shadow DOM을 활용한 완전한 캡슐화를 지원합니다.
</my-card>
HTML Templates
<template> 태그 안의 내용은 페이지가 로드될 때 렌더링되지 않습니다. JavaScript로 복제(clone)해서 DOM에 추가할 때 비로소 화면에 나타납니다.
<template id="card-template">
<div class="card">
<h2 class="card-title"></h2>
<p class="card-body"></p>
</div>
</template>
const template = document.getElementById('card-template');
const clone = template.content.cloneNode(true);
clone.querySelector('.card-title').textContent = '제목';
clone.querySelector('.card-body').textContent = '본문 내용';
document.body.appendChild(clone);
세 기술을 합친 완성형 컴포넌트
실제로는 세 가지를 조합해서 사용합니다. 아래는 점심 메뉴 추천 컴포넌트의 예시입니다.
class LunchRecommender extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
const template = document.createElement('template');
template.innerHTML = `
<style>
:host { display: block; padding: 2rem; border-radius: 1rem; }
button { background: #6891f8; color: white; border: none;
padding: 0.75rem 1.5rem; border-radius: 0.5rem; cursor: pointer; }
</style>
<div>
<p id="result">버튼을 눌러보세요!</p>
<button id="btn">추천받기</button>
</div>
`;
shadow.appendChild(template.content.cloneNode(true));
const menus = ['김치찌개', '비빔밥', '파스타', '초밥', '카레'];
shadow.getElementById('btn').addEventListener('click', () => {
shadow.getElementById('result').textContent =
menus[Math.floor(Math.random() * menus.length)];
});
}
}
customElements.define('lunch-recommender', LunchRecommender);
마치며
웹 컴포넌트는 프레임워크 없이도 재사용 가능한 UI를 만드는 브라우저 표준입니다. Custom Elements로 태그를 정의하고, Shadow DOM으로 캡슐화하며, HTML Templates로 마크업을 효율적으로 재사용하세요.
React나 Vue 같은 프레임워크와 함께 사용할 수도 있고, 단순한 위젯이나 디자인 시스템을 만들 때 특히 빛납니다. 브라우저 지원도 모든 모던 브라우저에서 완벽하게 동작하니 지금 바로 시작해보세요!