아티클

CustomElements API: ShadowDOM 컴포넌트와 접근성

2022-09-26 18:11:50

안녕하세요, 엔비전스입니다.

지난 시간에는 ARIA Reflection API의 확장 폴리필인 WAI-ARIA Script 1.0 소개로 찾아뵈었습니다. customElements API 아티클을 다룰 때, 늘 ShadowDOM이 우리와 함께 사용했습니다. ShadowDOM이 뭘까요?

 

ShadowDOM이란?

ShadowDOM은 특정 요소 안에 숨겨진 완전히 독립된 캡슐화된 DOM을 만드는 것입니다. 마치 iframe 속의 문서처럼, 독립된 트리를 가집니다. 이와 비교되는 일반 DOM 요소들을 LightDOM이라고도 부릅니다.

 

오염에 강한 스타일시트 환경

외부 오염에 강력한 하위 DOM 환경을 요소 단위로 가지게 됩니다. 물론, 스크립트를 통해 접근 및 수정할 수 있지만, 외부 스타일시트로부터의 오염에 비교적 자유롭습니다.

 

전역 중복 id 방지

ShadowDOM은 하나의 작은 DOM 뿌리를 요소별로 갖게 합니다.

외부 스타일시트로부터 오염을 방지하는 것은 물론, 전역 요소의 id 중복 문제로부터 자유로워집니다.

id는 유일성을 가져야 하다 보니, 요소마다 이름을 짓는 것은 꽤 고된 일입니다. 그래서 웹페이지 마크업 시 id 정의를 최소화하는 경향이 있습니다.

접근성이 좋은 페이지 또는 위젯을 만드는 데 id 속성은 매우 중요한 역할을 합니다. ShadowDOM은 완전히 독립된 환경이기 때문에 문서 전역 id와 공유되지 않습니다. id의 유일성을 ShadowDOM 안에서만 신경 쓰면 된다는 소리입니다.

 

ShadowDOM은 어떻게 사용해요?

ShadowDOM은 Element.attachShadow(init); 메소드를 통해 생성할 수 있습니다. 생성 시 init 내에 mode 속성을 필수로 설정합니다.

cElement.attachShadow({mode:"open"});

초기화 객체의 mode 속성은 close 또는 open 스트링 값을 지정할 수 있습니다. close로 설정하면 cElement 요소에서 shadowRoot 속성을 사용하여 shadow에 접근할 수 없으며, open으로 사용하면, shadowRoot 속성을 통해 접근할 수 있게 됩니다. 일반적으로는 open을 사용합니다.

 

ShadowDOM을 생성할 수 있는 요소

모든 요소에 ShadowDOM을 생성할 수 있는 건 아닙니다. 다음 요소에 attachShadow() 메소드를 사용해 Shadow를 만들 수 있습니다.

  • <article> (interface HTMLSectionElement)
  • <aside> (interface HTMLElement)
  • <body> (interface HTMLBodyElement)
  • <blockquote> (interface HTMLQuoteElement)
  • <div> (interface HTMLElement)
  • <footer> (interface HTMLElement)
  • <h1> (interface HTMLHeadingElement) ~ <h6> (interface HTMLHeadingElement)
  • <header> (interface HTMLElement)
  • <main> (interface HTMLElement)
  • <nav> (interface HTMLElement)
  • <p> (interface HTMLParagraphElement)
  • <section> (interface HTMLElement)
  • <span> (interface HTMLSpanElement)

 

ShadowDOM의 친구들

ShadowDOM에게는 때 놓을 수 없는 두 친구가 있습니다. 바로 <template>와 <slot>입니다. 이 두 친구는 ShadowDOM의 재사용할 수 있는 틀을 구조화하는 아주 절친한 사이입니다.

템플릿 태그: <template> - HTMLTemplateElement

템플릿(<template>) 태그 안에는 다른 HTML 컨테이너 요소와 마찬가지로 다른 요소를 담을 수 있습니다. 하지만, 담는다고 해서, 바로 나타나지 않습니다. 이 친구는 말 그대로 템플릿이기 때문입니다. 템플릿 요소를 사용하려면 Javascript를 통한 DOM 조작이 꼭 필요합니다.

템플릿 태그는 content 객체를 가지고 있으며, ShadowDOM에 이 content가 딥 클론 되었을 때, 진가를 발휘합니다.

템플릿 태그는 일반적으로 아래와 같이 사용합니다.

  class CustomTabListElement extends HTMLElement {
    constructor(){
      super();
      this.attachShadow({mode:"open"});;
      const template = document.createElement('template');
      template = `<div id="tablist" role="tablist"><slot></slot><div>`;
      this.shadowRoot.appendChild(template.content.cloneNode(true));
      
      // ...
    }
  }

 

슬롯 태그: <slot> - HTMLSlotElement

슬롯(<slot>) 태그 역시 마찬가지입니다. 템플릿 또는 ShadowDOM 안에 구조로써 사용됩니다. 이름 그대로 슬롯입니다. 슬롯이라는 정해진 공간이 있고, 그곳에 부품을 꽂듯이 요소를 슬롯에 전달하면, ShadowDOM 또는 템플릿에 구조화된 위치에 표시하게 됩니다.

