๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
๐Ÿ‘จ‍๐Ÿ’ป web.dev/fe

Web Component์™€ ํ•จ๊ป˜ ์ปดํฌ๋„ŒํŠธ ๋งŒ๋“ค๊ธฐ

by HandHand 2023. 8. 25.

 

๐Ÿ“Œ Web Component ๋ž€?

web component๋Š” HTML/CSS/JS๋ฅผ ์ด์šฉํ•˜์—ฌ ์žฌ์‚ฌ์šฉ๊ฐ€๋Šฅํ•œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋Š” ํ‘œ์ค€ ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค.

๋‚˜์˜จ ์ง€๋Š” ๊ฝค ์˜ค๋ž˜๋œ ๊ธฐ์ˆ ์ด์ง€๋งŒ, ์ง์ ‘ ์จ๋ณด์ง€ ์•Š๋Š” ์ด์ƒ ์ ‘ํ•ด๋ณผ ๊ธฐํšŒ๊ฐ€ ๋งŽ์ง€ ์•Š์•„์„œ

์ด๋ฒˆ ๊ธฐํšŒ์— ์‚ฌ์ด๋“œ ํ”„๋กœ์ ํŠธ์— ์ ์šฉํ•ด ๋ณผ ๊ฒธ ๊ด€๋ จ ์ŠคํŽ™์„ ์ฐพ์•„๋ณด๊ณ  ์ •๋ฆฌํ•ด ๋ดค์Šต๋‹ˆ๋‹ค.

 

๐Ÿ“Œ web component ํ†บ์•„๋ณด๊ธฐ

์›น ์ปดํฌ๋„ŒํŠธ๋Š” ์ปดํฌ๋„ŒํŠธ ์žฌ์‚ฌ์šฉ๊ณผ ์บก์Šํ™”๋ฅผ ์œ„ํ•ด์„œ Custom Element , Shadow DOM , HTML template ์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

1๏ธโƒฃ custom element API

์›น ํ‘œ์ค€์€ custom element ๋ผ๋Š” JavaScript API๋ฅผ ํ†ตํ•ด ์ง์ ‘ HTML ์—˜๋ฆฌ๋จผํŠธ๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํ‘œ์ค€์—์„œ๋Š” Autonomous custom ์™€ Customized built-in ๋ผ๋Š” 2๊ฐ€์ง€ ๋ฐฉ์‹์œผ๋กœ ๊ตฌํ˜„์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

 

  • Autonomous custom element
    • HTMLElement ๋ฅผ ์ง์ ‘ ์ƒ์†ํ•ด์„œ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.
  • Customized built-in element
    • HTML ํ‘œ์ค€ ๋นŒํŠธ์ธ ์—˜๋ฆฌ๋จผํŠธ (div, span, button …) ๋“ค์„ ์ƒ์†ํ•ด์„œ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.

 

์ด ๋‘ ๋ฐฉ์‹์€ ์ •์˜ํ•˜๋Š” ๋ฐฉ๋ฒ•๋„ ๋‹ค๋ฅด๊ณ , ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•๋„ ๋‹ค๋ฆ…๋‹ˆ๋‹ค.

๋‹ค๋งŒ ์•„์ง Safari ์—์„œ๋Š” ํ›„์ž์˜ ๋ฐฉ์‹์ด ์ง€์›๋˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— HTMLElement ๋ฅผ ์ง์ ‘ ์ƒ์†๋ฐ›๋Š” Autonomous ํ˜•ํƒœ๋กœ ๊ตฌํ˜„ํ•˜๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

class MyElement extends HTMLElement {
    // Fires when an instance of the element is created or updated
    constructor() {
        super();
    }

    // Fires when an instance was inserted into the document
    connectedCallback() {
    }

    // Fires when an instance was removed from the document
    disconnectedCallback() {
    }

    // Fires when an attribute was added, removed, or updated
    attributeChangedCallback(attrName, oldVal, newVal) {
    }

