[FE] 바닐라 자바스크립트로 웹 컴포넌트 만들기! - feat. 'POT' 프로젝트

이정훈·2021년 12월 7일
8
post-thumbnail

최근에 웹 컴포넌트라는 개념을 처음 듣고 관심이 생기게 되었다 😀

새로 알게 된 기술을 학습할 때 공식 문서를 참조하여 간단하게 작은 것이라도 만들어 보고자 하는 편인데, 더 좋은 방법은 기존에 진행했던 프로젝트를 발전시키는 데에 적용하는 것이라고 생각한다

최근에 진행한 프로젝트 중 게임을 같이 할 파티원들을 모집하는 'POT' 프로젝트를 리액트나 뷰와 같은 프레임워크 없이 진행하게 되었다.

문제는 멀티 페이지 애플리케이션이다 보니, 거의 모든 페이지에서 사용되는 헤더에 작성되는 html 코드가 중복되어 한 페이지에서 코드를 수정하게 된다면 다른 모든 페이지에서도 코드를 일일히 수정해줘야 한다는 것이다.

이를 재사용하기 쉽게 커스텀 태그로 변환하고, 관리하기 용이하게 리팩토링 해보고자 한다 🙂



웹 컴포넌트란?

재사용 가능한 커스텀 엘리먼트를 생성하고 웹 앱에서 활용할 수 있도록 해주는 다양한 기술들의 모음

세 가지 주요 기술들로 구성되며, 재사용을 원하는 어느 곳이든 코드 충돌에 대한 걱정이 없는 캡슐화된 기능을 갖춘 다용도의 커스텀 엘리먼트를 생성하기 위해 함께 사용될 수 있다.

  1. customElements
    새로운 HTML 요소를 만들거나 기존 HTML 요소를 확장할 수 있다.
  2. Shadow DOM
    문서의 다른 부분과 충돌할 염려 없이 스크립트를 작성하고 스타일을 지정할 수 있도록 캡슐화된 Shadow DOM 트리를 요소에 첨부하기 위한 API 모음
  3. HTML Template
    렌더링된 페이지에 표시되지 않는 마크업 템플릿을 작성하여 customElements 구조의 기초로 여러 번 재사용 할 수 있다.


구현 과정

커스텀 엘리먼트 생성

// components/Header.js
import setHeader from '../utils/header';

class Header extends HTMLElement {
  constructor() {
    super();

    this.innerHTML = `    
      <header class="header">
        <div class="header__wrapper">
          <h1>
            <a class="header__logo-link" href="/">
              <img class="header__logo" src="/images/logo.png" alt="logo">
            </a>
          </h1>
          <nav class="header__nav">
            <a class="header__nav-link" href="javascript:void(0);"><span class="header__nav-create-pot">POT 생성</span></a>
            <span class="header__nav-link login-info"></span>
          </nav>
        </div>
      </header> `;
  }

  connectedCallback() {
    setHeader();
  }
}

export default function defineMainHeader() {
  customElements.define('main-header', Header);
}

  • html 파일에서 공통으로 사용하던 header를 컴포넌트로 분리했다.
  • setHeader() 함수는 로그인 상태에 따라 로그인 버튼을 렌더링할지, 유저 이름을 렌더링할지 결정하는 기존 로직이다.


customElements.define()

  • 페이지에 커스텀 엘리먼트를 등록하는 메서드
  • 첫 번째 인자로 html에서 사용할 이름을, 두 번째 인자로 엘리먼트의 행위가 정의된 클래스를 전달한다
  • 이 메서드를 실행하는 함수를 모듈로 내보내 DOMContentLoaded 이벤트가 실행되면 defineMainHeader를 실행하도록 했다

lifecycle callback

커스텀 엘리먼트의 클래스 내에서 라이프사이클 콜백을 지정할 수 있다

  • connectedCallback - 사용자 정의 요소가 문서의 DOM에 처음 연결될 때 호출
  • disconnectedCallback - 사용자 정의 요소가 문서의 DOM에서 연결 해제될 때 호출
  • adoptedCallback - 사용자 정의 요소가 새 문서로 이동할 때 호출
  • attributeChangedCallback - 사용자 정의 요소의 어트리뷰트 중 하나가 추가, 제거 또는 변경될 때 호출

observedAttributes

