Logo

웹 컴포넌트 (Web Components)

컴포넌트 기반 UI 개발의 패러다임을 주도한 React가 등장한지도 어느 덧 10년이 넘었네요. 이제 Vue.js, Svelte, Angular 등 어떤 프론트엔드 프레임워크를 사용하든 현대 웹 개발에서 컴포넌트 기반으로 UI 개발하는 것은 거의 당연한 얘기가 되었죠. 그리고 마침내 웹 컴포넌트(Web Components)를 통해서 별다른 프론트엔드 프레임워크가 없어도 웹 표준 기술만을 이용해서 UI 컴포넌트를 만들 수 있는 길이 활짝 열렸습니다.

이번 포스팅에서는 UI 컴포넌트를 만들 수 있는 웹 표준 기술인 웹 컴포넌트의 기본 개념을 간단한 실습을 통해서 알아보도록 하겠습니다.

Web Components란?

웹 컴포넌트는 웹사이트나 애플리케이션에서 UI를 모듈화하고 재사용할 수도록 해주는 웹 표준 기술입니다. React와 같은 프론트엔드 프레임워크에서 오랫동안 경험해왔던 UI 컴포넌트를 떠올리시면 이해가 쉬우실 것 같습니다.

React 컴포넌트는 React에서만 쓸 수 있고, Vue.js 컴포넌트는 Vue.js에서만 쓸 수 있는 고질적인 한계가 있습니다. 하지만 웹 컴포넌트는 이와 같은 구애를 받지 않고 어떤 프론트엔드 프레임워크와도 함께 쓸 수 있습니다. 왜냐하면 웹 컴포넌트는 브라우저에서 네이티브(native)하게 작동하기 때문입니다.

이러한 웹 컴포넌트의 띄어난 호환성과 이식성, 재사용성 및 생산성 덕분에 최근에는 디자인 시스템(Design System)을 구현하는 최고의 기술로 주목받고 있습니다.

모든 기술이 그러하든 웹 컴포넌트가 장점만 있는 것은 아닙니다. 대표적인 단점으로 API가 직관적이지 않다고 평가받고 있고, 배우기 쉬운 기존 프런트엔드 프레임워크 대비 진입 장벽이 높은 편입니다. 그래서 실제 프로젝트에서는 이러한 단점을 보완한기 위해서 웹 컴포넌트를 직접 작성하기 보다는 LitStencil과 같은 라이브러리를 많이 사용합니다.

웹 컴포넌트은 크게 다음 세 가지 핵심 기술로 이루어집니다.

  • Custom Elements: 사용자 정의 HTML 요소(element) 또는 태그(tag)를 정의할 수 있습니다.
  • Shadow DOM: DOM 트리의 일부를 캡슐화하여 스타일과 마크업이 외부와 충돌하지 않도록 해줍니다.
  • HTML Templates: 화면에 나타나지 않는 HTML 조각(fragment)을 정의해놓고 재사용할 수 있습니다.

이 웹 표준 기술들은 현재 주요 브라우저에서 모두 지원되고 있습니다.

