shadowDOM

 

HTML, CSS, JS 전반적 특성 때문에 생기는 웹앨 빌드의 취약성을 제거한다.

id나 class가 중복되어도 충돌되는지에 대한 여부를 알려주지 않기때문에 버그가 많았다.

스타일이 쌓이고 쌓이다가.. 도저히 안될때면 !important를 통해 스타일을 해결하기도 했다.

 

shadowDOM은 CSS및 DOM을 수정한다.

vanilla 자바스크립트에서 CSS를 마크업과 번들로 묶고 세부정보를 숨겨 자체포함 구성요소로 작성할 수 있다.

 

구성요소

  • HTML Templates

  • Shadow DOM

  • Custom elements

  • HTML Imports

CSS범위, DOM 캡슐화 등이 shadowDOM으로 인해 재사용가능하기 때문에 

shadowDOM을 사용하면 웹 컴포넌트로 만들 필요가 없다.

 

  • 자체적으로 DOM이 포함되어있다.

  • document.querySelector 컴포넌트의 shadowDOM에서 노드를 반환하지 않음

  • CSS의 범위가 정해져있음

  • id, class 충돌에 대해 걱정하지 않아도됨

 

호스트에 shadow DOM을 붙이기 위해, attachShadow() 메서드를 사용한다.

주로 상호 작용하는 특정 요소들은 shadow host가 될 수 없기 때문에,

단순히 <a> 요소를 shadow host로 사용할 수 없다.

 

새로운 shadow tree를 만들기 위해 콘텐츠를 생성해야 한다.

shadow tree는 DOM tree와 비슷하지만 일반 DOM 대신 shadow DOM을 사용한다.

 

예시

-코드 출처 : https://wit.nts-corp.com/2019/03/27/5552

HTML

<div class="shadow-host"></div>

JS로 엘리먼트 삽입

// 호스트에 shadowDOM을 붙임
const shadowEl = document.querySelector(".shadow-host");
const shadow = shadowEl.attachShadow({mode: 'open'});
 
// 내부에 삽입할 엘리먼트 생성
const link = document.createElement("a");
link.href = shadowEl.querySelector("a").href;
link.innerHTML = `
    <span aria-label="Twitter icon"></span>
    ${shadowEl.querySelector("a").textContent}
`;
 
// shadowDOM에 삽입
shadow.appendChild(link);

JS로 CSS삽입

// shadowDOM에 추가할 스타일 태그 생성
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: #1b95e0;
    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: #0c7abf; }