static get observedAttributes() {
  return ['something'];
}
  • attributeChangedCallback 함수를 실행시키려면 observedAttributes에 관찰할 어트리뷰트를 명시해야 한다.
  • observedAttributes에 명시한 어트리뷰트 값이 변경되면 attributeChangedCallback를 실행한다.


기존 코드



커스텀 엘리먼트 적용 후

정상적으로 렌더링되는 것을 확인할 수 있고, 더 이상 헤더를 고칠때마다 매번 모든 페이지의 헤더를 고치는 것이 아니라 컴포넌트 단위로 수정하여 재사용성, 관리의 용이성을 높였다



❓HTML 모듈화

위의 실습을 통해 커스텀 엘리먼트를 생성해보았다
그렇다면 스타일까지 모두 지정해 컴포넌트만으로 의미가 있는 하나의 모듈처럼 사용할 수는 없을까?

결론은 "있다"

쉬운 방법으로는 위의 예시에 innerHTML 내부에 <style> 태그를 사용해서 그 안에 스타일을 지정해주면 제대로 동작한다.

하지만 css는 전역적으로 적용된다는 점 때문에 컴포넌트가 여러개 생기고 각각 스타일을 지정하다보면 충돌이 발생할 수도 있다

이를 Shadow DOM을 통해 해결할 수 있다



Shadow DOM

Shadow DOM은 결코 새로운 것이 아니다. 흔히들 알고 있는 <video> 태그나 <input type="range" />는 Shadow DOM 내부에 요소들이 포함되어 있다.

Shadow DOM을 사용하면 숨겨진 DOM 트리를 일반 DOM 트리의 요소에 첨부할 수 있다
Shadow DOM 트리는 모든 요소에 첨부할 수 있는 shadow 루트로 시작한다

  • Shadow host - shadow DOM이 연결된 일반 DOM 노드
  • Shadow tree - shadow DOM 내부의 DOM 트리
  • Shadow boundary - shadow DOM이 끝나고, DOM이 시작되는 곳
  • Shadow root - shadow tree의 루트 노드


특징

  • 노드와 같은 방식으로 Shadow DOM 노드에 영향을 줄 수 있다.
  • 차이점은 Shadow DOM 내부의 어떤 코드도 외부에 영향을 줄 수 없으므로 캡슐화를 허용한다는 것
  • 이러한 특징을 이용하여 Web Component의 완성도를 더 높일 수 있다.


Element.attachShadow()

const shadow = this.attachShadow({mode: 'open'});

  • Shadow root를 연결하는 메서드


위 사진과 같이 shadow-root가 연결되어 하위에 요소를 첨부할 수 있다
다만, 기존 프로젝트 스타일 적용 방식이 Sass를 컴파일 해서 적용하는 방식으로 사용했으므로 일관성을 위해 Shadow DOM은 학습 차원에서 마무리 지었다



느낀점

웹 컴포넌트를 학습하며 html, css, js가 모두 한 파일에 있는 것이 Vue.js의 .vue 파일과 상당히 유사하다는 느낌을 받았다
Vue나 React 같은 라이브러리나 프레임워크가 없던 시절에는 웹 컴포넌트를 사용했다고 한다
실제로 github에서는 프레임워크 없이 웹 컴포넌트를 사용해 사이트를 구축했다
특정 라이브러리나 프레임워크는 영원할 수 없고, 웹 컴포넌트는 표준 기술이기에 웹 컴포넌트를 사용해보는 것은 의미있는 일이라고 생각한다.

프로젝트가 이미 마무리된 시점에서 억지로 웹 컴포넌트를 적용하려고 하니 쉽지 않았고, 그렇기에 제한되는 조건이 많아서 아쉬움이 남는다. 하지만 이번 경험을 발판 삼아 다음에 다시 사용해 볼 기회가 생긴다면 더 잘해볼 수 있을 것 같다고 느낀다❗️




reference
https://developer.mozilla.org/en-US/docs/Web/Web_Components
https://www.youtube.com/watch?v=RtvSgptpfnY&t=284s![](https://velog.velcdn.com/images%2Fjhyj0521%2Fpost%2F51f6b40e-6d5f-456c-8749-1ecb46f26d81%2F%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202021-12-08%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%202.50.01.png)

0개의 댓글