web-components-browser-support (출처: https://www.webcomponents.org/)

지금부터 각각의 기반 기술을 하나씩 살펴보면서 웹 컴포넌트에 대한 이해를 높여보도록 하겠습니다.

Custom Elements

HTML에는 현재 110개가 넘는 요소(element)가 있습니다. HTML 4 시절에는 약 90개가 있었고, HTML 5 때 <article>, <section>, <nav>와 같은 20여개의 새로운 요소 추가 되었습니다.

<div>, <p>, <a>, <img>, <button>과 같이 HTML 명세에 정의되어 우리가 흔히 알고 있는 HTML 요소를 표준 요소(standard element)라고 합니다. 웹 브라우저와 스크린 리더와 같은 웹 접근성 도구들은 정해진 표준에 따라서 이러한 HTML 요소들을 화면에 표시하고 구문적인 역할을 부여합니다.

반면에 사용자 정의 요소(custom elements)는 웹 표준에 정의되어 있지 않는 HTML 요소를 뜻합니다. 브라우저는 이러한 비표준 요소(nonstandard element)를 어떻게 처리해야 하는지 알지 못하기 때문에, 마치 <span> 요소를 그리는 것처럼 아무 스타일없이 표시해줍니다.

예를 들어, 일반 <span> 요소와 <red-span>이라는 비표준 요소를 나란히 배치해볼까요?

<span>표준 요소</span> <red-span>비표준 요소</red-span>

그러면 아래와 같이 브라우저는 이 두 요소를 동일하게 그려주는 것을 볼 수 있습니다.

참고로 사용자 정의 요소의 이름에는 반드시 하이픈(-) 기호가 들어가도록 되어 있습니다. 미래에 HTML의 명세에 추가될 수 있는 표준 요소와의 이름 충돌을 원천적으로 방지하기 위합니다.

HTMLElement Interface

어떻게 하면 <red-span>과 같이 웹 표준에 정의되어 있지 않는 HTML 요소를 원하는 구조와 형태로 웹 페이지에 나타나게 할 수 있을까요? 다시 말해서, 사용자는 어떻게 새로운 HTML 요소를 정의할 수 있을까요?

네, 바로 정답은 웹 컴포넌트입니다!

웹 컴포넌트는 HTMLElement라는 인터페이스를 확장해서 만들 수 있습니다. ES6의 클래스(class) 문법을 사용하여, 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);

브라우저에서 동일한 웹 페이지를 다시 열어보면, 우리가 정의한데로 <red-span> 요소가 나타나는 것을 볼 수 있습니다.

여기서 <red-span> 요소로 감싼 비표준 요소 대신에 사용자 정의 요소가 텍스트로 표시되는 부분에 주의 바랍니다. 이 뜻은 <red-span> 요소 안에 어떤 텍스트를 넣든 무시되며, 웹 컴포넌트 내에서 마크업한 내용이 표시된다는 뜻입니다. 이 부분은 바로 다음 섹션에서 개선할 것입니다.

Shadow DOM

위 웹 페이지를 유심히 보시면 한 가지 의아한 점을 발견하실 거에요. 바로 웹 컴포넌트 밖에 있는 <span> 요소의 글자색도 빨간색으로 변했다는 건데요. 이렇게 사용자 정의 요소를 위해서 작성한 스타일이 사용자 요소 밖까지 영향을 미친다면 곤란하겠죠?

이 문제를 해결하기 위해서는 웹 컴포넌트 안에 있는 <span> 요소에 클래스나 아이디를 달아주고, CSS에서 타입 선택자를 사용하는 대신에 클래스 선택자나 아이디 선택자를 사용할 수 있는데요.

CSS의 선택자에 대해서는 별도 포스팅에서 자세히 다루고 있으니 참고하세요.

그런데 이렇게 전통적인 방법을 쓰지 않고 웹 컴포넌트를 마치 섬처럼 원천적으로 격리시키는 방법이 있습니다. 바로 Shadow DOM(쉐도우 돔)입니다.

우선 웹 개발을 좀 해보셨다면 DOM(document object model)에 대해서는 다들 아실 거에요. HTML 문서를 JavaScript에서 효과적으로 제어할 수 있도록 다수의 노드(node)로 이루어진 하나의 트리(tree)로 다루는 프로그래밍 모델이죠.

Shadow DOM은 쉽게 말해서, HTML 문서 전체가 아닌 단일 웹 컴포넌트를 다루기 위한 작은 DOM입니다. Shadow DOM을 통해서 작성하는 HTML과 CSS, JavaScript는 문서 전체의 DOM으로 부터 완전히 분리됩니다.

HTML 표준 요소 중에서 <select><video>와 같은 녀석들도 Shadow DOM을 사용하고 있습니다. Shadow DOM의 이러한 격리성 때문에 이러한 요소들은 예전부터 스타일하기 굉장히 까다로운 것으로 정평이 나있죠.