    // Fires when an element is moved to a new document
    adoptedCallback() {
    }
}

custom element ๋Š” ์œ„์™€ ๊ฐ™์ด ๋ณ„๋„์˜ ๋ผ์ดํ”„์‚ฌ์ดํด ๋ฉ”์„œ๋“œ๋ฅผ ์ œ๊ณตํ•ด ์ค๋‹ˆ๋‹ค.

 

์ถœ์ฒ˜:  https://javascript.works-hub.com/learn/web-components-api-lifecycle-events-and-custom-events-66668

 

  • constructor

์›น ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ดˆ๊ธฐํ™”๋˜๋ฉด ๊ฐ€์žฅ ๋จผ์ € constructor ๊ฐ€ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.

์—ฌ๊ธฐ์„œ๋Š” ์ฃผ๋กœ shadowRoot ํ• ๋‹น ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.

  • connectedCallback

์›น ์ปดํฌ๋„ŒํŠธ๊ฐ€ DOM์— ์ถ”๊ฐ€๋œ ์ดํ›„์— ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค.

DOM ๊ด€๋ จ ์—ฐ์‚ฐ์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ ํ•ด๋‹น ๋ผ์ดํ”„์‚ฌ์ดํด์— ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.

  • adoptedCallback

์›น ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ƒˆ๋กœ์šด Document๋กœ ์ด๋™๋˜์—ˆ์„ ๋•Œ ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค.

  • attributeChangedCallback

์›น ์ปดํฌ๋„ŒํŠธ์˜ static ์†์„ฑ์ธ observedAttributes ์— ์ •์˜๋œ ์†์„ฑ๊ฐ’์ด ์ถ”๊ฐ€, ๋ณ€๊ฒฝ, ์‚ญ์ œ๋˜์—ˆ์„ ๋•Œ ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค.

์ด๋ฅผ ํ†ตํ•ด์„œ observable ํ•œ ์†์„ฑ์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

2๏ธโƒฃ template & slot

template ๊ณผ slot ์€ ๋ณด๋‹ค ์œ ์—ฐํ•œ ๋งˆํฌ์—… ๊ตฌ์กฐ๋ฅผ ์œ„ํ•ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ํ‘œ์ค€ ๊ธฐ์ˆ ์ž…๋‹ˆ๋‹ค.

template ์€ ์ผ์ข…์˜ Fragment ๋กœ์„œ ๋งŽ์€ ์–‘์˜ JS ์ฝ”๋“œ ์—†์ด ์›ํ•˜๋Š” ํ˜•ํƒœ์˜ ํ…œํ”Œ๋ฆฟ ๊ตฌ์กฐ๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

JS ๋ฅผ ํ†ตํ•ด clone ๋˜์–ด DOM ํŠธ๋ฆฌ์— ์‚ฝ์ž…๋  ์ˆ˜ ์žˆ์œผ๋ฉฐ ๋‚ด๋ถ€์— style ๋ฐ script ์š”์†Œ๋ฅผ ํฌํ•จํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋…๋ฆฝ์ ์ธ ๊ธฐ์ˆ ์ด์ง€๋งŒ, ์›น ์ปดํฌ๋„ŒํŠธ์™€ ํ•จ๊ป˜ ์‚ฌ์šฉ๋  ๋•Œ ์‹œ๋„ˆ์ง€๋ฅผ ๋ฐœํœ˜ํ•ฉ๋‹ˆ๋‹ค.

 

<template>
    <style>
    :host {
      display: block;
      width: fit-content;
      padding: 15px 20px;
    }
  </style>
  <div></div>
</template>

 

์‚ฌ์‹ค ES6 Template literals ์ด ์ŠคํŽ™์— ์ถ”๊ฐ€๋˜๋ฉด์„œ ์˜๋ฏธ๊ฐ€ ํ‡ด์ƒ‰๋œ ์ŠคํŽ™์œผ๋กœ ๋ณด์ด์ง€๋งŒ,