템플릿의 예제에서 잠깐 등장했는데요. 설계된 구조에 맞게 슬롯을 파는 것처럼, 원하는 위치에 slot을 배치하고, 특정 요소가 실제 DOM 위치와 상관없이 해당 slot 위치에 표시되게 합니다.

slot 태그에는 아래와 같이 name 속성이 포함될 수 있습니다.

  <span role="tab">
    <slot id="text"></slot>
    <slot name="icon"></slot>
  </span>

 

slot 속성이 없는 요소가 커스텀 요소 하위에 추가되었을 때, 기본적으로 name이 없는 슬롯에 담깁니다.

name 속성을 가진 이름이 지어진 슬롯(named slot)에 특정 요소를 나타나게 하려면, 다음과 같이 name을 slot 속성에 지정해 주면 됩니다.

<custom-tab-element>
  Foods <!-- 텍스트 또는 slot 이름이 지정되지 않은 요소는 이름 없는 슬롯에 표시됩니다. -->
  <span class="ico-chevron" slot="icon"></span>
</custom-tab-element>

 

DOM에서 slot에 연결된 요소에 접근하거나, 슬롯에 표시된 요소에서 slot을 가져올 수도 있습니다.

  1. 슬롯에 꽂힌 요소에서 슬롯 DOM 객체 가져오기

      slotted.assignedSlot // <slot...></slot>
  2. 슬롯 DOM 객체에서 슬롯에 꽂힌 요소 객체 가져오기

      slotted.assignedNodes() // ()=>Node[] : 텍스트 노드를 포함한 모든 요소를 가져옵니다.
      slotted.assignedElements() // ()=>HTMLElement[] : 텍스트 노드를 제외한 모든 요소를 가져옵니다.

ShadowDOM과 두 친구가 주는 접근성의 이점

ShadowDOM과 두 친구를 잘 활용하면 커스텀 WAI-ARIA 위젯을 만들 때, 마크업 구조를 단순화할 수 있습니다.

기본적으로 커스텀 요소는 아무런 요소 유형도 갖지 않습니다. 접근성 트리 상에서 무시된 상태로 랜더링 됩니다. 그리고, ShadowDOM에 클론 된 구조, slot에 위치에 따라 접근성 트리가 그려집니다.

말이 어려우니, 예를 들어보겠습니다. Tablist와 Tab 구조를 만들 때, 일반적으로 다음과 같이 작성합니다.

<ul role="tablist">
  <li role="none"><a href="#" role="tab" aria-selected="true">Foods</a></li>
  <li role="none"><a href="#" role="tab" aria-selected="false">Clothes</a></li>
  <li role="none"><a href="#" role="tab" aria-selected="false">Digital, IT And Appliances</a></li>
</ul>

딱 봐도 지저분하고 번거롭습니다.

접근성 트리 상에서 Tab은 Tablist 바로 아래에 있어야 올바른 정보를 주기 때문에 ul과 li 그리고 a 태그로 만들어진 탭 계층 구조에서는 항상 li에 role=“none”을 사용하여 요소 유형을 무력화해야만 합니다.

그러나, template와 slot으로 잘 구조화된 커스텀 탭 목록은 그렇지 않습니다.

  <tab-list>
   <tab-item selected>Foods</tab-item>
   <tab-item>Clothes</tab-item> 
   <tab-item>Digital & Appliances</tab-item>
  <tab-list>

HTML 구조만 보자면, 이게 끝입니다. 아주 심플해 보이지요?

아래는 DOM과 접근성 트리를 찍은 캡처 화면입니다.

‘DOM 트리 모습’

실제로는 tab-list는 래핑 요소일 뿐이고, 안에 있는 Shadow가 진짜 tablist 요소입니다. 그리고, slot은 tab-item과 ShadowDOM 안에 있는 진짜 tablist를 연결해 줍니다.

‘접근성 트리에서의 모습’분명히, tab과 tablist 사이에는 여러 계층이 존재하지만, 접근성 트리 상에서는 마치, role=“tablist” 요소 안에 “tab”이 있는 것으로 인식하게 됩니다.

이 커스텀 요소 스크립트 파일에는 분명히 이보다 많은 코드가 필요하지만, 잘만 구현한다면, 반복적인 마크업은 훨씬 편해집니다.

 

ShadowDOM의 단점

ShadowDOM에 무조건 장점만 있는 것은 아니지요. 아래 단점들이 있습니다.

  • 특정 스크린리더에서 가상 커서 인식이 제대로 안 되는 둥 ShadowDOM은 아직 꽤 최신 기술이기 때문에, 생각보다 버그가 많습니다.
  • CSS에 대한 이해도가 많이 필요합니다. 특히 CSS나 웹 아이콘 폰트 적용 시, ShadowDOM의 환경이 별도의 문서 환경임을 항상 생각해야 합니다.
  • 만능은 아닙니다. 분리된 문서 계층이기 때문에, aria-activedescendant나 aria-labelledby 같이 아이디를 참조하는 경우, LightDOM과 ShadowDOM은 서로 아이디를 공유하지 않기 때문에 접근성 적용 시 오류가 발생할 수 있습니다.

지금까지, ShadowDOM이 접근성에 주는 이점에 관해 설명했습니다. 감사합니다.

댓글 0
댓글을 작성하려면 해주세요.