Code Logs

22.08.10

Web component의 핵심인 encapsulation은 어떻게 이루어질까?
Shadow DOM의 이해
고양이 빵 Cat bread - 세마리의 고양이와 홈베이킹 이야기

Web component - Shadow DOM

Web component의 핵심인 encapsulation은 어떻게 이루어질까? Shadow DOM의 이해

Table of contents

  1. Shadow DOM
  2. Document tree에 Shadow root 삽입하기
    1. Open mode shadow root
    2. Closed mode shadow root
    3. delegatesFocus를 이용한 focus 지정
  3. Composed 속성을 이용한 Custom Event 전파

Shadow DOM

웹 컴포넌트로 제작된 커스텀 엘리먼트는 Shadow DOM 영역에 존재하며 외부의 스타일 정의로 부터 독립적으로 동작할 수 있도록 돕고 컴포넌트 내부에서 발생하는 커스텀 이벤트의 버블링이 Shadow DOM 바깥의 영역까지 전파되어 발생 할 수 있는 side effect를 방지 할 수 있도록 돕는다.

이렇게 encapsulation 처리가된 커스텀 엘리먼트는 어떤 DOM Tree에 존재하더라도 항상 동일한 생김새와 기능을 보장 할 수 있다.

Shadow DOM은 구조적으로 아래의 용어로 세분화 할 수 있다.

  • Shadow root
    • Shadow rootShadow tree의 관점에서 바라본 root 노드를 의미한다.
  • Shadow host
    • Shadow hostDocument tree의 관점에서 바라본 Shadow treeroot 노드를 의미한다.
  • Shadow tree
    • Shadow treeDocument tree와 상응하는 개념으로 Shadow root를 포함한 모든 하위 노드의 트리를 의미한다.
  • Shadow boundary
    • Shadow boundaryShadow DOMDocument tree의 경계를 의미한다.

Document tree에 Shadow root 삽입하기

Shadow root를 생성하기 위해 ElementattachShadow 메서드를 사용한다.

const container = document.querySelector('div#container')
container.attachShadow({ mode: 'open' })

모든 엘리먼트에 Shadow DOM을 삽입 할 수 있을까?

보안상의 이유로 일부 엘리먼트에는 shadow dom을 삽입 할 수 없다. (ex. anchor) shadow dom을 삽입 할 수 있는 엘리먼트는 커스텀 엘리먼트와 article, aside, blockquote, body, div, footer, h1~h6, header, main, nav, p, section, span이 있다.

attachShadow 메서드는 option을 객체 형태의 매개변수로 전달 받는다. option 객체는 아래의 형식을 갖는다

interface ShadowRootInit {
  mode: 'closed' | 'open'
  delegatesFocus?: boolean
}

Open mode shadow root

shadow dom이 삽입 될 때 mode 프로퍼티를 open으로 설정하면 JavaScript를 통해 shadowRoot에 접근 할 수 있도록 허용한다. mode 프로퍼티는 앞서 이야기한 캡슐화와 관계 없는 옵션으로 open 모드를 사용하더라도 캡슐화는 여전히 유효하다.

document.body.attachShadow({ mode: 'open' })
console.log(document.body.shadowRoot) // #shadow-root (open)

Closed mode shadow root

shadow dom이 삽입 될 때 mode 프로퍼티를 closed로 설정하면 JavaScript를 통해 shadowRoot에 접근 할 수 없게된다.

document.body.attachShadow({ mode: 'closed' })
console.log(document.body.shadowRoot) // null

Closed 모드를 사용하는 것은 엔드유저가 할 수 있는 일에 상당한 제약을 준다. 더불어 closed 모드를 사용하더라도 shadow dom에 우회적으로 접근할 수 있다. 특별한 이유가 없다면 open 모드를 사용하는 것이 좋다.

Closed 모드인 shadow dom에 접근하는 방법

Element.prototype._attachShadow = Element.prototype.attachShadow
Element.prototype.attachShadow = function () {
  return this._attachShadow({ mode: 'open' })
}

delegatesFocus를 이용한 focus 지정

shadow dom은 또 다른 shadow dom 아래에 삽입 될 수 있다. 중첩된 shadow dom 중 상위 요소를 클릭 했을 때 focus가 대체될 대상 shadow dom을 생성하기 위해 delegatesFocus 속성을 사용한다.

document.body.attachShadow({ mode: 'open' })

const shadowRoot = document.body.shadowRoot
shadowRoot.innerHTML = `
  <style>
    div {
      padding: 20px;
      background-color: tomato;
    }
  </style>
  <div></div>
`
const div = shadowRoot.querySelector('div')
const focusableInput = div.attachShadow({
  mode: 'open',
  delegatesFocus: true,
})
focusableInput.innerHTML = `<input placeholder="focusable" />`

위와 같은 형식으로 DOMshadow DOM이 구성되어 있다면 input 엘리먼트를 감싸고 있는 div의 여백 영역을 클릭하면 input으로 포커스가 이동하게 된다.

Composed 속성을 이용한 Custom Event 전파

커스텀 엘리먼트를 만들게되면 Event를 통해 외부 요소와 커뮤니케이션 해야하는 일들이 빈번히 발생한다. click, touch, mouseover와 같은 모든 UI 이벤트는 기본적으로 composed 속성이 설정되어 있고, 다시 말해 shadow boundary를 넘어 외부 DOM 요소로 이벤트가 전파된다. 하지만 Custom Event를 사용할 경우 기본적으로 composedfalse로 설정되어 있다. 다시 말해 shadow boundary 내부에서 생성된 Custom Event는 기본적으로 외부에서 이벤트를 위임할 수 없다는 의미가 된다.

예측할 수 없는 커스텀 엘리먼트의 이벤트 전파로 인해 발생할 수 있는 부작용을 차단하기 위함이지만 경우에 따라 버블링을 통한 이벤트 전파 및 위임이 필요하기도 하다.

shadowElement.dispatchEvent(new CustomEvent('notify', {
  bubbles: true,
  composed: true
})

composed 속성은 반드시 bubbles 속성이 true일 때 의도와 같이 설정되고 마침내 상위 엘리먼트에서 하위의 shadow dom에서 발생한 custom event를 전파 받을 수 있게된다.

Event capturing과 shadow DOM

Event capturing은 shadow DOM에서 상위 엘리멘트로의 이벤트 전파가 아니기 때문에 shadow DOM의 존재 유무와 관계 없이 일반적인 형태로 흐른다.

연관 포스팅

카테고리 더보기

    참고

    댓글