์›น ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ตฌ์„ฑํ•˜๋Š” ๊ธฐ์ˆ ๋กœ์„œ๋Š” ํ•œ ๋ฒˆ์ฏค ์‚ดํŽด๋ณผ๋งŒํ•œ ๊ฐ€์น˜๊ฐ€ ์žˆ๋‹ค๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค.

slot ์€ ์›น ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€์— ์‚ฌ์šฉ์ž๊ฐ€ ์ž์‹ ๋งŒ์˜ ๋งˆํฌ์—…์„ ์ฑ„์›Œ ๋„ฃ์„ ์ˆ˜ ์žˆ๋„๋ก ํ•ด์ค๋‹ˆ๋‹ค.

๋งŒ์•ฝ ๋‹ค์Œ๊ณผ ๊ฐ™์ด description ์„ slot ์œผ๋กœ ์ง€์ •ํ•œ๋‹ค๋ฉด,

 

<p>
    <slot name="description">default description</slot>
</p>

 

์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€์— ๋ Œ๋”๋ง ํ•˜๊ณ ์ž ํ•˜๋Š” ๋‚ด์šฉ์„ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ฑ„์šธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋งŒ์•ฝ description ์Šฌ๋กฏ์„ ์ง€์ •ํ•˜์ง€ ์•Š๊ฑฐ๋‚˜ ๋ธŒ๋ผ์šฐ์ €์—์„œ slot ์„ ์ง€์›ํ•˜์ง€ ์•Š๋Š”๋‹ค๋ฉด

์œ„์—์„œ ์„ ์–ธํ•œ default description ์ด๋ผ๋Š” ํ…์ŠคํŠธ๊ฐ€ ๋…ธ์ถœ๋ฉ๋‹ˆ๋‹ค.

 

<my-webcomponent>
    <span slot="description">Hello world!</span>
</my-webcomponent>

 

3๏ธโƒฃ shadow DOM

 

shadow DOM ์€ ์™ธ๋ถ€ DOM ๊ตฌ์กฐ๋กœ๋ถ€ํ„ฐ ์บก์Šํ™”๋ฅผ ์ œ๊ณตํ•ด ์ค๋‹ˆ๋‹ค.

๊ธฐ์กด์˜ DOM API๋Š” ์™ธ๋ถ€ ๋งˆํฌ์—…๊ตฌ์กฐ, ์Šคํƒ€์ผ ๋“ฑ์˜ ์˜ํ–ฅ์„ ๋ฐ›์ง€๋งŒ shadow DOM ์„ ํ†ตํ•ด์„œ

๊ฐ€๋ ค์ง„ DOM ํŠธ๋ฆฌ ๋ฅผ ์ƒ์„ฑํ•œ ๋’ค ์™ธ๋ถ€ DOM ํŠธ๋ฆฌ์™€ ๋ถ„๋ฆฌ๋œ ํ™˜๊ฒฝ์„ ์œ ์ง€ํ•œ ์ฑ„๋กœ ์—ฐ๊ฒฐ๋ฉ๋‹ˆ๋‹ค.

 

element.attachShadow({ mode: 'open' });

 

mode ๋Š” ์บก์Šํ™” ๋ชจ๋“œ๋ฅผ ์„ค์ •ํ•˜๋Š” open ๊ณผ closed ์˜ต์…˜์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

closed ๋กœ ์„ค์ •ํ•˜๋ฉด shadowRoot์— ์•„์–˜ ์ ‘๊ทผ์ด ๋ถˆ๊ฐ€๋Šฅํ•˜๊ณ ,

open ์œผ๋กœ ์„ค์ • ์‹œ JavaScript ๋กœ shadowRoot์— ์ ‘๊ทผ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

 

์ด๋Ÿฌํ•œ ์บก์Šํ™”๋Š” ์–ธ์ œ ํšจ๊ณผ์ ์œผ๋กœ ํ™œ์šฉ๋ ๊นŒ์š”? ๋ฐ”๋กœ ์ปดํฌ๋„ŒํŠธ ์Šคํƒ€์ผ๋ง์ž…๋‹ˆ๋‹ค.

