webComponent

woolee의 기록보관소·2022년 10월 31일
0

FE 기능구현 연습

목록 보기
7/33

shadow DOM

shadow DOM API는 웹 컴포넌트의 중요한 측면인 캡슐화의 핵심 파트이며 숨겨진 DOM을 요소에 부착하는 방법을 제공한다.

각 html 요소들에는 이런 shadow DOM이 부착되어 있다.
개발자 도구에서 F1 혹은 settings를 눌러서 (즉, 개발자도구에서 톱니바퀴 클릭하면 나오는 화면)

Elements > Show user agent shadow DOM을 체크해주면 각각의 html 요소에 숨겨진 요소들을 확인할 수 있다.

elem.attachShadow() 메서드를 사용하면 어떤 요소든 shadow root를 부착할 수 있다. 이 메서드는 매개변수로 옵션 객체를 갖는데, 그 옵션은 mode이며 open 혹은 closed 값을 갖는다.


예제 (class, constructor, super, attachShadow, connectedCallback)

class로 웹 컴포넌트 만들기

class란? constructor, prototype을 사용해 상속 기능을 만들 수 있는 문법이다.
constructor는 오브젝트를 안전하게 뽑아주는 기계.

extends는 다른 class를 상속할 수 있게 도와주는 문법. 예를 들어 class A extends B {} 면, B의 기능을 A로 상속할 수 있게 해주는 것임.

아래 코드에서 보면, 내가 만들고 싶은 html 요소를 만들기 위해 extends를 사용해 HTMLElement로부터 기능들을 상속 받으면 된다. (class <내가 만들고 싶은 웹 컴포넌트> extends HTMLElement {})

extends로 상속 기능을 받을 때는 super()를 쓰는데, super()는 'extends로 상속받은 부모 class의 constructor()를 의미한다'

즉, 내가 만들고 싶은 웹 컴포넌트에 constructor()를 추가하려면 super() 다음에 this.~로 코드를 이어 나가면 된다.

웹 컴포넌트 html을 클래스 형태로 만들고 define에 작성하면 된다.

  class 클래스 extends HTMLElement {
   connectedCallback() {
      this.innerHTML = 
...줄이고 싶은 html 내용들 ~~ 
   }
}
customElements.define("태그 작명", 클래스);

attachShadow() 메서드로 shadowRoot라는 공간을 만들어 준 뒤, 여기에 원하는 html을 숨겨 넣는 것이다.

class MyCounter extends HTMLElement {
  constructor() {
    super();
    this.shadow = this.attachShadow( { mode: 'open' } );
  }
  
  connectedCallback() {
    this.render();
  }
  
  render() {
    this.shadow.innerHTML = `
		<h1>Counter</h1>
		${this.count}
		<button id='btn'>Increment</button>
	`;
  }
}

customElements.define('my-counter', MyCounter);

예제 (get,set, static, observedAttributes, attributeChangedCallback)

set이 붙은 함수들을 setter라고 부른다. 데이터를 수정하는 함수들. (파라미터 1개 이상 존재해야)
get이 붙은 함수들을 getter라고 부른다. 데이터를 가져오는 함수들. (get은 파라미터가 있으면 안 되고, 함수 내에 return 존재해야. )

getter, setter => 새로 뽑힌 오브젝트 내용들을 쉽게 수정 관리. 오브젝트 내의 함수들을 괄호 없이 쓰게 해주는 건데, 직접 원본 데이터를 만지는 게 아니라 간접적으로 다룸으로써 데이터의 무결성을 보존.

static은 클래스 함수 자체에 메서드를 설정할 수 있는 방법임. 메서드를 프로퍼티 형태로 직접 할당하게 해준다.

class MyCustomElemet extends HTMLElemnt {
  constructor() {
    super();
  }
  // 커스텀 요소가 생성될 때마다 실행
  connectedCallback() {
    this.render();
  }
  // 해당 요소가 새로운 문서로 이동될 때마다  호출
  adoptCallback() {
  }
  // 요소의 속성 변동 사항(추가,제거,업데이트,교체)을 관찰하고 호출함
  // observedAttributes() 속성에 나열된 특성에서만 호출
  attributeChangedCallback(attrName, oldVal, newVal) {
    this.render();
  }
  // attributeChangedCallback 에서 관찰하는 항목을 반환.
  static get observedAttributes() {
    return ['title'];
  }
  get title() {
    return this.getAttribute('title');
  }
  // 커스텀 요소가 제거될 때 호출 
  disconnectedCallback() {
    alert('bye bye');
  }
  // 커스텀 메서드 
  render() {
    this.innerHTML = `
    <h1>${this.title}</h1>
   `
  }
}
customElements.define('my-counter', MyCustomElemet);

버튼 누르면 count 증가 예제

getter, setter도 결국 함수야. 근데, 이걸 원본을 다루지 않고 간접적으로 다룸으로써 데이터 무결성 지키는 방법이야.
그냥 함수로 작성해도 되고 편하게 쓰려면 get, set을 써도 됨. 꼭 get, set 써야 하는 건 아니라는 의미.

아래 예제 코드에서 보면,
get count() { return this.getAttribute('count'); }를 작성해야 html에서 count 속성을 다룰 수 있어. index.html에서 count='20'을 넣어도 위 get 함수가 없으면 undefined가 뜸.

또한, set count(newValue) { this.setAttribute('count', newValue); } 를 작성해줘야 setAttribute를 쓸 수 있는 듯. 예를 들어 index.html 코드에서 아래처럼 setAttribute() 쓸 수 있게 되는 것.

<button onclick="update();">Update to 100</button>

  <script>
    function update() {
      let counter = document.querySelector('#counter');
      counter.setAttribute('count', 100);
    }
  </script>

(counter.js)

class MyCounter extends HTMLElement {
  constructor() {
    super();
    // shadow DOM 열어주고
    this.shadow = this.attachShadow( { mode : "open" } );
  }
  // getter, setter
  get count() {
    return this.getAttribute('count');
  }

  set count(newValue) {
    this.setAttribute('count', newValue);
  }

  // attributeChangedCallback()에서 관찰하는 항목 반환
  static get observedAttributes() {
    return ['count']; // 여기에 속성값(문자열)들을 배열로 [a, b, c, ... ] 이렇게 추가하면 되는듯
  }

  // 변동사항 추적 
  attributeChangedCallback(prop, oldVal, newVal) {
    if (prop === 'count') {
      this.render();
      let btn = this.shadow.querySelector('#btn');
      btn.addEventListener('click', this.inc.bind(this));
    }
  }

  inc() {
    this.count++;
  }
  // 커스텀 요소 생성될 때마다 실행
  connectedCallback() {
    this.render();
    let btn = this.shadow.querySelector('#btn');
    btn.addEventListener('click', this.inc.bind(this));
  }

  render() {
    this.shadow.innerHTML = `
    <h1>Counter</h1>
    ${this.count}
    <button id='btn'>Increment</button>
    `;
  }
}

customElements.define('my-counter', MyCounter);

(index.html)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Counter</title>
</head>
<body>
  <my-counter id="counter" count="20"></my-counter>
  <script src="./counter.js"></script>
  <br><br>
  <button onclick="update();">Update to 100</button>

  <script>
    function update() {
      let counter = document.querySelector('#counter');
      counter.setAttribute('count', 100);
    }
  </script>
</body>
</html>

참고

How to create a Web Component using Vanilla JS
shadow DOM 사용하기
사용자 정의 요소 사용하기
정적 메서드와 정적 프로퍼티
https://alexband.tistory.com/53

profile
https://medium.com/@wooleejaan

0개의 댓글