[HTML/CSS] 쉐도우 돔(Shadow DOM)으로 스타일 충돌 해결하기
코드로 알아보는 CSS 충돌
다음과 같이 header 컴포넌트(header.vue)와 header 컴포넌트를 사용하는 content(index.vue)영역이 있다.
index.vue
<template>
<div id="app">
<Header />
<div class="content">
<h3>content의 tittle</h3>
</div>
</div>
</template>
...
<style>
@import "../assets/index.css";
</style>
index.css
body {
width: 100%;
height: 100%;
}
header.vue
<template>
<header id="header">
<h3>header tittle</h3>
</header>
</template>
<style>
@import "./assets/header.css";
</style>
header에 글자 색상을 회색으로 바꾸고 싶어서 아래와 같이 header.css를 코드를 수정하면 어떻게 될까?
header.css
h3 {
margin: 0 10px;
color: gray;
}
#header {
margin: 0;
width: 1000px;
height: 50px;
background-color: black;
display: flex;
flex-direction: colunm;
align-items: center;
}
아래와 같이 의도와는 다른 결과를 불러오게 된다.
의도 : header의 text만 회색으로 변경
결과 : header와 content의 text 모두 색상이 회색으로 변경
사용하는 코드에서는 각각의 컴포넌트에 style을 별개로 지정해주는 것 같이 보이지만, 추가한 style은 범위가 제한되지 않는다. (style 태그는 어디에 선언되든 글로벌 영역이기 때문이다.)
기본적인 DOM의 구조
DOM(Document Object Model)은 웹 페이지의 구조와 콘텐츠를 나타내는 계층구조로, HTML의 문서화된 표현이다.
각 요소는 노드(Node)로 표현되며, 각 노드들은 부모-자식 관계를 가지며 트리 형태로 구성된다.
그리고 HTML 문서의 모든 element와 style로 이루어진 DOM은 하나의 큰 글로벌 범위 내에 있다.
css 충돌을 해결하는 다양한 방법
selector로 범위 제한하기
만약, header를 모듈로 제공한다면, content 영역과 완전히 무관한 디펜던시를 보장해야할 것이다.
- header의 style이 content 영역에 영향을 줘서도 안되고,
- 반대로 content 영역의 css가 header의 style에 영향을 줘서도 안된다.
간단한 해결방법은 상위 클래스의 selector로 범위를 제한하는 것이다. (ex. #header h3 { ... })
하지만, 이 방법도 다음과 같은 문제가 있다.
/** 1. 복잡해지는 style 구조 **/
#header .tittle span {
...
}
/** 2. 스타일 리셋 필요 **/
/** index.css **/
h3 {
margin: 10px;
padding: 10px;
}
/** header.css **/
#header h3 {
margin: 0;
padding: 0;
}
1. 범위를 제한하기 위해 css 구조가 복잡해진다.
- #header .title span { }
2. 범위를 제한하더라도, 공통으로 적용되는 style을 무시할 수 없다.
- h3 { margin:10px; ... }
- 따라서 이 경우 공통으로 적용된 style을 리셋시키는 코드가 필요할 것이다.
기본 태그에 주어진 속성을 하나하나 리셋시켜줘야한다니..
scoped css(styled component)
HTML5부터는 <style> 요소에 scoped 속성을 사용할 수 있다.
Vue에서는 vue-loader가 제공하는 scoped css 라는 것을 사용해서 특정 컴포넌트에만 해당 스타일을 적용하도록 할 수 있다.
사용 방법도 매우 간단하다. component 내에 선언한 style에 scoped 속성을 추가하면 된다.
scoped style은 다음과 같이 data-v-[hash]와 같은 scoped 속성이 추가된다.
다만, 이 방법도 문제점이 있다.
- v-html 디렉티브로 추가된 element는 scoped 속성이 없다.
- 하위 컴포넌트에 style이 적용되지 못한다.
두 번째 문제의 경우는 Deep Selector(v-deep, '>>>')로 해결이 가능하지만, 사이드 이펙트를 발생시킬 수 있기 때문에 예외적인 케이스에만 사용하는 것이 좋다.
<style scoped>
.a::v-deep .b { /* ... */ }
</style>
iframe
iframe를 이용해서 DOM을 분리하는 경우 아래와 같은 단점이 있다.
- http 요청이 한차례 더 일어난다
- 별도의 페이지이기 때문에, 소비되는 리소스도 높고 느리다
- iframe의 주소가 같은 도메인이 아닌 경우 접근 불가능하다
2016년도에 트위터는 위와 같은 이유로, iframe으로 지원하던 기능을 Shadow DOM 방식으로 전환했다. (지원하지 않는 브라우저는 기존과 동일하게 iframe으로 지원)
Shadow DOM 도입으로 브라우저 메모리 사용률이 훨씬 낮아지고, 렌더링 시간이 빨라졌다고 한다.
쉐도우 돔(Shadow DOM)이란?
Shadow DOM은 기존의 DOM 트리에서 완전히 분리된 고유의 element와 style을 가진 DOM 트리이다.
즉, Shadow DOM은 글로벌 범위에 포함되지 않는다.
Shadow DOM 트리는 Shadow root로부터 시작되어 원하는 모든 요소의 안에 부착될 수 있으며, 방법은 일반 DOM과 동일하다.
Shadow DOM의 장점
Scoped DOM
예를들어 document.querySelector()구성 요소의 Shadow DOM에 있는 노드를 반환하지 않는다.
CSS 단순화
Scoped DOM은 외부와 디펜던시가 없기 때문에, 일반적인 id/class 이름 사용이 가능하다.
생산성
하나의 큰 글로벌 페이지가 아니라 청크된 DOM을 가진다.
Scoped CSS
Shadow DOM 내부에 정의된 CSS는 해당 범위로 지정된다.
스타일 규칙이 외부로 전파되지 않고 외부 스타일의 영향을 받지 않는다.
Shadow DOM 관련 용어
Shadow host : Shadow DOM이 부착되는 통상적인 DOM 노드
Shadow tree : Shadow DOM 내부의 DOM 트리
Shadow boundary : Shadow DOM이 끝나고, 통상적인 DOM이 시작되는 장소
Shadow root : Shadow 트리의 root 노드
낯설지 않은 Shadow DOM
Shadow DOM이 안보일 때
개발자 도구 설정에서 'Show user agent shadow DOM'을 체크한다.
Shadow DOM 사용해보기
'element.attachShadow({mode: 'open'})'를 이용해 특정 element에 Shadow DOM을 추가할 수 있다.
mode 옵션의 값으로는 'open'|'close'를 가질 수 있으며 각각은 아래와 같은 의미를 가진다.
'open'
ShadowRoot에 포함된 요소에 대해 외부 스크립트에서 접근할 수 있도록 허용한다.
이 모드를 사용하면 외부에서 DOM 트리의 요소에 대한 참조를 가져와서 조작할 수 있다.
'closed'
ShadowRoot에 포함된 요소에 대해 외부 스크립트에서 접근할 수 없다.
이 모드를 사용하면 외부 스크립트에서는 DOM 트리에 포함된 요소에 대한 참조를 가져올 수 없다.
'closed'에서 attachShadow로 element로 접근을 시도할 때는 아래와 같은 오류가 발생한다.
기본값은 closed로, open 모드에서는 :host 선택자를 사용하여 외부에서 스타일을 적용할 수 있다.
const header = document.createElement('header');
const shadowRoot = header.attachShadow({mode: 'open'});
shadowRoot.innerHTML = '<h1>Hello Shadow DOM</h1>'; // Could also use appendChild().
// header.shadowRoot === shadowRoot
// shadowRoot.host === header
.innerHTML 이외에 textContent 와 같은 다른 DOM API로도 element를 설정할 수 있다.
shadowRoot 하위로 <style> 태그를 추가하면, 해당 style은 shadowRoot 하위로만 범위가 제한되는 scoped Style로 적용된다.
아래는 예시코드로, #header의 h3을 Shadow DOM으로 생성해서 style을 적용했다.
#header의 h3에 적용된 스타일은 content 영역의 h3에 영향을 주지도, 받지도 않는다.
- header 영역의 h3(외부 영향 X) : {margin: 0 10px; color: gray;}
- content 영역의 h3 : {background-color: #bbb1a5; padding: 5px;}
Shadow DOM을 적용할 수 없는 Element
<textarea>, <input>
브라우저 자체에서 내부 Shadow DOM을 호스팅한다.
<img>
외부 이미지 리소스를 가져와 렌더링하는 태그로, 특별한 컨텐츠를 감싸거나 다른 요소의 자식 요소로 포함되지 않기 때문이다.
지원 브라우저
역시나 IE, OperaMini에서는 지원을 하지 않는다.
Polyfill : webcomponentsjs
웹 구성 요소 사양을 지원하는 폴리필 모음 :
- Custom Elements v1 : 작성자가 자신의 사용자 지정 태그( spec , tutorial , polyfill )를 정의할 수 있다.
- Shadow DOM v1 : Shadow 루트( spec , tutorial , shadydom polyfill , shadycss polyfill ) 아래에 DOM 하위 트리를 숨겨서 캡슐화를 제공한다 .
이를 필요로 하는 브라우저의 경우 몇 가지 사소한 폴리필도 포함되어 있다.
- HTMLTemplateElement
- Promise
- Event, CustomEvent, MouseEvent생성자 및 Object.assign, Array.from ( webcomponents-platform 참조 )
- URL constructor
npm install @webcomponents/webcomponentsjs
import '@webcomponents/webcomponentsjs';