Shadow DOM ์„ ํ™œ์šฉํ•˜๋ฉด ์™ธ๋ถ€ DOM์— ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š๋Š” ๊ณ ์œ ํ•œ ์Šคํƒ€์ผ์„ ๊ฐ€์ง€๋Š” ์™„์ „ํžˆ ๋…๋ฆฝ๋œ sandbox๋ฅผ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

<style>
  body { color: white; } 
  .test { background-color: red; }
</style>

<styled-element>
  #shadow-root
    <style>
      div { background-color: blue; }
    </style>
    <div class="test">Test</div>
</styled-element>

 

์œ„๋Š” ์˜ˆ์‹œ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค. div ๋Š” ์–ด๋–ค ์ƒ‰์ผ๊นŒ์š”?

shadow tree ์™ธ๋ถ€์˜ css ์„ ํƒ์ž๋Š” shadow tree ๋‚ด๋ถ€์˜ ์„ ํƒ์ž์™€ ์ผ์น˜๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ ์ด์™ธ์— ์ƒ์† ๊ฐ€๋Šฅํ•œ ์Šคํƒ€์ผ์€ host์—์„œ shadow tree ๋‚ด๋ถ€๋กœ ์ƒ์†๋ฉ๋‹ˆ๋‹ค.

์œ„ ์˜ˆ์‹œ์—์„œ test ์„ ํƒ์ž๋Š” shadow tree ๋‚ด๋ถ€์˜ test ์™€๋Š” ์ผ์น˜๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๋‹ค๋งŒ body ํƒœ๊ทธ์— ํ• ๋‹น๋œ ์ปฌ๋Ÿฌ ์Šคํƒ€์ผ์€ ์ƒ์†๋˜์ง€์š”.

 

๐Ÿ“Œ ๊ฐ„๋‹จํ•œ web component๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž

1๏ธโƒฃ custom element ์ฒซ ์‚ฝ ๋œจ๊ธฐ

๊ฐ„๋‹จํ•œ ์›น ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ง์ ‘ ๋งŒ๋“ค์–ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

์‚ฌ์ง„๊ณผ ์ƒ์„ธ ์„ค๋ช…์„ ๋ฌถ์–ด์„œ ๋ณด์—ฌ์ฃผ๋Š” ์ปดํฌ๋„ŒํŠธ์ž…๋‹ˆ๋‹ค.

๋จผ์ € HTMLElement ๋ฅผ ํ™•์žฅํ•œ ImageFigure ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.

 

class ImageFigure extends HTMLElement {
    constructor() {
        super();
    }

        get src() {
            return this.getAttribute("src") || null;
        }

        get caption() {
            return this.getAttribute("caption") || "";
        }

        get alt() {
            return this.getAttribute("alt") || null;
        }

    connectedCallback() {
        this.render();
    }

    render() {
        this.innerHTML = this.template({
            src: this.src,
            alt: this.alt,
            caption: this.caption,
        });
    }

    template(state) {
        return `
           <figure>
              <img src="${state.src}" alt="${state.alt || state.caption}" />
              <figcaption>${state.caption}</figcaption>
           </figure>
        `;
    }
}

 

2๏ธโƒฃ custom element ๋“ฑ๋ก

๊ทธ๋ฆฌ๊ณ  ํ•˜๋‹จ์— ์šฐ๋ฆฌ๊ฐ€ ๊ตฌํ˜„ํ•œ custom element ๋ฅผ ๋“ฑ๋กํ•ด ์ฃผ๋Š” ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

CustomElementRegistry ์ธํ„ฐํŽ˜์ด์Šค์—์„œ ์ œ๊ณตํ•ด ์ฃผ๋Š” define ํ•จ์ˆ˜๋ฅผ ์ด์šฉํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

