# Shadow DOM

우리는 DOM트리 구축 후 웹에서 제공하는 API를 통해 자유롭게 요소에 접근할 수 있다. DOM 요소의 위치에 상관없이 어디에서나 접근할 수 있다. 이 말은 즉슨 DOM 요소 모두가 글로벌 스코프에 위치하였다는 것이다. 글로벌 스코프에 위치한 요소에 대해 통일된 속성을 한꺼번에 적용할 수 있다는 것은 장점이지만 동시에 큰 단점이 된다. 특정 요소를 캡슐화(encapsulation - 컴포넌트화) 하기가 어렵다는 것이다.

위 요소는 iframe으로, 웹 페이지를 현재 페이지 내에 삽입하는 태그이다. 캡슐화 성격이 어느정도 있다고 볼 수 있지만 현재 페이지에 임포트 중인 클래스 명과 충돌을 일으키거나 각종 속성과 맞지 않는 부분이 생기면 원하는 결과물이 나오지 않을 수 있다. 즉 완전한 캡슐화가 이루어지지 않은 것이다. iframe도 해결하지 못하는 요소 캡슐화를 어떻게 해결할 수 있을까? 바로 섀도우 돔을 이용하면 된다.

섀도우 돔은 DOM within a DOM 으로 간단히 정의할 수 있다.input 태그를 생각해보자.


크롬 개발자 도구 - Prference - Elements 메뉴에 Show user agent shadow DOM 체크박스를 체크하면 게이지 바를 이루는 내부 요소들을 표시해준다.

섀도우로 만들어진 내부 요소는 전역 스코프에서의 DOM으로부터 별다른 영향이 없다. 섀도우 돔을 직접 만들어보자.

먼저 섀도우 요소가 들어갈 호스트를 만든다. 이는 오리지널 DOM의 HTML요소이며 이 요소 안에 섀도우 돔이 부착된다. (다음의 코드를 현재 문서의 크롬 개발자 도구 콘솔에 입력해보자.

const spanTag = document.createElement("span");
spanTag.setAttribute("class", "shadow-host"); // 버튼 제작

const aTag = document.createElement("a");
aTag.setAttribute("href", "https://parkjju.github.io/vue-TIL/");

spanTag.appendChild(aTag); // 버튼 링크를 자식 요소로 삽입

const txt = document.createTextNode("Go to Parkjju's TIL!!"); // a태그의 자식 텍스트 노드 생성
aTag.appendChild(txt);

// spanTag를 문서 아무곳이나 붙여주세요.
// 저는 현재 문서 기준 Shadow DOM 요약 문서 아래쪽에 붙이겠습니다.
document.querySelector("#shadow-dom").nextElementSibling.appendChild(spanTag);

// 그런 다음 문서로부터 방금 붙인 spanTag요소를 다시 찾아서 읽어옵니다.
const shadowElement = document.querySelector(".shadow-host");

// 읽어온 요소를 섀도우 돔으로 만듭니다.
const shadow = shadowElement.attachShadow({ mode: "open" });

console.log(shadowElement);

섀도우 요소를 직접 찍어보면 shadowElement 내에 섀도우 루트가 정상적으로 삽입된 것을 볼 수 있다. #shadow-root라고 되어 있는 부분은 HTML 문서의 시작을 알리는 html태그라고 보면 된다. DOM 엔트리 포인트인 html로 봐도 된다.

섀도우 삽입과 동시에 기존에 보이던 요소가 갑자기 사라지게 되는 것을 볼 수 있는데, 추가적인 작업을 더 진행해보자. (해당 돔은 엔트리포인트가 더 이상 html이 아닌 shadow-root가 되어 페이지에서 사라지는 것이다.)

이제 우리는 루트가 shadow-root인 새로운 트리를 구축해야한다. DOM과 닮았지만 전혀 다른 트리이다. 섀도우 트리 구축을 위해 문서를 작성해보자.

const link = document.createElement("a");
const aTagTextShadow = document.createTextNode(
  `${shadowElement.querySelector("a").textContent}`
);

link.appendChild(aTagTextShadow);
link.href = shadowElement.querySelector("a").href;

shadow.appendChild(link); // 뿅

링크가 나타난 것을 볼 수 있는가? 섀도우 돔의 캡슐화를 더 직접적으로 체감하기 위해 스타일을 적용해보자. 먼저 스타일태그 코드를 참고 자료 (opens new window)에서 긁어오자.

const styles = document.createElement("style"); // 스타일 요소 생성

// 스타일 속성 정의
styles.textContent = `
a, span {
  vertical-align: top;
  display: inline-block;
  box-sizing: border-box;
}

a {
    height: 20px;
    padding: 1px 8px 1px 6px;
    background-color: #3EAF7C;
    color: #fff;
    border-radius: 3px;
    font-weight: 500;
    font-size: 11px;
    font-family:'Helvetica Neue', Arial, sans-serif;
    line-height: 18px;
    text-decoration: none;   
}

a:hover {  background-color: #45B884; }

span {
    position: relative;
    top: 2px;
    width: 14px;
    height: 14px;
    margin-right: 3px;
    background: transparent 0 0 no-repeat;
}
`;

스타일 정의 후 먼저 shadowElement 객체에 appendChild 메서드로 스타일을 등록해보자. style 내에 셀렉팅 된 요소 전체에 스타일이 적용될 것이다.

shadowElement.appendChild(styles); // 전체 적용되어버림

shadowElement는 Shadow-host로, 섀도우 호스트까지는 DOM으로 취급받기 때문에 글로벌 스코프에 존재하는 것으로 인식되며 새로 정의한 styles 객체도 결국 글로벌 스코프에 존재하기 때문에 페이지 전체에 해당 속성들이 적용되어버린다.

그러면 shadow 객체에 자식으로 붙여보면 어떻게될까?

shadow.appendChild(styles);

새로 만든 섀도우 돔 요소에만 스타일이 적용된 것을 볼 수 있을 것이다. 나머지 페이지에는 적용되지 않는 스타일이 돔 내에서만 적용된다. 섀도우 돔을 만들고 스타일까지 정의한 뒤에 해당 스타일에서 셀렉트하는 태그를 하나 요소로 생성하여 새롭게 섀도우 돔에 붙여보자.

const testAnchor = document.createElement("a");
testAnchor.innerText = "Testing Anchor...";

shadow.appendChild(testAnchor); // shadowElement가 아닌 shadow에 붙여야함.

생성한 testAnchor에 스타일이 적용되는 것을 확인할 수 있다.