ย
Using Custom Elements
High-level View
ย
Web Components๋ฅผ ์์ฑํ๊ธฐ ์ํด์๋
CustomElementRegistry
interface๋ฅผ ๊ตฌํํ window.customElements
์ define()
๋ฉ์๋๋ฅผ ์ด์ฉํ๋ฉด ๋๋ค.ย
์ด ๋ฉ์๋๋ ๋ค์ ์ธ ๊ฐ์ arguments๋ฅผ ๋ฐ์ custom element๋ฅผ registryํ๊ฒ ๋๋๋ฐ, ๊ฐ๊ฐ ๋ค์๊ณผ ๊ฐ๋ค.
ย
DOMString
: custom element์ ์ด๋ฆ (kebab-case)
class extends HTMLElement
: element์ ํ๋์ ์ ์ํ class
{ extends }
: inheritsํ Node name (optional)
ย
๊ฐ๋ น ๋ค์๊ณผ ๊ฐ์ด ์ฌ์ฉ์ด ๊ฐ๋ฅํ๋ค.
ย
class WordCount extends HTMLParagraphElement { constructor() { super(); // always call // element functionality } } customElements.define('word-count', WordCount, { extends: 'p' });
ย
๋ฟ๋ง ์๋๋ผ
connectedCallback
, attributeChangedCallback
๋ฑ lifecycle callbacks๋ ์กด์ฌํ๋ฉฐ, ์ด๋ ์๋์์ ๋ค๋ฃจ๋๋ก ํ๊ฒ ๋ค.ย
์ํผ ์์ ๊ฐ์ด registration ํ์ผ๋ฉด ๋ค์๊ณผ ๊ฐ์ด ์ฌ์ฉ์ด ๊ฐ๋ฅํ๋ค.
ย
<word-count></word-count> <!-- or --> <p is="word-count"></p>
ย
๋ฌผ๋ก js๋ก๋ programmaticํ๊ฒ ์ฌ์ฉํ ์๋ ์๋ค.
ย
document.createElement('word-count'); // or document.createElement('p', { is: 'word-count' });
ย
๋ฌด์์ ์ฌ์ฉํ๋ ์์ .
ย
Working through some simple examples
ย
๊ฐ๋จํ ์์ ๋ฅผ ๋ณด๋ฉฐ ์ข ๋ ์ดํดํด๋ณด๋๋ก ํ์.
ย
Autonomous custom elements
ย
๋ค์์ spec์ ๊ฐ์ง element๋ฅผ ํ๋ registration ํด ๋ณด๋๋ก ํ๊ฒ ๋ค.
ย
- image icon๊ณผ text string์ผ๋ก ๊ตฌ์ฑ๋จ
- text string์ hide, icon์ display
- icon focused ์ text๊ฐ popup์ผ๋ก ๋์ด
ย
์ผ๋จ ์ด๋ฅผ ์ํด์๋ ๋ค์๊ณผ ๊ฐ์ class๋ฅผ ํ๋ ์ ์ํด์ค๋ค. ์ด ๋
HTMLElement
๋ฅผ extendsํ๋๊ฒ์ ์ ์.ย
class PopupInfo extends HTMLElement { constructor() { super(); // always // functionality } }
ย
super()
๋ ํญ์ constructor()
์ฒซ ๋ฒ์งธ์ ์์นํด์ผ ํจ์ ์์ง ๋ง์.super()
์๋์๋ ์ด๋ค ํ๋์ ํ ๊ฒ์ธ์ง ๋ช
์ํ ์ ์์ผ๋ฉฐ, ์ฌ๊ธฐ์๋ ๋ค์์ ์ฝ๋๋ฅผ ์์ฑํ๋๋ก ํ๊ฒ ๋ค.ย
class PopupInfo extends HTMLElement { constructor() { super(); // create a shadow root this.attachShadow({ mode: 'open' }); // create nested elements const wrapper = document.createElement('span'); wrapper.classList.add('wrapper'); const icon = wrapper.appendChild(document.createElement('span')); icon.classList.add('icon'); icon.setAttribute('tabindex', 0); const img = icon.appendChild(document.createElement('img')); img.src = this.hasAttribute('img') ? this.getAttribute('img') : '/img/default.png'; const info = wrapper.appendChild(document.createElement('span')); info.classList.add('info'); info.textContent = this.getAttribute('data-text'); const style = document.createElement('style'); style.textContent = /* css strings */; // attach ccreated elements to the Shadow DOM this.shadowRoot.append(style, wrapper); } } customElements.define('popup-info', PopupInfo);
ย
Shadow Root๋ ๋ฌด์์ธ๊ฐ์? ๊ฑฑ์ ํ์ง ๋ง์. ์ง๊ธ์ ์ ํ์ ์๊ณ ์ด ๋ด์ฉ๋ ์๋์์ ๋ค๋ฃฌ๋ค. ์ง๊ธ์ ๊ทธ๋ฅ ๋ ํ๋์ independentํ DOM์ ์์ฑํ๋ค๊ณ ๋ฐ์๋ค์ด๋ฉด ๋๋ค.
ย
์๋ฌดํผ ์ด๋ ๊ฒ registration์ ํด ์ฃผ๋ฉด ์์ ๋งํ๋ฏ์ด ๋ค์๊ณผ ๊ฐ์ด ์ฌ์ฉ์ด ๊ฐ๋ฅํ๋ค.
ย
<popup-info img="/img/alt.png" data-text="text contents"></popup-info>
ย
ย
์์์
this.hasAttribute()
๊ทธ๋ฆฌ๊ณ this.getAttribute()
๋ฅผ ํตํด DOM attribute๋ฅผ ์ฐธ์กฐํ๊ฒ ๋ณด์ด๋๊ฐ? ์ด๋ค์ด ๋ฐ๋ก custom element์ ์ ์๋ attribute๋ฅผ ๊ฐ๋ฆฌํค๋ ๊ฒ์ด๋ค.ย
Internal vs External styles
ย
style์ ์์ ๊ฐ์ด ์ง์ css string์ ์ง์ด๋ฃ์ด๋ ๋๋, ๋ค์๊ณผ ๊ฐ์ด
<link>
ํ๊ทธ๋ฅผ ์ด์ฉํ ์๋ ์์ ๊ฒ์ด๋ค.ย
const linkElement = document.createElement('link'); linkElement.setAttribute('rel', 'stylesheet'); linkElement.setAttribute('href', 'style.css'); // attach to the Shadow DOM this.shadowRoot.appendChild(linkElement);
ย
์ด ํธ์ด ์ข ๋ ์ฝ๊ธฐ๋ ํ๋ฐ, ๋ญ ์ด๊ฒ๋ ์๋์์ ๋ ์ฌ์ด ๋ฐฉ๋ฒ์ ๋ณผ ๊ฒ์ด๋ค.
ย
Customized built-in elements
ย
๋ค๋ฅธ ์๋ฅผ ํ๋ ๋ ๋ณด์.
expending-list
๋ผ๋ ์ด๋ฆ์ ๊ฐ์ง custom element์ด๋ค.ย
class ExpandingList extends HTMLUListElement { constructor() { super(); // nothing } } customElements.define('expanding-list', ExpandingList, { extends: 'ul' });
ย
์ด๋ฒ์๋ ์ด๋ป๊ฒ ๊ตฌ์ฑ๋ ์ง ์ ์ํ์ง ์๊ณ ๊ทธ๋ฅ registration์ ํ๋ค. ๋ค๋ง
ul
node๋ฅผ extends ํ์ ๋ฟ์ด๊ณ . ๊ทธ๋ผ ๊ฒฐ๊ณผ๋ ์ด๋จ๊น?ย
<ul is="expanding-list"> <!-- ... --> </ul>
ย
ย
๊ฒฐ๊ณผ๋ ์ผ๋ฐ์ ์ธ
<ul>
์ ์ฌ์ฉํ ๋์ ๋์ผํ๋ค. ๋ฌผ๋ก is
attribute๋ฅผ ์ฌ์ฉํ์ง ์๊ณ ๋ฐ๋ก <expanding-list>
๋ฅผ ์ฌ์ฉํด๋ ๋๋ค.ย
Using the Lifecycle callbacks
ย
์์ ๋งํ๋ฏ์ด ๊ฐ lifecycle๋ง๋ค ํธ์ถ ๊ฐ๋ฅํ callback์ด ์กด์ฌํ๋ค.
ย
connectedCallback
: custom element๊ฐ DOM์ connected๋ ๋ ํธ์ถ- ๋ชจ๋ DOM์ด parsing๋๊ธฐ ์ ์๋ ํธ์ถ๋ ์ ์์
- node moved ์์๋ ํธ์ถ๋จ
- disconnected ์์๋ ํธ์ถ๋ ์ ์์ผ๋ฉฐ, ์ด๋
isConnected
property๋ฅผ ์ด์ฉํด ํ๋ณ์ด ๊ฐ๋ฅ
disconnectedCallback
: custom element๊ฐ DOM์์ disconn ๋ ๋ ํธ์ถ
adoptedCallback
: node๊ฐ moved ๋์์ ๋ ํธ์ถ
attributeChangedCallback
: custom element์ attributes๊ฐ added, removed, changed ๋ ๋ ํธ์ถ- ๋จ,
static get observedAttributes()
๋ฉ์๋๋ก observeํ attributes๋ฅผ ์๋ ค์ค์ผ ํจ
ย
์ค์ ์๋ฅผ ๋ณด๋ฉฐ ์ดํดํด๋ณด๋๋ก ํ์.
ย
<custom-square l="100" c="red"></custom-square>
ย
class CustomSquare extends HTMLElement { constructor() { super(); this.shadow = this.attachShadow({ mode: 'open' }); const div = document.createElement('div'); const style = document.createElement('style'); this.shadow.appendChild(style); this.shadow.appendChild(div); } updateStyle() { this.querySelector('style').textContent = /* css string */; } /** * lifecycle 'connected' * */ connectedCallback() { console.log('Custom square element added to page'); this.updateStyle(); } /** * lifecycle 'disconnected' * */ disconnectedCallback() { console.log('Custom square element removed from page'); } /** * lifecycle 'adopted' * */ adoptedCallback() { console.log('Custom square element moved to new page'); } /** * lifecycle 'attribute changed' * */ attributeChangedCallback(name, oldValue, newValue) { console.log('Custom square element attributes changed'); updateStyle(); } static get observedAttributes() { // should return an array containing names of attributes return ['c', 'l']; // watch 'c' and 'l' attributes } }
ย
ย
๋์ถฉ ๋๋์ด ์ค์ง ์๋๊ฐ?
์ฐธ๊ณ ๋ก, ์ธ๊ธํ๋ฏ์ด
attributeChangedCallback()
์ observedAttributes()
๋ก ์ด๋ค attribute๋ฅผ watching ํ ๊ฒ์ธ์ง ์ง์ ํด์ค์ผ๋ง ํ๋ค. ๋ฐ๋์.ย
Using Shadow DOM
์น ์ปดํฌ๋ํธ์์ ๊ฐ์ฅ ์ค์ํ ๊ฐ๋
์ encapsulation ์ด๋ค.
ย
๋ณต์กํ๊ฒ ๊ตฌํ๋ Component ๋ด๋ถ๋ฅผ ์ด๋ป๊ฒ ํด์ผ ์ ์ ์งํ ์ ์์ ๊ฒ์ด๋ฉฐ, ๋ ๋ค๋ฅธ Component์๋ ์ด๋ป๊ฒ ์ถฉ๋์ ํผํ ์ ์์๊น? ์ด๋ป๊ฒ Scope๋ฅผ ๊ด๋ฆฌํด์ผ๋ง ํ ๊น? ์ด๋ป๊ฒ ํด์ผ ๊น๋ํ๊ณ ๊ฐ๊ฒฐํ ์ฝ๋๋ฅผ ์ ์งํ ์ ์์๊น?
ย
Shadow DOM API๋ ์ด๋ฌํ ๊ฒ์ ์ด์ ์ ๋ง์ถ์์ผ๋ฉฐ, ์ฌ๊ธฐ์๋ ์ด๋ฅผ ๋ฐํ์ผ๋ก ์ด๋ป๊ฒ ํด์ผ ๊ฐ๊ฐ์ DOM์ ์๋ก ์ถฉ๋ ์์ด ๋ถ๋ฆฌ๊ฐ ๊ฐ๋ฅํ์ง ๊ทธ ๋ฐฉ๋ฒ์ ๋ณด๋๋ก ํ๊ฒ ๋ค.
(๋ฌผ๋ก ์์ DOM์ ๋ํด ์๊ณ ์์ด์ผ๋ง ํจ)
ย
High-level view
ย
๋ค์๊ณผ ๊ฐ์ HTML ์ฝ๋๊ฐ ์๋ค๊ณ ํ ๋
ย
<!DOCTYPE html> <html> <head> <meta charste="utf-8"> <title>Simple DOM</title> </head> <body> <section> <img src="dinosaur.png" alt="T-Rex"> <p>Here we will add a link to the <a href="https://www.mozilla.org/">Mozilla</a></p> </section> </body> </html>
ย
์ด๋ฅผ DOM์์๋ ๋ค์๊ณผ ๊ฐ์ tree ํํ๋ก ๋ํ๋ธ๋ค.
ย
ย
Shadow DOM ์ญ์ ์ด์ ํฌ๊ฒ ๋ค๋ฅผ ๊ฒ์ด ์์ผ๋ฉฐ, ๋ค์ ๊ทธ๋ฆผ์ ํตํด ์ดํดํด๋ณด์.
ย
ย
๊ทธ๋ฆผ์ ๋์ค๋ ๋ค ๊ฐ์ง ๋จ์ด์ ์๋ฏธ๋ ๋ค์๊ณผ ๊ฐ๋ค.
ย
Shadow host
: ์ผ๋ฐ์ ์ธ DOM Node์ฒ๋ผ ๋ณด์ด๋, Shadow DOM ์ฐ๊ฒฐ ์ง์
Shadow tree
: Shadow DOM ๋ด๋ถ์ DOM Tree
Shadow boundary
: Shadow DOM์ ์์ node๋ถํฐ Shadow DOM์ ๋ node๊น์ง์ ๊ณต๊ฐ
Shadow root
: Shadow tree์ root node
ย
ํฌ๊ฒ ์ด๋ ต์ง ์๋ค.
์ผ๋ฐ์ ์ธ DOM Structure์ ๋น๊ตํด๋ ํฌ๊ฒ ๋ค๋ฅผ ๊ฒ ์์ผ๋ฉฐ, ์ฌ์ฉ๋ฒ ๋ํ ๋์ผ. ๊ทธ์ Shadow DOM ๋ด๋ถ์ ๋ชจ๋ ๊ฒ๋ค์ Shadow Boundary ์ธ๋ถ์ ์ํฅ์ ๋ผ์น ์ ์๋ค๋ ๊ฒ์ ์ ์ธํ๊ณ ๋ ๋ง์ด๋ค.
ย
Basic usage
ย
์์
Element.attachShadow()
๋ฉ์๋๋ฅผ ํตํด Shadow DOM์ ๊ตฌ์ฑํด๋ดค๋๋ฐ, ์ฌ๊ธฐ์ ๋ ๊ฐ์ง mode
๋ฅผ ์ง์ ํด์ค ์ ์๋ค.ย
const shadow = element.attachShadow({ mode: 'open' }); const shadow = element.attachShadow({ mode: 'closed' });
ย
open
:Element.shadowRoot
property๋ฅผ ์ด์ฉํด Shadow DOM์ ๋ํ ์ฐธ์กฐ๋ฅผ ์ป์ ์ ์์
closed
: ์ฐธ์กฐ๊ฐ ๋ถ๊ฐ๋ฅํ๊ฒ๋ Shadow DOM์ ๊ตฌ์ฑ
ย
๋ฌด์จ ๋ง์ด๋?
ย
const myShadowDOM = myCustomElement.shadowRoot; // when mode is 'open'
ย
์ฌ๊ธฐ์
closed
mode์ธ ๊ฒฝ์ฐ์๋ Element.shadowRoot
์ null
์ด ๋ค์ด๊ฐ๋ฉฐ, ๋น์ฐํ ์ด ๊ฒฝ์ฐ๋ ์ด๋ ํ ๋ฐฉ๋ฒ์ ํตํด์๋ Shadow DOM์ ์ฐธ์กฐํ ์ ์๋ค๋ ๋ง์ด๋ค.ย
์ด ๋๋ฌธ์ ์์ constructor ๋ด๋ถ์์๋ ์ฐธ์กฐ๋ฅผ ์ํด
{ mode: 'open' }
์ผ๋ก ์ค์ ํ ๊ฒ.๋ฐ๋ก ์์ ๋ ํด๋ณด์ง ์๊ฒ ๋ค. ์์์ ์งํํ ๊ฒ๊ณผ ๋์ผํ๊ธฐ ๋๋ฌธ.
ย
Using Templates and Slots
ย
์ฌ๊ธฐ์๋
<template>
๊ณผ <slot>
ํ๊ทธ์ ๋ํด ๋ณด๋๋ก ํ๊ฒ ๋ค.ย
The truth about templates
ย
์ฌ์ฌ์ฉ ๊ฐ๋ฅํ components๋ฅผ ์์ฑํ๊ธฐ ์ํด์๋ ์ฌ๋ฌ ๋ฐฉ๋ฒ์ด ์๊ฒ ์ง๋ง,
<template>
ํ๊ทธ๋ฅผ ์ด์ฉํ๋ ๊ฒ์ด ๊ฐ์ฅ ์ฝ๊ณ ํธ๋ฆฌํ ๋ฐฉ๋ฒ์ด๋ค.ย
<template>
๋ด์ Nodes๋ DOM์ ๋ ๋๋ง๋์ง๋ ์์ผ๋, Programmaticํ ๋ฐฉ๋ฒ์ผ๋ก ์ฐธ์กฐ๊ฐ ๊ฐ๋ฅํ๊ธฐ ๋๋ฌธ.ย
<template id="my-paragraph"> <p>My Paragraph</p> <style> p { font-size: 30px; } </style> </template>
ย
๊ฐ๋ น ์์ ๊ฐ์ template์ด ์๋ค๋ฉด...
ย
const template = document.getElementById('my-paragraph'); const templateContent = template.content; document.body.appendChild(templateContent);
ย
์ด๋ ๊ฒ DOM์ ์ถ๊ฐํ ์ ์๊ฒ ๋๋ค.
๋ค๋ง ์ด๋ฌํ ๊ฒฝ์ฐ ์์์ ์ง์ ํ
style
์ ์ ์ญ์ ์ผ๋ก ์ ์ธ๋๋ค๋ ๊ฒ์ ์ ์.ย
Using templates with Web Components
ย
์ด๋ฅผ web component์ ํจ๊ป ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ์ ๋ค์๊ณผ ๊ฐ๋ค.
ย
customElements.define( 'my-paragraph', class extends HTMLElement { constructor() { super(); const template = document.getElementById('my-paragraph'); this.attachShadow({ mode: 'open' }); this.shadowRoot.appendChild(template.content.cloneNode(true)); } } );
ย
์ฌ๊ธฐ์ ๊ตณ์ด
cloneNode()
๋ฅผ ํ๋ ์ด์ ๋? ์ด์จ๋ Template์ ํ๋์ด๊ธฐ ๋๋ฌธ์ ์ฌ๋ฌ ๊ณณ์์ my-paragraph
๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํด์๋ cloneNode()
๋ฅผ ์ด์ฉํด์ผ ์ ์์ ์ผ๋ก ๋์ํ๊ฒ ๋๋ค.ย
์ํผ ์์ ๊ฐ์ด ์ฌ์ฉ์ด ๊ฐ๋ฅํ๋ค.
ย
Adding flexibility with Slots
ย
์ฌ๊ธฐ์
<slot>
ํ๊ทธ๋ฅผ ์ด์ฉํด๋ณด๋๋ก ํ๊ฒ ๋ค.์ด๋ฅผ ์ด์ฉํ๋ฉด ๋งค์ฐ flexibleํ Web Component์ ๊ตฌ์ฑ์ด ๊ฐ๋ฅํด์ง๋ค.
ย
<template> <p><slot></slot></p> </template>
ย
์ด๋ ๊ฒ template์ ๊ตฌ์ฑํ๋ค๋ฉด, ๋ค์๊ณผ ๊ฐ์ด
<slot>
ํ๊ทธ ์๋ฆฌ์ Element๋ฅผ ์ ์ํ ์ ์๊ฒ ๋๋ค.ย
<my-paragraph> TEXT </my-paragraph>
ย
๋ ๋๋ง ์ ์๋์ ๊ฐ๋ค.
ย
<p>TEXT</p>
ย
๋ง์ฝ ์ฌ๋ฌ ๊ฐ์
<slot>
์ด ํ์ํ๋ค๋ฉด? ๊ฐ๋จํ๊ฒ ์ด๋ฆ์ ์ง์ ํด ์ฌ์ฉ์ด ๊ฐ๋ฅํ๋ค.ย
<template> <p class="title"><slot name="title"></slot></p> <p><slot></slot></p> </template>
ย
์ด๋ ๊ฒ ์ ์๋ ํ
ํ๋ฆฟ์ ์๋์ ๊ฐ์ด ์ฌ์ฉ์ด ๊ฐ๋ฅํ๋ค.
ย
<my-paragraph> <span slot="title">TITLE</span> Contents </my-paragraph>
ย
๋ ๋๋ง ์ ์๋์ ๊ฐ๋ค.
ย
<p class="title"><span>TITLE</span></p> <p>Contents</p>
ย
์ฐธ๊ณ ๋ก... ์ง๊ธ์
<slot>
๋ด๋ถ์ ์ด๋ ํ ๋ด์ฉ๋ ๋ค์ด๊ฐ์์ง ์์ผ๋ ๋ง์ฝ default node๋ฅผ ์ํ๋ค๋ฉด? ์ํ๋ ์ฝ๋๋ฅผ <slot>
๋ด๋ถ์ ๋ฃ์ด์ฃผ๋๋ก ํ์.ย
<template> <p><slot>default text</slot></p> </template>
ย
ย
Scoped styles
๋์ผ๋ก, Styles๋ฅผ Component ๋ด์๋ง ์ ์ํ๋ ๋ฐฉ๋ฒ์ ๋ํด ๋ณด๊ฒ ๋ค.
์ฌ์ค ๋ฐฉ๋ฒ์ ๊ฐ๋จํ๋ฐ, ๊ทธ๋ฅ Shadow DOM ๋ด์
<style>
ํ๊ทธ๊ฐ ์ค๊ฒ๋ ๋ง๋ค์ด์ฃผ๋ฉด ๋๋ค.ย
const html = ` <div></div> <style> div { width: 100px; height: 100px; background-color: black; } </style> `; class extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'closed' }) .innerHTML = html; }
ย
์์์
<template>
ํ๊ทธ๋ฅผ ์ด์ฉํ์ ๋๋ <style>
ํ๊ทธ์ ์กด์ฌํ๋ CSS Styles๊ฐ ์ ์ญ์ ์ผ๋ก ์ ์ธ๋์๋ ๊ฒ์ ๋ฐํด, ์ ์ฝ๋๋ Shadow DOM์ ์ด์ฉํ๊ธฐ์ <style>
ํ๊ทธ์ ์กด์ฌํ๋ CSS Styles๋ Shadow Boundary ๋ฐ์ผ๋ก ์ํฅ์ ๋ผ์น ์ ์๊ฒ ๋๋ค.ย