์ฃผ์˜ํ•  ์ ์€ ์›น ์ปดํฌ๋„ŒํŠธ๋Š” - ๋กœ ๊ตฌ๋ถ„๋˜๋Š” 2 ๋‹จ์–ด ์ด์ƒ์œผ๋กœ ๋ช…๋ช…๋˜์–ด์•ผ ํ•œ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

HTML parser๊ฐ€ ์ด ํ˜•์‹์œผ๋กœ ์ผ๋ฐ˜์ ์ธ HTML ํƒœ๊ทธ์™€ ๊ตฌ๋ถ„ํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

 

// ... ImageFigure ํด๋ž˜์Šค ์ฝ”๋“œ

window.customElements.define('image-figure', ImageFigure);

 

์ด์ œ ์ด ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ถˆ๋Ÿฌ์™€์„œ ์‚ฌ์šฉํ•ด ๋ณผ๊นŒ์š”?

 

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>my-web-component</title>

    <!-- โœ… Imports custom element -->
    <script type="module" src="my-element.js"></script>
</head>
<body>

    <!-- โœ… Use custom-element -->
    <image-figure 
        style="max-width: 100px"    
        src="https://picsum.photos/id/237/200/300" 
        alt="Free Static Hosting"
        caption="์ƒˆ๋กœ์šด ์ด๋ฏธ์ง€">
    </image-figure>

</body>
</html>

 

 

3๏ธโƒฃ ๋ผ์ดํ”„์‚ฌ์ดํด ์ถ”๊ฐ€ํ•˜๊ธฐ

๋งŒ์•ฝ ํŠน์ • ์†์„ฑ์˜ ๋ณ€ํ™”๋ฅผ ๊ฐ์ง€ํ•˜์—ฌ ๊ทธ์— ๋”ฐ๋ฅธ effect ๋ฅผ ์ˆ˜ํ–‰ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ์–ด๋–ป๊ฒŒ ํ•  ์ˆ˜ ์žˆ์„๊นŒ์š”?

์ด๋ฅผ ์œ„ํ•ด์„œ๋Š” attributeChangedCallback ๋ผ๋Š” ๋ผ์ดํ”„์‚ฌ์ดํด ๋ฉ”์„œ๋“œ๋ฅผ ํ™œ์šฉํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

ํ•ด๋‹น ๋ผ์ดํ”„์‚ฌ์ดํด์€ observedAttributes ์—์„œ ๋ฐ˜ํ™˜ํ•˜๋Š” ์†์„ฑ์˜ ๋ณ€๊ฒฝ์„ ๊ฐ์ง€ํ•ฉ๋‹ˆ๋‹ค.

์ด์ œ ์œ„ ์ฝ”๋“œ์—์„œ src ์†์„ฑ์„ ๋ฐ˜์‘ํ˜•์œผ๋กœ ๋งŒ๋“ค์–ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

 

class ImageFigure extends HTMLElement {
        // โœ… observe ํ•  ์†์„ฑ์„ ์ •์˜ํ•ด์ฃผ๊ณ 
    static get observedAttributes() {
        return ['src']
    }

        // โœ… ํ•ด๋‹น ์†์„ฑ์ด ๋ณ€ํ•  ๊ฒฝ์šฐ์— ๋Œ€์‘ํ•˜๋Š” ์ฝœ๋ฐฑ์„ ์ •์˜ํ•จ
    attributeChangedCallback(attr, oldValue, newValue) {
        if (oldValue === newValue) {
            return
        }

                if (attr === "src") {
              const $image = this.querySelector("img");
              if ($image) {
                $image.src = newValue;
              }
            }
    }

        // ... other code
}

window.customElements.define("image-figure", ImageFigure);

 

์ด์ œ ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด ํƒ€์ด๋จธ๋ฅผ ์„ค์ •ํ•ด์„œ ์ด๋ฏธ์ง€ ์†Œ์Šค ๋ฐฐ์—ด์„ ์ˆœํšŒํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

 

 

