컴포넌트 기반으로 프론트엔드 개발을 하기 위해 우리는 React, Vue, Svelte, Angular 등 다양한 현대 웹 개발에서 컴포넌트 기반으로 UI 개발하는 것은 당연한 이야기가 되었죠. 그리고 마침내 웹 컴포넌트(Web Components)를 통해서 별다른 프론트엔드 프레임워크가 없어도 웹 표준 기술만을 이용해서 UI 컴포넌트를 만들어서 프론트엔드 프레임워크, 라이브러리에 구속받지 않고 확장성 있는 컴포넌트를 구성할 수 있게 되었습니다.
웹 컴포넌트는 웹 사이트나 애플리케이션에서 UI를 모듈화하고 재사용할 수 있도록 해주는 웹 표준 기술입니다. React나 Vue.js와 같은 프론트엔드 프레임워크에서 오랫동안 경험해왔던 UI 컴포넌트를 떠올리시면 이해하기 쉽습니다.
하지만 React 컴포넌트는 React에서만 사용할 수 있고, Vue.js컴포넌트는 Vue.js에서만 사용할 수 있기 때문입니다. 하지만 웹 컴포넌트는 이와 같은 구애를 받지 않고 어떤 프론트엔드 프레임워크와도 함께 쓸 수 있습니다.
이러한 웹 컴포넌트의 뛰어난 호환성과 이식성, 재사용성 덕분에 디자인 시스템을 구현하는 최고의 기술로 주목받고 있습니다. 다만, 배우기 어렵고 진입 장벽이 높다는 단점이 있습니다.
웹 컴포넌트는 크게 다음과 같이 세 가지 핵심 기술로 이루어집니다.
뿐만 아니라 이 웹 표준 기술들은 주요 브라우저에서 모두 지원되고 있습니다. 지금부터 각각의 기반 기술을 하나씩 살펴보면서 웹 컴포넌트에 대한 이해를 높여보도록 하겠습니다.
HTML에는 현재 110개가 넘는 요소(element)가 있습니다. HTML4 시절에는 약 90개가 있었고, HTML 5때는 article
, section
와 같은 20여 개의 새로운 요소가 추가되었습니다.
div
, p
, a
, button
과 같이 HTML 명세에 정의되어 우리가 흔히 알고 있는 HTML요소를 표준 요소라고 합니다. 웹 브라우저와 스크린 리더와 같은 웹 접근성 도구들은 정해진 표준에 따라서 이러한 HTML요소들을 화면에 표시하고 구문적인 역할을 부여합니다.
반면에 사용자 정의 요소(custom elements)는 웹 표준에 저의되어 있지 않는 HTML 요소를 뜻합니다. 브라우저는 이러한 비표준 요소를 어떻게 처리하는지 알지 못하기 때문에, 마치 span 요소를 그리는 것처럼 아무 스타일 없이 표시해줍니다.
예를 들어, 일반 span요소와 red-span이라는 비표준 요소를 나란히 배치해볼까요?
<span>표준 요소</span> <red-span>비표준 요소</red-span>
참고로 사용자 정의 요소의 이름에는 반드시 하이픈(-)기호가 들어가도록 되어 있습니다. 미래에 HTML의 명세에 추가될 수 있는 표준 요소와의 이름 충돌을 원천적으로 방지하기 위함입니다.
어떻게 하면 red-span
과 같이 웹 표준에 정의되어 있지 않은 HTML 요소를 원하는 구조와 형태로 웹 페이지에 나타나게 할 수 있을까요? 다시 말해서, 사용자는 어떻게 새로운 HTML요소를 정의할 수 있을까요?
바로 정답은 웹 컴포넌트입니다.
웹 컴포넌트는 HTMLElement 라는 인터페이스를 확장해서 만들 수 있습니다. ES6의 클래스 문법을 사용하여, HTMLElement를 확장한 후, 해당 요소가 어떻게 생성되어야하는지를 구현할 수 있습니다.
예를 들어, red-span요소를 정의하기 위해서 RedSpan클래스를 작성해볼까요? 사용자 정의 요소라는 텍스트를 담고 있는 span요소를 마크업하고 글자색이 빨간색이 되도록 스타일을 해보겠습니다.
class RedSpan extends HTMLElement {
constructor() {
super();
this.innerHTML = `
<style>
span {
color: red;
}
</style>
<span>
사용자 정의 요소
</span>
`;
}
}
그 다음, customElements 전역 객체의 define()함수를 통해 요소 이름, red-span과 해당 요소를 정의하는 클래스, RedSpan을 연결해줍니다.
customElements.define("red-span", RedSpan);
다음과 같이 구현을 했을 때 전역적으로 적용되는 style 태그를 변경했기 때문에 모든 텍스트의 요소가 빨간색으로 변경되는 것을 알 수 있습니다.
이 부분은 Shadow DOM으로 개선할 수 있습니다.
위 웹 페이지를 유심히 보시면 한 가지 의아한 점을 발견하실 거에요. 바로 웹 컴포넌트 밖에 있는 span
요소의 글자색도 빨간색으로 변했다는 건데요. 이렇게 사용자 정의 요소를 위해서 작성한 스타일이 사용자 요소 밖까지 영향을 미친다면 곤란하겠죠??
이 문제를 해결하기 위해서는 웹 컴포넌트 안에 있는 span
클래스나 아이디를 달아주고, CSS에서 타입 선택자를 사용하는 대신에 클래스 선택자나 아이디 선택자를 사용할 수 있는데요
그런데 이렇게 전통적인 방법을 쓰지 않고 웹 컴포넌트를 마치 섬처럼 원천적으로 격리시키는 방법이 있습니다. 바로 Shadow DOM입니다.
HTML 문서를 JavaScript에서 효과적으로 제어할 수 있도록 다수의 노드(node)로 이루어진 하나의 트리(tree)로 다루는 프로그래밍 모델이죠.
Shadow DOM은 쉽게 말해서, HTML 문서 전체가 아닌 단일 웹 컴포넌트를 다루기 위한 작은 DOM입니다. Shadow DOM을 통해서 작성하는 HTML과 CSS, Javascript는 문서 전체의 DOM으로 부터 완전히 분리됩니다.
HTML 표준 요소 중에서 select, vide와 같은 녀석들도 Shadow DOM을 사용하고 있습니다. Shadow DOM의 이러한 격리성 때문에 이러한 요소들은 예전부터 스타일하기 굉장히 까다로운 것으로 정평이 나있죠.
웹 컴포넌트에서 Shadow DOM을 사용하는 방법은 간단합니다. 우선 attachShadow()함수를 통해서 해당 사용자 요소에 Shadow DOM을 연결합니다. 그 다음 ShadowRoot속성을 통해서 Shadow DOM 상대로 작업을 하면 됩니다.
class RedSpan extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = `
<style>
span {
color: red;
}
</style>
<span>
<slot/>
</span>
`;
}
}
customElements.define("red-span", RedSpan);
Shadow DOM을 이용하여 웹 컴포넌트를 완전히 격리시키고 slot요소를 사용해서 사용자 정의 요소가 감싸고 있는 내용도 살릴 수 있었지만 한 가지 마음에 걸리는 부분이 있습니다. 바로 JavaScript 코드 안에 바로 HTML과 CSS코드를 작성했다는 점인데요. 이렇게 하면 코드 편집기에서 구문 강조나 자동완성이 잘 동작하지 않아서 개발자 경험이 별로 좋지 않을 것입니다.
HTML Templates은 웹 컴포넌트의 이러한 단점을 보완해줄 수 있는 웹 표준 기술입니다. template
요소를 사용해서 작성하는 HTML 마크업을 HTML 템플릿이라고 하는데요. 브라우저는 template요소의 화면에 내용을 그려주지 않기 때문에, 웹 컴포넌트가 사용할 HTML코드를 보관하기 안성맞춤입니다.
<template id="red-span">
<style>
span {
color: red;
}
</style>
<span>
<slot />
</span>
</template>
RedSpan클래스가 방금 작성한 HTML 템플릿을 사용하도록 수정해주겠습니다. 생성자에서 아ㅣ디로 HTML 템플릿에 접근하고, 깊은 복제를 한 후 Shadow DOM에 추가합니다.
class RedSpan extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
const template = document.getElementById("red-span");
const clone = template.content.cloneNode(true);
this.shadowRoot.appendChild(clone);
}
}
customElements.define("red-span", RedSpan);