HTML Custom Elements (feat: Shadow DOM)

2024. 9. 2. 23:48it/프론트엔드

Web Components : Custom Elements

html 에서도 react 나 vue 처럼 컴포넌트 단위로 작업을 할 수 있는지 궁금하여 검색해보다가 Custom Elements 에 대해서 알게 되었다.

문서에는 Custom Elements에 대해 아래와 같이 설명이 되어 있다.

웹 컴포넌트 표준의 주요 기능 중 하나는 사용자 정의 페이지 기능을 제공하는 길고 중첩된 요소들의 묶음으로 만족하는 것보다는, HTML 페이지에서 기능을 캡슐화하는 사용자 정의 요소를 생성하는 능력입니다.

 

Custom Elements 작성

크게 두 파트로 나뉜다. 1. 커스텀 요소를 구현. 하고 2. 커스텀 요소를 정의(등록) 한다.

 

1. 구현

class 클래스이름 extends HTMLParagraphElement

<script>
    class WordCount extends HTMLParagraphElement {
      constructor() {
        // 항상 super를 생성자에서 먼저 호출합니다
        super();

        // 요소 기능은 여기 작성됩니다

        ...
      }
    }
</script>

 

2. 정의(등록)

.define()

<script>
/* 
	첫번째 매개변수 : 요소의 이름 
	두번째 매개변수 : 요소의 기능을 정의한 클래스 이름
	세번째 매개변수 : 옵션(예: 실제로 확장할 HTML 태그 이름 등)
*/
	customElements.define("word-count", WordCount, { extends: "p" });
</script>

 

3. 사용

<body>
  <word-count></word-count>
</body>

 

Custom Elements 간단한 사용 예시

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

		this.style.border = '1px solid';
		this.addEventListender('click', (e) => e.preventDefault());
	}
}
customElements.define("my-button", MyButton);
<my-button>Click</my-button>

결과

결과 : 1px solid border 스타일을 가지고, click 이벤트를 가지고 있는 my-button 이라는 HTML 태그

Shadow DOM

웹 컴포넌트 API가 추가되면서, Custom Elements와 함께 Shadow DOM도 추가되었다고 한다.

문서에서는 Shadow DOM에 대해 아래 처럼 설명하고 있다.

웹 컴포넌트의 중요한 측면은 캡슐화입니다. 캡슐화를 통해 마크업 구조, 스타일, 동작을 숨기고 페이지의 다른 코드로부터의 분리하여 각기 다른 부분들이 충돌하지 않게 하고, 코드가 깔끔하게 유지될 수 있게 합니다. Shadow DOM API는 캡슐화의 핵심 파트이며, 숨겨진 분리된 DOM을 요소에 부착하는 방법을 제공합니다.

 

즉, Shadow DOM을 사용하면 마크업과 스타일, 동작을 전부 캡슐화할 수 있다.

 

Shadow DOM 작성 및 사용 예시

Shadow DOM을 사용하려면, Shadow DOM으로 사용하려는 일반적인 Element에 Shadow를 부착한다고 생각하면 쉽다.

Shadow를 부착하면 해당 요소는 Shadow root가 된다.
this.attachShadow({ mode: "open" }); 이 코드 하나로 Shadow root가 생성된다.

<script>
    class PopupInfo extends HTMLElement {
      constructor() {
        // Always call super first in constructor
        super();
      }

      connectedCallback() {
        // 해당 PopupInfo CustomElement에 shadow root 생성
        const shadow = this.attachShadow({ mode: "open" });
        // 옵션 mode가 open이면 javascript의 Element.shadowRoot를 사용하여 외부에서 접근이 가능하다.
        // 예시 : let myShadowDom = myCustomElem.shadowRoot;
        // 옵션 mode가 close이면 접근 불가능.

        // 다양한 요소 및 속성 추가
        // ...생략...

        // 외부 stylesheet도 추가 가능하다.
        const linkElem = document.createElement("link");
        linkElem.setAttribute("rel", "stylesheet");
        linkElem.setAttribute("href", "style.css");

        // shadow dom에 여러 Elements와 스타일을 부착할 수 있다.
        shadow.appendChild(linkElem);
        shadow.appendChild(wrapper);
        wrapper.appendChild(icon);
        wrapper.appendChild(info);
      }
    }

    // 위에서 본 Custom Element정의하기 처럼 define() 으로 정의.
    customElements.define("popup-info", PopupInfo);
</script>

 

위 처럼 작성하면 해당하는 요소는 shadow Element (shadow root)가 되고, 해당 root로부터 추가되는 모든 자식 요소들도 shadow가 된다.

shadow DOM 내부의 코드 중 아무 것도 shadow DOM 외부의 모든 것에 영향을 주지 않기 때문에 캡슐화를 가능케 합니다.

 

마지막

태그 이름을 커스텀하여 사용하기 때문에 혹시나 싶어 웹 표준이나 웹 접근성에 문제가 있지 않을까 생각되어서 검색해 보니, Custom Elements 는 당연하게도 웹 표준의 일부이므로 웹 표준에는 문제가 없다고 한다.

 

한편, 웹 접근성 측면에서는 몇가지 고려 사항이 있는데,

  • 명명 규칙으로 태그 이름에 반드시  하이픈(-)을 반드시 포함해야 한다고 한다.
    예를 들어 <my-element> 는 유효하지만, <myelement> 는 유효하지 않다.
    이 규칙은 기존 HTML 태그와 Custom Element를 구분하기 위해 존재한다고 한다.
  • ARIA 속성을 사용하여 요소의 역할과 상태를 명확히하면 좋다.
class MyButton extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'open' });
    const button = document.createElement('button');
    button.setAttribute('role', 'button'); // ARIA 속성 설정
    button.textContent = 'Click Me';
    shadow.appendChild(button);
  }
}
customElements.define('my-button', MyButton);
  • 기본 요소를 확장하여 사용하기.
    예를 들어 <button> 요소를 확장한 커스텀 버튼을 만들면, 기본 접근성 기능을 그대로 유지하면서 추가적인 기능을 제공할 수 있다.
    customElements.define() 함수의 3번째 매개변수로 확장할 HTML 태그 이름을 넣어주면 된다.
class MyButton extends HTMLButtonElement {
  constructor() {
    super();
    this.textContent = 'Custom Button';
  }
}
customElements.define('my-button', MyButton, { extends: 'button' });
<button is="my-button"></button>

 

 

 

참고

https://developer.mozilla.org/ko/docs/Web/API/Web_components/Using_custom_elements

https://developer.mozilla.org/ko/docs/Web/API/Web_components/Using_shadow_DOM

https://solo5star.tistory.com/25

https://github.com/mdn/web-components-examples/blob/main/popup-info-box-external-stylesheet/main.js