๐ 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
๋ ์์ ๊ฐ์ด ๋ณ๋์ ๋ผ์ดํ์ฌ์ดํด ๋ฉ์๋๋ฅผ ์ ๊ณตํด ์ค๋๋ค.
- 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๋ฅผ ์ฐธ๊ณ ํ์๋ฉด ๋ฉ๋๋ค.
๐ Web Component๋ก Todo ์ฑ ๋ง๋ค๊ธฐ
์น ์ปดํฌ๋ํธ๋ฅผ ์ง์ ํ์ฉํด ๋ณด๊ธฐ ์ํด์ ๊ฐ๋จํ Todo App์ ๋ง๋ค์ด๋ดค์ต๋๋ค.
PoC๋ฅผ ์ํด ๋ฌํํ๊ฒ ๊ตฌํํ๋๋ผ ์ฝ๋๋ ์ข ์ง์ ๋ถํฉ๋๋ค. ๐
์ฐธ๊ณ ์ฉ์ผ๋ก๋ง ๋ด์ฃผ์๋ฉด ์ข์ ๊ฒ ๊ฐ์์!
๐ web component ๋ผ์ด๋ธ๋ฌ๋ฆฌ
PoC ์ฑ์ผ๋ก ํ์ธํด ๋ณด๋ ์น ์ปดํฌ๋ํธ API ๋ง์ ์ด์ฉํ๊ธฐ์๋ ๋ถํธํ ์ ๋ค์ด ์์๋๋ฐ์,
๋ง์นจ ์ด๋ฌํ ๋ฌธ์ ์ ๋ค์ ๊ฐ์ ํ ์ฌ๋ฌ ๋ํผ ํจํค์ง๋ค์ด ์กด์ฌํฉ๋๋ค.
๋ง์ฝ ์น ์ปดํฌ๋ํธ๋ก ์ฑ์ ๊ฐ๋ฐํ๊ธฐ๋ก ๊ฒฐ์ ํ๋ค๋ฉด, ์์ฐ์ฑ์ ์ํด ์๋ ํจํค์ง๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ด ์ข์ ๊ฒ ๊ฐ๋ค๋ ์๊ฒฌ์ ๋๋ค. ๐
@polymer/polymer vs @stencil/core vs hybrids vs lit-element vs slim-js | npm trends
๐ ์ฐธ๊ณ ์๋ฃ
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
'๐จโ๐ป web.dev > fe' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
Canvas ๋ํ ํ์ ์ ์๋ฆฌ์ ๊ตฌํ ๋ฐฉ๋ฒ (1) | 2023.08.29 |
---|---|
Storybook useArgs ํ์ฉํ๊ธฐ (1) | 2023.08.28 |
Storybook loaders ๋ก story์ ๋น๋๊ธฐ ๋ฐ์ดํฐ ์ฃผ์ ํ๊ธฐ (0) | 2023.06.24 |
Storybook ์์ ์ปดํฌ๋ํธ ๋จ์ ํ ์คํธํ๊ธฐ (0) | 2023.06.24 |
React Framer Motion ํบ์๋ณด๊ธฐ (0) | 2023.03.27 |
๐ฌ ๋๊ธ