웹 컴포넌트에서 Shadow DOM을 사용하는 방법은 매우 간단합니다. 우선 attachShadow() 함수를 통해서 해당 사용자 요소에 Shadow DOM을 연결합니다. 그 다음 shadowRoot 속성을 통해서 Shadow DOM 상대로 작업을 하면 됩니다.

예를 들어, RedSpan 클래스가 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);

이제 원했던 바와 같이 <span> 요소의 글자색이 다시 검정색으로 돌아오는 것을 확인할 수 있으실 겁니다.

여기서 <span> 요소 안에 사용자 정의 요소 텍스트 대신에 <slot/>을 사용한 것을 주의깊게 봐주세요. 이렇게 Shadow DOM을 사용하면 사용자 요소가 감싸고 있는 내용도 그대로 보여줄 수가 있게 됩니다. 따라서 원래의 텍스트였던 비표준 요소가 다시 웹 페이지에 나타나는 것을 볼 수 있습니다.

위 예제에서는 attachShadow() 함수를 호출할 때, mode 옵션을 open으로 주었지만 closed로 줄 수도 있습니다. mode 옵션이 open으로 설정되어 있으면 외부에서 해당 웹 컴포넌트의 Shadow Dom을 shadowRoot 속성을 통해서 접근할 수 있지만, closed로 설정되어 있으면 외부에서 Shadow Dom 접근이 완전히 차단됩니다.

HTML Templates

Shadow DOM을 이용하여 웹 컴포넌트를 완전히 격리시키고 <slot> 요소를 사용해서 사용자 정의 요소가 감싸고 있는 내용도 살릴 수 있었지만 한 가지 마음에 걸리는 부분이 있습니다. 바로 JavaScript 코드 안에 바로 HTM과 CSS 코드를 작성했다는 점인데요. 이렇게 하면 코드 편집기에서 구문 강조(syntax highlighting)나 자동 완성(autocomplete) 기능이 잘 동작하지 않아서 개발자 경험이 별로 좋지 않을 것입니다.

HTML Templates(템플릿)은 웹 컴포넌트의 이러한 단점을 보완해줄 수 있는 웹 표준 기술입니다. <template> 요소를 사용해서 작성하는 HTML 마크업을 HTML 템플릿이라고 하는데요. 브라우저는 <template> 요소의 내용을 화면에 그려주지 않기 때문에, 웹 컴포넌트가 사용할 HTML 코드를 보관히기 안성맞춤입니다.

실습을 위해서 OurDiv 클래스 안에 있던 <style> 요소와 <span> 요소를 그대로 HTML 문서의 <body> 태그 아래로 복사해옵니다. 그 다음 <template> 요소로 감싸주시고, 웹 컴포넌트에서 참조할 수 있도록 아이디 속성을 달아주세요.

<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);

웹 페이지는 동일한 결과를 나타나지만, JavaScript 코드로 부터 HTML과 CSS 코드가 분리되었습니다. 이러한 방식으로 웹 컴포넌트를 작성하는 것이 가독성과 유지 보수성 측면에서 분명 유리하겠죠?

HTML 템플릿은 일반 요소와 다르게 사용하려면 우선 복제가 선행되야 합니다. HTML 템플릿 자체를 Shadow DOM에 추가하시면 브라우저는 해당 사용자 요소를 그려주지 않을 것입니다. 왜 “템플릿”이라고 불리는지 그 의미를 곱씹어 보시면 납득이 되실 거라고 생각합니다.

마치면서

지금까지 웹 컴포넌트이 무엇이고 어떻게 사용하는지에 대해서 아주 대략적으로 알아보았습니다. 그리고 웹 컴포넌트를 가능케하는 기반 기술인 Custom Elements, Shadow DOM, HTML Templates에 대해서도 살펴보았습니다.

본 포스팅에서 웹 컴포넌트에 대해서 정말 기본적인 내용만 다루었으며, 웹 컴포넌트를 제대로 쓰시려면 속성(attribute)에 어떻게 접근하고 이벤트 처리는 어떻게 해야하는지에 대해서 배우셔야 합니다. 이 부분에 대해서는 후속 포스팅에서 다루도록 하겠습니다.