<body>
    <image-figure
        id="image-figure-example"
        style="max-width: 100px"    
        src="https://picsum.photos/id/237/200/300" 
        alt="Free Static Hosting"
        caption="์ƒˆ๋กœ์šด ์ด๋ฏธ์ง€">
    </image-figure>

    <script>
        const imageFigure = document.querySelector('#image-figure-example')

        const imageSources = [
            'https://picsum.photos/id/237/200/300',
            'https://picsum.photos/id/238/200/300',
            'https://picsum.photos/id/239/200/300',
            'https://picsum.photos/id/240/200/300'
        ]

        let idx = 0

        setInterval(() => {
            imageFigure.setAttribute('src', imageSources[idx])
            idx = (idx + 1) % imageSources.length
        }, 2000)
    </script>
</body>

 

5๏ธโƒฃ shadow DOM์„ ํ™œ์šฉํ•œ ์Šคํƒ€์ผ ์บก์Šํ™”

Vue๋ฅผ ๊ฒฝํ—˜ํ•ด ๋ณด์…จ๋‹ค๋ฉด ์ปดํฌ๋„ŒํŠธ ์Šคํƒ€์ผ๋ง์‹œ์— ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ฝ”๋“œ๋ฅผ ์‚ฌ์šฉํ•ด ๋ณด์‹  ๊ฒฝํ—˜์ด ์žˆ์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

 

<style scoped>
</style>

 

์—ฌ๊ธฐ์„œ scoped ์†์„ฑ์€ ์Šคํƒ€์ผ ์บก์Šํ™”๋ฅผ ์œ„ํ•ด Vue ํ…œํ”Œ๋ฆฟ ๋ฌธ๋ฒ•์—์„œ ์ง€์›ํ•˜๊ณ  ์žˆ๋Š” ๊ธฐ๋Šฅ์ธ๋ฐ์š”,

shadow DOM ์„ ํ™œ์šฉํ•˜๋ฉด ์ „์—ญ ํ™˜๊ฒฝ์— ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š๋Š” ์Šคํƒ€์ผ ์บก์Šํ™”๋ผ๋Š” ๋™์ผํ•œ ๋ชฉ์ ์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

์ด์ œ ImageFigure ์— shadow DOM์„ ์ ์šฉํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

 

class ImageFigure extends HTMLElement {
    #shadow;
    // ...  

    constructor() {
    super();
    this.#shadow = this.attachShadow({ mode: "open" }); // โœ… attach shadow root
  }

    connectedCallback() {
      this.render();
  }

    attributeChangedCallback(attr, oldValue, newValue) {
    if (oldValue === newValue) {
      return;
    }

    if (attr === "src") {
            // โœ… query element from shadow root
      const $image = this.#shadow.querySelector("img");
      if ($image) {
        $image.src = newValue;
      }
    }
  }

    render() {
                // โœ… set innerHTML below shadowRoot
        this.#shadow.innerHTML = this.template({
            src: this.src,
            alt: this.alt,
            caption: this.caption,
        });
    }

    template(state) {
        return `
            <style> 
            :host {
                display: inline-block; 
                margin: 5px; 
                padding: 5px;
                border: 1px solid #ccc; 
                border-radius: 5px; 
            }
            figure { margin: 0; }
            figure img {
                max-width: 100%; 
                border: 1px solid #aaa; 
                border-radius: 5px; 
                box-sizing: border-box; 
            } 
            </style>
            <figure>
                <img src="${state.src}" alt="${state.alt || state.caption}" />
                <figcaption>${state.caption}</figcaption>
            </figure>
        `;
    }
}

 

์—ฌ๊ธฐ์„œ :host ๋Š” shadow root host์— ๋Œ€ํ•œ pseudo class selector์ž…๋‹ˆ๋‹ค.

 

๊ฐœ๋ฐœ์ž๋„๊ตฌ๋กœ ์ง€์ •ํ•œ ์Šคํƒ€์ผ์ด ์ž˜ ์ ์šฉ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•ด๋ณด์ž

 