span {
    position: relative;
    top: 2px;
    width: 14px;
    height: 14px;
    margin-right: 3px;
    background: transparent 0 0 no-repeat;
    background-image: url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2072%2072%22%3E%3Cpath%20fill%3D%22none%22%20d%3D%22M0%200h72v72H0z%22%2F%3E%3Cpath%20class%3D%22icon%22%20fill%3D%22%23fff%22%20d%3D%22M68.812%2015.14c-2.348%201.04-4.87%201.744-7.52%202.06%202.704-1.62%204.78-4.186%205.757-7.243-2.53%201.5-5.33%202.592-8.314%203.176C56.35%2010.59%2052.948%209%2049.182%209c-7.23%200-13.092%205.86-13.092%2013.093%200%201.026.118%202.02.338%202.98C25.543%2024.527%2015.9%2019.318%209.44%2011.396c-1.125%201.936-1.77%204.184-1.77%206.58%200%204.543%202.312%208.552%205.824%2010.9-2.146-.07-4.165-.658-5.93-1.64-.002.056-.002.11-.002.163%200%206.345%204.513%2011.638%2010.504%2012.84-1.1.298-2.256.457-3.45.457-.845%200-1.666-.078-2.464-.23%201.667%205.2%206.5%208.985%2012.23%209.09-4.482%203.51-10.13%205.605-16.26%205.605-1.055%200-2.096-.06-3.122-.184%205.794%203.717%2012.676%205.882%2020.067%205.882%2024.083%200%2037.25-19.95%2037.25-37.25%200-.565-.013-1.133-.038-1.693%202.558-1.847%204.778-4.15%206.532-6.774z%22%2F%3E%3C%2Fsvg%3E);
}다
`;
 
// shadowDOM에 삽입
shadow.appendChild(styles);

일반적인 방법과 동일하게 appendChild() 메서드를 사용하여 shadow DOM에 새로운 요소를 추가합니다.

shadowDOM을 하나의 페이지로 보지않고 각각의 컴포넌트 덩어리로 생각하면 편하다.

원본 글 캡쳐
원본 글 캡쳐
원본 글 캡쳐

shadow-root, html template를 확인할 수 있다.

 

원본 글 캡쳐


<slot> 태그 내부에 html template의 내용이 삽입된다.

만약 <slot> 비어있다면 슬롯이 대체 컨텐츠를 렌더링한다.

 

그외에 호스팅태그 접근, 상위태그접근 등 어떻게 사용하면되는지에 대해 자세하게 적혀있다.

 

역사관련 작성된 부분에서 말하듯 지난 2년동안 chrome 35+/opera가 한동안 shadowDOM을 제공했다고한다.

아티클 프로젝트를 하면서 내가 얼마나 최신 트렌드에 무뎠는지를 알게되는 경우가 많다.(최신도 아니지..)

 

shadowDOM v1은 chrome53, Opera 40, Safari 10 및 Firefox 63으로 제공됩니다. Edge 는 개발을 시작했다고하니.
IE 하위를 맞춰야하는것이 아니라면 <iframe> 대신에 써볼만 할 것같다.

iframe과는 다른 것으로 shadowDOM은 경량화된, 감춰진 DOM 트리로 보면된다.

보안을 원하면 iframe을 사용하도록...

 


원본 글

https://developers.google.com/web/fundamentals/web-components/shadowdom

그 외 참고

https://wit.nts-corp.com/2019/03/27/5552

https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow

 

 

강의 유튜브 주소 :

https://www.youtube.com/watch?v=V3QsSrldHqI&list=PLcqDmjxt30RtqbStQqk-eYMK8N-1SYIFn

 


 

일단 index.html 파일이 필요하다.

(webpack.config.js에서 entry에 적었던 파일들을 output으로 출력했을 때, 이 파일을 로드할 index.html!)

 

프로젝트 루트에 index.html을 생성한다.

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>예제</title>
</head>

<body>
  // react에서 DOM을 생성하여 넣어줄 태그! app.js에서 id값으로 접근하고 있다.
  <div id="root"></div> 
  
  // webpack.config.js에서 output에 경로를 작성한 파일 입력
  <script src="dist/app.js"></script> 
</body>
</html>

 

그리고 html에서 처음으로 로드할 app.js 파일이 필요하다.

이 파일은 entry.

 

프로젝트 루트에 app.js 생성한다.

// insatll한 리액트 모듈
const React = require('react');

// install한 리액트 돔 모듈
const ReactDOM = require('react-dom');

// 화면에 뿌려줄 컴포넌트
const Gugudan = require('./Example'); 

// ReactDOM의 render 메서드를 통해 <Example /> 컴포넌트를 #root에 생성
ReactDOM.render(<Example />, document.querySelector('#root'));

 

위 app.js에서 로드하고 있는 컴포넌트 <Example>를 생성해보자!

const React = require('react');

// 리액트의 component 메서드 선언를 비구조화할당으로 선언한다.
// 비구조화 할당으로 선언하기 위해서는 모듈이 export 시에 해당 메서드를 export하고 있어야한다.
// 이것은 ES6문법
const { Component } = React;

// Example 컴포넌트를 생성한다.
// 위에서 비구조화할당으로 Component 선언한 메서드를 사용한다.
class Example extends Component {

  // this를 사용하기 위해 construct를 사용했으나 하단에서 arrow function을 사용하면 없어도된다.
  constructor(props){ 
    super(props);

    // state 선언
    this.state = { 
      value : '',
      result : ''
    };
  }

  setExampleNumber = () => { 
  	return '123';
  };

	
  // state 선언시에는 무조건 this.setState를 사용해서 해줘야한다.
  onChange = (e) => { 
    this.setState({
    	value : e.target.value
    })
  };

  // inputEl 선언 후 Ref 설정해주면 객체에 접근가능
  inputEl; 

  // dom에 접근하기 위해서는 ref를 사용한다.
  this.inputEl.focus();

  // ref선언
  onRefInput = (o) => {  
  	this.inputEl = o;
  };

  render() {
    return (
	
	  // React.Fragment 로 감싸줄 수 있다. (불필요한 태그 생성 방지) 
      <React.Fragment>
        <div>
          {this.state.value}
        </div>

        <form onSubmit={this.onSubmit}>
          <input

            // ref 선언
            ref = {this.onRefInput} 
            type="number"

            // value와 onChange는 함께 쓰여야한다.
            // 하나만 사용하려면 value 속성 대신 defaultValue를 쓸수도 있다.
            value={this.state.value}
            onChange={this.onChange}
          />
          <button>입력</button>
        </form>
        <div>
        {this.state.result}
        </div>
      </React.Fragment> 
    );
  }
}

// 모듈 내보내기
module.exports = Example; 

 

<slot>에 대해 배워보겠습니다.

저는 강의를 들으면서 slot에 대해 처음알게되었습니다.

slots는 하위컴포넌트에게 HTML, Component를 전달 할 수 있다고합니다.

그리고 <slot>을 사용하는 이유는 props 정의안하고 parent에서 child로 데이터를 간단히 표현할 때 사용한다고 합니다.

 

컴포넌트를 사용할때 <Component/>로 사용하고는 했는데, 가끔 강의를 보면 <Component></Component>로 사용하고는 하는 사람들을 봤습니다.

<slot>은 여닫는 함수가 모두 존재할 때에만 사용할 수 있다고 합니다.


문법

child

<div>
    <slot></slot>
</div>

parent

<child>
    <p>여기에 HTML을 넣으면 slot 안에 표현된다</p>
</child>

<slot>을 vue파일에 추가합니다.

 

App.vue

App.vue 파일에서 <Editor></Editor> 태그 사이에 타이틀을 삽입해보겠습니다.

 

Editor.vue

<slot>을 사용해줍니다.

아래와 같이 확인이 됩니다.

정상적으로 된거 맞아? 하고 궁금해서 렌더링된 태그를 봤습니다.

Editor.vue 컴포넌트 영역에 포함된 것 같은 모습입니다.

 

<slot>에 data도 {{ }}로 삽입할 수 있습니다.

 

이렇게 하면 동적인 데이터도 넣을 수 있을 듯 싶습니다.

vue.js 지역 컴포넌트를 만들어보자.

 

-화면에 비춰지는 뷰의 단위를 쪼개어 재활용이 가능한 형태로 관리하기 위하여 Component로 제작한다.

-Vue 문법 특성 상 최상위 루트 태그가 1개여야만 렌더가 가능하다.

-Component는 지역(Local)과 전역(Global)로 나눌 수 있다.

출처:vue공식사이트

 

자 문법을 몰라도 컴포넌트 만들어 봅시다.

 

일단 new Vue ({   }); 가 뭘까요? https://kr.vuejs.org/v2/guide/instance.html

var vm = new Vue({

    // options .... (template, el, methods, created, watch.. 등등)

});

Vue 인스턴스 입니다.

인스턴스란?

설계도를 바탕으로 소프르웨어 세계에 구현된 구체적인 실체

-즉, 객체를 소프트웨어에 실체화하면 그것을 인스턴스라 함

-실체화된 인스턴스는 메모리에 할당됨.

-객체에 포함된다고 볼 수 있다.

 

실체 실체 객체 그러죠? 메모리에 할당되는 객체?로 알면될까요??

 

모든 Vue 앱은 이 인스턴스로 부터 시작됩니다^^ 가이드 한 번만 읽고 오세용 (솔직히 읽어도 잘 모르겠다)

<body>

  <div id="app"></div> // 1.

</body>

<script>

var cmp = { // 2.

    template : '<div>my component</div>';

};

 

new Vue({

    el : '#app', // 1.

    components : { // 3.

        'my-cmp' : cmp // 4.

    }

});

</script>

과정을 풀어보자면...

1. <div> id가 app이고, 나는 #app 영역에 vue의 모든 문법을 쓸거고, 컴포넌트도 만들거다.

그러니까 그 #app을 new Vue({ }) 안에 el로 선언해야합니다.

2. 그리고 컴포넌트를 넣어야 하니까 컴포넌트를 먼저 변수로 선언합니다.

template : 해서 선언해줍니다.

3. cmp를 #app에서 사용하기 위해 vue에 등록해줄겁니다. 그러기 위해서 영역을 만듭니다.

4. cmp변수를 html에 넣기위해 components : {   } 라고 작성했던 내부에 넣어줍니다.

여기서 'my-cmp' 는 html에 <my-cmp>로 사용이 되는 이름이고, cmp는 template를 만들어줬던 변수이름입니다!

 

결과는  

예제내용
html결과


위의 예제로 봤을 때 ..

컴포넌트를 넣을 영역은 el로 선언

html에 내용을 넣을 것을 js 변수로 선언(cmp)

js변수를 html에서 컴포넌트로 쓰려면 components에 선언

 

저기서 my-cmp는 #app 내부면 얼마든지 계속 쓸 수 있어요~

 

+ Recent posts