6๏ธโƒฃ slot์œผ๋กœ ํ™•์žฅํ•˜๊ธฐ

ํ˜„์žฌ๋Š” caption ์„ ๋ฌธ์ž์—ด๋กœ ๋ฐ›๊ณ  ์žˆ๋Š”๋ฐ, ์ด๋ฅผ ์›ํ•˜๋Š” HTML ์—˜๋ฆฌ๋จผํŠธ๋กœ ์ฑ„์›Œ ๋„ฃ์„ ์ˆ˜ ์žˆ๋„๋ก ํ™•์žฅํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

class ImageFigure extends HTMLElement {
    // ... other code

    template(state) {
        return `
            // ...

         <figure>
        <img src="${state.src}" alt="${state.alt || state.caption}" />
        <figcaption>
          <slot name="catpion"></slot>
        </figcaption>
     </figure>
        `
    }
}

 

์‚ฌ์šฉํ•˜๋Š” ์ชฝ์—์„œ๋Š” ์›ํ•˜๋Š” ์—˜๋ฆฌ๋จผํŠธ๋ฅผ ๋ช…๋ช…๋œ slot๊ณผ ํ•จ๊ป˜ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.

 

<image-figure
  id="image-figure-example"
  style="max-width: 100px;"
  src="https://picsum.photos/id/237/200/300"
  alt="Free Static Hosting"
>
  <span slot="catpion">image caption</span>
</image-figure>

 

7๏ธโƒฃ custom event

์›น ์ปดํฌ๋„ŒํŠธ๋กœ ์ƒˆ๋กœ์šด ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค๋ฉด ํŠน์ • ์ด๋ฒคํŠธ๋ฅผ ์ƒ์„ฑํ•ด์„œ ์ „๋‹ฌํ•ด์ฃผ๊ณ  ์‹ถ์€ ๊ฒฝ์šฐ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด ๊ฒฝ์šฐ์—๋Š” ๊ธฐ๋ณธ Event ๊ฐ์ฒด๋ฅผ ํ™•์žฅํ•œ ์ƒˆ๋กœ์šด Native ์ด๋ฒคํŠธ ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋Š” CustomEvent ์ƒ์„ฑ์ž๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

 

new CustomEvent(type, options)

 

์ด๋ ‡๊ฒŒ ์ƒ์„ฑ๋œ ์ด๋ฒคํŠธ๋Š” dispatchEvent ๋ฅผ ํ†ตํ•ด ์ƒ์œ„ ๋…ธ๋“œ๋กœ ์ „ํŒŒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด๋ฒˆ ImageFigure ์˜ˆ์ œ์—์„œ๋Š” src ๊ฐ€ ๋ฐ”๋€”๋•Œ change:src ์ด๋ฒคํŠธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

 

attributeChangedCallback(attr, oldValue, newValue) {
    // ...

    if (attr === "src") {
    const $image = this.#shadow.querySelector("img");
    if ($image) {
      $image.src = newValue;
      this.dispatchEvent(
        new CustomEvent("change:src", { bubbles: true, composed: true })
      );
    }
  }
}

 

์ด๋ฒคํŠธ๋ฅผ ๋ฐ›์œผ๋ฉด ๋กœ๊ทธ๋ฅผ ์ฐ์–ด์„œ ํ™•์ธํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

 

 

<script>
    const imageFigure = document.querySelector("#image-figure-example");

    imageFigure.addEventListener("change:src", () => {
      console.log("change");
    });

    // ... 
</script>

 

๐Ÿ“Œ ์ตœ์ข… ์ฝ”๋“œ

์œ„์—์„œ ์‚ดํŽด๋ณธ ImageFigure ์˜ ์ตœ์ข… ์ฝ”๋“œ๋Š” ๋‹ค์Œ sandbox๋ฅผ ์ฐธ๊ณ ํ•˜์‹œ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

wc-image-figure - CodeSandbox

 

wc-image-figure - CodeSandbox

wc-image-figure by sohnjunior using parcel-bundler

codesandbox.io

 

๐Ÿ“Œ Web Component๋กœ Todo ์•ฑ ๋งŒ๋“ค๊ธฐ

์›น ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ง์ ‘ ํ™œ์šฉํ•ด ๋ณด๊ธฐ ์œ„ํ•ด์„œ ๊ฐ„๋‹จํ•œ Todo App์„ ๋งŒ๋“ค์–ด๋ดค์Šต๋‹ˆ๋‹ค.

PoC๋ฅผ ์œ„ํ•ด ๋Ÿฌํ”„ํ•˜๊ฒŒ ๊ตฌํ˜„ํ•˜๋Š๋ผ ์ฝ”๋“œ๋Š” ์ข€ ์ง€์ €๋ถ„ํ•ฉ๋‹ˆ๋‹ค. ๐Ÿ™

์ฐธ๊ณ ์šฉ์œผ๋กœ๋งŒ ๋ด์ฃผ์‹œ๋ฉด ์ข‹์„ ๊ฒƒ ๊ฐ™์•„์š”!

Todo App (with Web Component)

 

Todo App (with Web Component) - CodeSandbox

web component ๋ฅผ ํ™œ์šฉํ•œ ๊ฐ„๋‹จํ•œ todo ์•ฑ

codesandbox.io

 

๐Ÿ“Œ web component ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

PoC ์•ฑ์œผ๋กœ ํ™•์ธํ•ด ๋ณด๋‹ˆ ์›น ์ปดํฌ๋„ŒํŠธ API ๋งŒ์„ ์ด์šฉํ•˜๊ธฐ์—๋Š” ๋ถˆํŽธํ•œ ์ ๋“ค์ด ์žˆ์—ˆ๋Š”๋ฐ์š”,

๋งˆ์นจ ์ด๋Ÿฌํ•œ ๋ฌธ์ œ์ ๋“ค์„ ๊ฐœ์„ ํ•œ ์—ฌ๋Ÿฌ ๋ž˜ํผ ํŒจํ‚ค์ง€๋“ค์ด ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.

๋งŒ์•ฝ ์›น ์ปดํฌ๋„ŒํŠธ๋กœ ์•ฑ์„ ๊ฐœ๋ฐœํ•˜๊ธฐ๋กœ ๊ฒฐ์ •ํ–ˆ๋‹ค๋ฉด, ์ƒ์‚ฐ์„ฑ์„ ์œ„ํ•ด ์•„๋ž˜ ํŒจํ‚ค์ง€๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹์„ ๊ฒƒ ๊ฐ™๋‹ค๋Š” ์˜๊ฒฌ์ž…๋‹ˆ๋‹ค. ๐Ÿ˜„

@polymer/polymer vs @stencil/core vs hybrids vs lit-element vs slim-js | npm trends

 

@polymer/polymer vs @stencil/core vs hybrids vs lit-element vs slim-js | npm trends

Comparing trends for @polymer/polymer 3.5.1 which has 82,734 weekly downloads and 21,952 GitHub stars vs. @stencil/core 4.1.0 which has 686,559 weekly downloads and 11,869 GitHub stars vs. hybrids 8.2.2 which has 3,005 weekly downloads and 2,862 GitHub sta

npmtrends.com

 

๐Ÿ“Œ ์ฐธ๊ณ ์ž๋ฃŒ

webcomponents.org - Discuss & share web components

Web Components(2): Custom Elements : NHN Cloud Meetup

Custom Elements v1 - Reusable Web Components

The Complete Guide to Web Components

Web Components API: Lifecycle Events and Custom Events

Lifecycle Hooks in Web Components - Ultimate Courses

Shadow DOM์€ ๋ฌด์—‡์ผ๊นŒ? | WIT๋ธ”๋กœ๊ทธ

๋ฐ˜์‘ํ˜•

๐Ÿ’ฌ ๋Œ“๊ธ€