์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐ŸŽจ ์›น ์ปดํฌ๋„ŒํŠธ์™€ ์ปค์Šคํ…€ ์—˜๋ฆฌ๋จผํŠธ๋กœ ๋งŒ๋“œ๋Š” ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ UI ์„ธ์ƒ

๐ŸŽจ ์›น ์ปดํฌ๋„ŒํŠธ์™€ ์ปค์Šคํ…€ ์—˜๋ฆฌ๋จผํŠธ๋กœ ๋งŒ๋“œ๋Š” ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ UI ์„ธ์ƒ

ํ•œ ๋ฒˆ ๋งŒ๋“ค์–ด์„œ ์–ด๋””์„œ๋“  ์“ฐ๋Š” ๋งˆ๋ฒ• ๊ฐ™์€ ๊ฐœ๋ฐœ ์ด์•ผ๊ธฐ

์•ˆ๋…•! ๐Ÿ‘‹ ์˜ค๋Š˜์€ ์›น ๊ฐœ๋ฐœ์ž๋ผ๋ฉด ๊ผญ ์•Œ์•„์•ผ ํ•  ์›น ์ปดํฌ๋„ŒํŠธ์™€ ์ปค์Šคํ…€ ์—˜๋ฆฌ๋จผํŠธ์— ๋Œ€ํ•ด ์ด์•ผ๊ธฐํ•ด๋ณผ๊ฒŒ. ํ˜น์‹œ ๊ฐ™์€ UI๋ฅผ ์—ฌ๋Ÿฌ ๋ฒˆ ๋งŒ๋“ค๋ฉด์„œ "์•„, ์ด๊ฑฐ ๋ณต์‚ฌ ๋ถ™์—ฌ๋„ฃ๊ธฐ ํ•˜๊ธฐ๋„ ๊ท€์ฐฎ์€๋ฐ ํ•œ ๋ฒˆ๋งŒ ๋งŒ๋“ค์–ด์„œ ๊ณ„์† ์“ธ ์ˆ˜ ์—†๋‚˜?" ํ•˜๊ณ  ์ƒ๊ฐํ•ด๋ณธ ์  ์žˆ์–ด? ๋ฐ”๋กœ ๊ทธ ๊ณ ๋ฏผ์„ ํ•ด๊ฒฐํ•ด์ฃผ๋Š” ๊ฒŒ ์›น ์ปดํฌ๋„ŒํŠธ์•ผ! ๐Ÿš€

์š”์ฆ˜ ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ ํŠธ๋ Œ๋“œ๋ฅผ ๋ณด๋ฉด ์ปดํฌ๋„ŒํŠธ ๊ธฐ๋ฐ˜ ๊ฐœ๋ฐœ์ด ๋Œ€์„ธ์ž–์•„? React, Vue, Angular ๊ฐ™์€ ํ”„๋ ˆ์ž„์›Œํฌ๋“ค์ด ์ธ๊ธฐ๋ฅผ ๋„๋Š” ์ด์œ ๋„ ๋ฐ”๋กœ ์ด ์ปดํฌ๋„ŒํŠธ ๊ฐœ๋… ๋•๋ถ„์ด์•ผ. ๊ทธ๋Ÿฐ๋ฐ ์›น ํ‘œ์ค€ ์ž์ฒด์—์„œ๋„ ์ด๋Ÿฐ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฑฐ, ์•Œ๊ณ  ์žˆ์—ˆ์–ด? ๐Ÿ˜Ž

์›น ์ปดํฌ๋„ŒํŠธ ์ปค์Šคํ…€ ์—˜๋ฆฌ๋จผํŠธ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ UI ํ‘œ์ค€ ๊ธฐ์ˆ ๋กœ ๊ตฌํ˜„ํ•˜๋Š” ๋ฏธ๋ž˜

๐ŸŽฏ ์›น ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ญ”๋ฐ?

์›น ์ปดํฌ๋„ŒํŠธ๋Š” ์‰ฝ๊ฒŒ ๋งํ•ด์„œ ๋ ˆ๊ณ  ๋ธ”๋ก ๊ฐ™์€ ๊ฑฐ์•ผ. ๋ ˆ๊ณ ๋กœ ์ง‘์„ ๋งŒ๋“ค ๋•Œ ์ฐฝ๋ฌธ ๋ธ”๋ก, ๋ฌธ ๋ธ”๋ก, ์ง€๋ถ• ๋ธ”๋ก์„ ์กฐ๋ฆฝํ•˜๋“ฏ์ด, ์›นํŽ˜์ด์ง€๋„ ์—ฌ๋Ÿฌ ๊ฐœ์˜ ๋…๋ฆฝ์ ์ธ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์กฐ๋ฆฝํ•ด์„œ ๋งŒ๋“œ๋Š” ๊ฑฐ์ง€. ๐Ÿ—๏ธ

๋” ์ •ํ™•ํ•˜๊ฒŒ ๋งํ•˜๋ฉด, ์›น ์ปดํฌ๋„ŒํŠธ๋Š” W3C์—์„œ ์ •์˜ํ•œ ์›น ํ‘œ์ค€ ๊ธฐ์ˆ ์˜ ๋ชจ์Œ์ด์•ผ. ํ”„๋ ˆ์ž„์›Œํฌ์— ์˜์กดํ•˜์ง€ ์•Š๊ณ  ์ˆœ์ˆ˜ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ์™€ HTML๋งŒ์œผ๋กœ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ UI ์š”์†Œ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด. ๋ฉ‹์ง€์ง€ ์•Š์•„?

๐Ÿ”‘ ์›น ์ปดํฌ๋„ŒํŠธ์˜ ํ•ต์‹ฌ 3์š”์†Œ

1๏ธโƒฃ ์ปค์Šคํ…€ ์—˜๋ฆฌ๋จผํŠธ(Custom Elements): ์ž์‹ ๋งŒ์˜ HTML ํƒœ๊ทธ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด
2๏ธโƒฃ ์„€๋„์šฐ DOM(Shadow DOM): ์Šคํƒ€์ผ๊ณผ ๊ตฌ์กฐ๋ฅผ ์บก์Аํ™”ํ•ด์„œ ์™ธ๋ถ€ ๊ฐ„์„ญ์„ ๋ง‰์•„์ค˜
3๏ธโƒฃ HTML ํ…œํ”Œ๋ฆฟ(HTML Templates): ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋งˆํฌ์—… ์กฐ๊ฐ์„ ์ •์˜ํ•  ์ˆ˜ ์žˆ์–ด

์ด ์„ธ ๊ฐ€์ง€ ๊ธฐ์ˆ ์ด ํ•ฉ์ณ์ง€๋ฉด ์ •๋ง ๊ฐ•๋ ฅํ•œ ์ปดํฌ๋„ŒํŠธ ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด. React๋‚˜ Vue ์—†์ด๋„ ๋ง์ด์•ผ! ๋ฌผ๋ก  ํ”„๋ ˆ์ž„์›Œํฌ๋“ค๋„ ๊ฐ์ž์˜ ์žฅ์ ์ด ์žˆ์ง€๋งŒ, ์›น ํ‘œ์ค€์œผ๋กœ ๋งŒ๋“ ๋‹ค๋Š” ๊ฑด ์–ด๋””์„œ๋“  ๋™์ž‘ํ•œ๋‹ค๋Š” ํฐ ์žฅ์ ์ด ์žˆ์ง€. ๐Ÿ’ช

๐ŸŽจ ์ปค์Šคํ…€ ์—˜๋ฆฌ๋จผํŠธ ๊นŠ์ด ํŒŒํ—ค์น˜๊ธฐ

์ปค์Šคํ…€ ์—˜๋ฆฌ๋จผํŠธ๋Š” ์›น ์ปดํฌ๋„ŒํŠธ์˜ ํ•ต์‹ฌ์ด์•ผ. ์ด๊ฑธ ์ดํ•ดํ•˜๋ฉด ๋‚˜๋จธ์ง€๋Š” ์ˆ ์ˆ  ํ’€๋ ค! ๊ธฐ๋ณธ ๊ฐœ๋…๋ถ€ํ„ฐ ์ฐจ๊ทผ์ฐจ๊ทผ ์•Œ์•„๋ณด์ž. ๐Ÿš€

๐Ÿ“Œ ์ปค์Šคํ…€ ์—˜๋ฆฌ๋จผํŠธ๋ž€?

HTML์—๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ์ œ๊ณต๋˜๋Š” ํƒœ๊ทธ๋“ค์ด ์žˆ์ž–์•„? div, span, button, input ๊ฐ™์€ ๊ฒƒ๋“ค ๋ง์ด์•ผ. ์ปค์Šคํ…€ ์—˜๋ฆฌ๋จผํŠธ๋Š” ์ด๋Ÿฐ ํ‘œ์ค€ ํƒœ๊ทธ๋“ค์ฒ˜๋Ÿผ ๋™์ž‘ํ•˜๋Š” ๋‚˜๋งŒ์˜ ํƒœ๊ทธ๋ฅผ ๋งŒ๋“œ๋Š” ๊ฑฐ์•ผ.

์˜ˆ๋ฅผ ๋“ค์–ด <my-button>, <user-card>, <product-slider> ๊ฐ™์€ ํƒœ๊ทธ๋ฅผ ์ง์ ‘ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด. ๊ทธ๋ฆฌ๊ณ  ์ด ํƒœ๊ทธ๋“ค์€ ์ผ๋ฐ˜ HTML ํƒœ๊ทธ์ฒ˜๋Ÿผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์ง€! ๐Ÿ˜Š

๐Ÿ’ก ์ปค์Šคํ…€ ์—˜๋ฆฌ๋จผํŠธ ๋„ค์ด๋ฐ ๊ทœ์น™

์ปค์Šคํ…€ ์—˜๋ฆฌ๋จผํŠธ ์ด๋ฆ„์—๋Š” ๋ฐ˜๋“œ์‹œ ํ•˜์ดํ”ˆ(-)์ด ๋“ค์–ด๊ฐ€์•ผ ํ•ด. ์ด๊ฑด ํ‘œ์ค€ HTML ํƒœ๊ทธ์™€ ๊ตฌ๋ถ„ํ•˜๊ธฐ ์œ„ํ•œ ๊ทœ์น™์ด์•ผ.
โœ… ์˜ฌ๋ฐ”๋ฅธ ์˜ˆ: my-component, user-profile, custom-button
โŒ ์ž˜๋ชป๋œ ์˜ˆ: mycomponent, userprofile (ํ•˜์ดํ”ˆ ์—†์Œ)

๐Ÿ”ง ์ปค์Šคํ…€ ์—˜๋ฆฌ๋จผํŠธ ๋งŒ๋“ค๊ธฐ - ๊ธฐ๋ณธํŽธ

์ž, ์ด์ œ ์‹ค์ œ๋กœ ์ปค์Šคํ…€ ์—˜๋ฆฌ๋จผํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณผ๊นŒ? ๊ฐ€์žฅ ๊ฐ„๋‹จํ•œ ์˜ˆ์ œ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด๋ณด์ž. ์‚ฌ์šฉ์ž ํ”„๋กœํ•„ ์นด๋“œ๋ฅผ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ! ๐Ÿ‘ค

// 1๋‹จ๊ณ„: HTMLElement๋ฅผ ์ƒ์†๋ฐ›๋Š” ํด๋ž˜์Šค ์ •์˜
class UserCard extends HTMLElement {
  constructor() {
    super(); // ๋ฐ˜๋“œ์‹œ super()๋ฅผ ๋จผ์ € ํ˜ธ์ถœํ•ด์•ผ ํ•ด!
    
    // ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ƒ์„ฑ๋  ๋•Œ ์‹คํ–‰๋˜๋Š” ์ฝ”๋“œ
    console.log('UserCard ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ƒ์„ฑ๋˜์—ˆ์–ด์š”!');
  }
  
  // ์ปดํฌ๋„ŒํŠธ๊ฐ€ DOM์— ์ถ”๊ฐ€๋  ๋•Œ ํ˜ธ์ถœ
  connectedCallback() {
    // ์—ฌ๊ธฐ์„œ HTML ๊ตฌ์กฐ๋ฅผ ๋งŒ๋“ค์–ด์ค˜
    this.innerHTML = `
      <div class="user-card">
        <h3>์‚ฌ์šฉ์ž ์ด๋ฆ„</h3>
        <p>์‚ฌ์šฉ์ž ์„ค๋ช…์ด ๋“ค์–ด๊ฐ‘๋‹ˆ๋‹ค.</p>
      </div>
    `;
  }
}

// 2๋‹จ๊ณ„: ์ปค์Šคํ…€ ์—˜๋ฆฌ๋จผํŠธ ๋“ฑ๋ก
customElements.define('user-card', UserCard);

// ์ด์ œ HTML์—์„œ ์ด๋ ‡๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด!
// <user-card></user-card>

์–ด๋•Œ? ์ƒ๊ฐ๋ณด๋‹ค ๊ฐ„๋‹จํ•˜์ง€? ์ด์ œ HTML ํŒŒ์ผ์—์„œ <user-card></user-card> ํƒœ๊ทธ๋ฅผ ์“ฐ๋ฉด ์šฐ๋ฆฌ๊ฐ€ ์ •์˜ํ•œ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋‚˜ํƒ€๋‚˜! ๐ŸŽ‰

๐ŸŽ“ ๋ผ์ดํ”„์‚ฌ์ดํด ๋ฉ”์„œ๋“œ ์ดํ•ดํ•˜๊ธฐ

์ปค์Šคํ…€ ์—˜๋ฆฌ๋จผํŠธ์—๋Š” ์—ฌ๋Ÿฌ ๋ผ์ดํ”„์‚ฌ์ดํด ๋ฉ”์„œ๋“œ๊ฐ€ ์žˆ์–ด. ๊ฐ๊ฐ ์–ธ์ œ ํ˜ธ์ถœ๋˜๋Š”์ง€ ์•Œ์•„๋‘๋ฉด ์ •๋ง ์œ ์šฉํ•ด!

constructor(): ์—˜๋ฆฌ๋จผํŠธ๊ฐ€ ์ƒ์„ฑ๋  ๋•Œ (new ํ‚ค์›Œ๋“œ๋กœ ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ ์‹œ)
connectedCallback(): DOM์— ์ถ”๊ฐ€๋  ๋•Œ
disconnectedCallback(): DOM์—์„œ ์ œ๊ฑฐ๋  ๋•Œ
attributeChangedCallback(): ์†์„ฑ์ด ๋ณ€๊ฒฝ๋  ๋•Œ
adoptedCallback(): ๋‹ค๋ฅธ document๋กœ ์ด๋™๋  ๋•Œ

โšก ์†์„ฑ(Attributes)๊ณผ ํ”„๋กœํผํ‹ฐ(Properties) ๋‹ค๋ฃจ๊ธฐ

์‹ค์ œ๋กœ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ๋ฐ์ดํ„ฐ๋ฅผ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ์–ด์•ผ๊ฒ ์ง€? HTML ์†์„ฑ์„ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„๋ณด์ž! ๐Ÿ“ฆ

class UserCard extends HTMLElement {
  // ๊ฐ์‹œํ•  ์†์„ฑ ๋ชฉ๋ก ์ •์˜
  static get observedAttributes() {
    return ['name', 'email', 'avatar'];
  }
  
  constructor() {
    super();
    this._name = '';
    this._email = '';
    this._avatar = '';
  }
  
  // ์†์„ฑ์ด ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค ํ˜ธ์ถœ๋จ
  attributeChangedCallback(name, oldValue, newValue) {
    console.log(`${name} ์†์„ฑ์ด ${oldValue}์—์„œ ${newValue}๋กœ ๋ณ€๊ฒฝ๋จ`);
    
    switch(name) {
      case 'name':
        this._name = newValue;
        break;
      case 'email':
        this._email = newValue;
        break;
      case 'avatar':
        this._avatar = newValue;
        break;
    }
    
    this.render(); // ํ™”๋ฉด ์—…๋ฐ์ดํŠธ
  }
  
  connectedCallback() {
    this.render();
  }
  
  render() {
    this.innerHTML = `
      <div class="user-card">
        <img src="${this._avatar}" alt="${this._name}">
        <h3>${this._name}</h3>
        <p>${this._email}</p>
      </div>
    `;
  }
}

customElements.define('user-card', UserCard);

// HTML์—์„œ ์ด๋ ‡๊ฒŒ ์‚ฌ์šฉ!
// <user-card 
//   name="๊น€๊ฐœ๋ฐœ" 
//   email="dev@example.com"
//   avatar="profile.jpg">
// </user-card>

์ด์ œ ์†์„ฑ์„ ํ†ตํ•ด ๋™์ ์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ์–ด! ๊ฐ™์€ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋‹ค๋ฅธ ๋ฐ์ดํ„ฐ๋กœ ์—ฌ๋Ÿฌ ๋ฒˆ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋œ ๊ฑฐ์ง€. ์ด๊ฒŒ ๋ฐ”๋กœ ์žฌ์‚ฌ์šฉ์„ฑ์ด์•ผ! ๐Ÿ”„

๐ŸŒ‘ ์„€๋„์šฐ DOM - ์Šคํƒ€์ผ ์บก์Аํ™”์˜ ๋งˆ๋ฒ•

์ž, ์ด์ œ ์›น ์ปดํฌ๋„ŒํŠธ์˜ ์ง„์งœ ํŒŒ์›Œ๋ฅผ ๋ณด์—ฌ์ค„ ์‹œ๊ฐ„์ด์•ผ. ์„€๋„์šฐ DOM์€ ์ปดํฌ๋„ŒํŠธ์˜ ์Šคํƒ€์ผ๊ณผ ๊ตฌ์กฐ๋ฅผ ์™„์ „ํžˆ ๊ฒฉ๋ฆฌ์‹œ์ผœ์ฃผ๋Š” ๊ธฐ์ˆ ์ด์•ผ. ๐Ÿ›ก๏ธ

์ผ๋ฐ˜์ ์œผ๋กœ CSS๋Š” ์ „์—ญ์œผ๋กœ ์ ์šฉ๋˜์ž–์•„? ๊ทธ๋ž˜์„œ ๋‹ค๋ฅธ ๊ณณ์—์„œ ์ •์˜ํ•œ ์Šคํƒ€์ผ์ด ์˜๋„์น˜ ์•Š๊ฒŒ ๋‚ด ์ปดํฌ๋„ŒํŠธ์— ์˜ํ–ฅ์„ ์ค„ ์ˆ˜ ์žˆ์–ด. ์„€๋„์šฐ DOM์„ ์‚ฌ์šฉํ•˜๋ฉด ์ด๋Ÿฐ ๋ฌธ์ œ๋ฅผ ์™„๋ฒฝํ•˜๊ฒŒ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์–ด!

โš ๏ธ ์ผ๋ฐ˜ DOM vs ์„€๋„์šฐ DOM

์ผ๋ฐ˜ DOM: ๋ชจ๋“  ์Šคํƒ€์ผ์ด ๊ณต์œ ๋จ, CSS ์ถฉ๋Œ ๊ฐ€๋Šฅ์„ฑ ๋†’์Œ
์„€๋„์šฐ DOM: ์™„์ „ํžˆ ๊ฒฉ๋ฆฌ๋œ ์Šคํƒ€์ผ ๊ณต๊ฐ„, ์™ธ๋ถ€ ์Šคํƒ€์ผ์˜ ์˜ํ–ฅ์„ ๋ฐ›์ง€ ์•Š์Œ

์ด๊ฒŒ ๋ฐ”๋กœ ์ปดํฌ๋„ŒํŠธ ๊ธฐ๋ฐ˜ ๊ฐœ๋ฐœ์—์„œ ์„€๋„์šฐ DOM์ด ์ค‘์š”ํ•œ ์ด์œ ์•ผ!

๐ŸŽญ ์„€๋„์šฐ DOM ์‚ฌ์šฉํ•˜๊ธฐ

class StyledButton extends HTMLElement {
  constructor() {
    super();
    
    // ์„€๋„์šฐ ๋ฃจํŠธ ์ƒ์„ฑ (mode: 'open' ๋˜๋Š” 'closed')
    this.attachShadow({ mode: 'open' });
    
    // ์„€๋„์šฐ DOM ๋‚ด๋ถ€์— HTML๊ณผ ์Šคํƒ€์ผ ์ถ”๊ฐ€
    this.shadowRoot.innerHTML = `
      <style>.tx-header{background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);
color:#fff;
padding:40px 30px;
border-radius:16px;
text-align:center;
margin-bottom:40px;
box-shadow:0 8px 24px rgba(102,126,234,0.25);}
.tx-intro{background:#f8f9fa;
padding:25px;
border-radius:12px;
margin:30px 0;
border-left:5px solid #667eea;}
.tx-section{margin:40px 0;}
.tx-highlight{background:linear-gradient(120deg,#fef6e4 0%,#f3e7ff 100%);
padding:20px;
border-radius:10px;
margin:25px 0;}
.tx-tip{background:#e8f5e9;
padding:18px;
border-radius:10px;
margin:20px 0;
border-left:4px solid #4caf50;}
.tx-warning{background:#fff3e0;
padding:18px;
border-radius:10px;
margin:20px 0;
border-left:4px solid #ff9800;}
.tx-point{background:#e3f2fd;
padding:18px;
border-radius:10px;
margin:20px 0;
border-left:4px solid #2196f3;}
.tx-table-wrap{overflow-x:auto;
margin:25px 0;}
.tx-table{width:100%;
border-collapse:collapse;
background:#fff;
border-radius:8px;
overflow:hidden;
box-shadow:0 2px 8px rgba(0,0,0,0.08);}
.tx-table th{background:#667eea;
color:#fff;
padding:12px;
text-align:left;
font-weight:600;}
.tx-table td{padding:12px;
border-bottom:1px solid #e0e0e0;}
.tx-table tr:last-child td{border-bottom:none;}
.tx-svg-container{text-align:center;
margin:40px 0;}
.tx-concept-box{background:#fff;
padding:20px;
border-radius:12px;
margin:20px 0;
box-shadow:0 4px 12px rgba(0,0,0,0.08);}
.tx-step{background:linear-gradient(135deg,#ffecd2 0%,#fcb69f 100%);
padding:15px 20px;
border-radius:10px;
margin:15px 0;}
.tx-benefit{background:linear-gradient(135deg,#a8edea 0%,#fed6e3 100%);
padding:15px 20px;
border-radius:10px;
margin:15px 0;}</style>
      
      <button class="btn">
        <slot>ํด๋ฆญํ•˜์„ธ์š”</slot>
      </button>
    `;
  }
}

customElements.define('styled-button', StyledButton);

์—ฌ๊ธฐ์„œ ์ค‘์š”ํ•œ ํฌ์ธํŠธ๊ฐ€ ๋ช‡ ๊ฐ€์ง€ ์žˆ์–ด! ๐ŸŽฏ

1. attachShadow({ mode: 'open' }): ์„€๋„์šฐ ๋ฃจํŠธ๋ฅผ ์ƒ์„ฑํ•ด. 'open'์€ ์™ธ๋ถ€์—์„œ shadowRoot์— ์ ‘๊ทผ ๊ฐ€๋Šฅํ•˜๋‹ค๋Š” ๋œป์ด๊ณ , 'closed'๋Š” ์ ‘๊ทผ ๋ถˆ๊ฐ€๋Šฅํ•˜๋‹ค๋Š” ๋œป์ด์•ผ.

2. <slot>: ์™ธ๋ถ€์—์„œ ์ „๋‹ฌ๋œ ์ฝ˜ํ…์ธ ๊ฐ€ ๋“ค์–ด๊ฐˆ ์ž๋ฆฌ์•ผ. ์ •๋ง ์œ ์šฉํ•œ ๊ธฐ๋Šฅ์ด์ง€!

3. ์Šคํƒ€์ผ ๊ฒฉ๋ฆฌ: ์œ„ ์ฝ”๋“œ์˜ .btn ํด๋ž˜์Šค๋Š” ์ด ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€์—๋งŒ ์ ์šฉ๋ผ. ํŽ˜์ด์ง€์˜ ๋‹ค๋ฅธ .btn ํด๋ž˜์Šค์™€ ์ถฉ๋Œํ•˜์ง€ ์•Š์•„!

๐ŸŽช Slot์˜ ๋งˆ๋ฒ•

Slot์€ ์ปดํฌ๋„ŒํŠธ ์‚ฌ์šฉ์ž๊ฐ€ ์ปค์Šคํ…€ ์ฝ˜ํ…์ธ ๋ฅผ ์‚ฝ์ž…ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ๊ธฐ๋Šฅ์ด์•ผ. ์˜ˆ๋ฅผ ๋“ค์–ด:

<styled-button>์ €์žฅํ•˜๊ธฐ</styled-button>
<styled-button>์ทจ์†Œ</styled-button>

๊ฐ™์€ ์ปดํฌ๋„ŒํŠธ์ง€๋งŒ ๋‹ค๋ฅธ ํ…์ŠคํŠธ๋ฅผ ๋ณด์—ฌ์ค„ ์ˆ˜ ์žˆ์–ด. Named slot์„ ์‚ฌ์šฉํ•˜๋ฉด ์—ฌ๋Ÿฌ ๊ฐœ์˜ slot์„ ๋งŒ๋“ค ์ˆ˜๋„ ์žˆ์–ด!

๐ŸŽจ Named Slots๊ณผ ๊ณ ๊ธ‰ ํ™œ์šฉ

class ProductCard extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    
    this.shadowRoot.innerHTML = `
      <style>.tx-header{background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);
color:#fff;
padding:40px 30px;
border-radius:16px;
text-align:center;
margin-bottom:40px;
box-shadow:0 8px 24px rgba(102,126,234,0.25);}
.tx-intro{background:#f8f9fa;
padding:25px;
border-radius:12px;
margin:30px 0;
border-left:5px solid #667eea;}
.tx-section{margin:40px 0;}
.tx-highlight{background:linear-gradient(120deg,#fef6e4 0%,#f3e7ff 100%);
padding:20px;
border-radius:10px;
margin:25px 0;}
.tx-tip{background:#e8f5e9;
padding:18px;
border-radius:10px;
margin:20px 0;
border-left:4px solid #4caf50;}
.tx-warning{background:#fff3e0;
padding:18px;
border-radius:10px;
margin:20px 0;
border-left:4px solid #ff9800;}
.tx-point{background:#e3f2fd;
padding:18px;
border-radius:10px;
margin:20px 0;
border-left:4px solid #2196f3;}
.tx-table-wrap{overflow-x:auto;
margin:25px 0;}
.tx-table{width:100%;
border-collapse:collapse;
background:#fff;
border-radius:8px;
overflow:hidden;
box-shadow:0 2px 8px rgba(0,0,0,0.08);}
.tx-table th{background:#667eea;
color:#fff;
padding:12px;
text-align:left;
font-weight:600;}
.tx-table td{padding:12px;
border-bottom:1px solid #e0e0e0;}
.tx-table tr:last-child td{border-bottom:none;}
.tx-svg-container{text-align:center;
margin:40px 0;}
.tx-concept-box{background:#fff;
padding:20px;
border-radius:12px;
margin:20px 0;
box-shadow:0 4px 12px rgba(0,0,0,0.08);}
.tx-step{background:linear-gradient(135deg,#ffecd2 0%,#fcb69f 100%);
padding:15px 20px;
border-radius:10px;
margin:15px 0;}
.tx-benefit{background:linear-gradient(135deg,#a8edea 0%,#fed6e3 100%);
padding:15px 20px;
border-radius:10px;
margin:15px 0;}</style>
      
      <div class="card">
        <div class="header">
          <slot name="title">์ œํ’ˆ ์ด๋ฆ„</slot>
        </div>
        <div class="image">
          <slot name="image"></slot>
        </div>
        <div class="description">
          <slot name="description">์ œํ’ˆ ์„ค๋ช…</slot>
        </div>
        <div class="footer">
          <slot name="price">๊ฐ€๊ฒฉ</slot>
          <slot name="action">๋ฒ„ํŠผ</slot>
        </div>
      </div>
    `;
  }
}

customElements.define('product-card', ProductCard);

// HTML์—์„œ ์ด๋ ‡๊ฒŒ ์‚ฌ์šฉ!
// <product-card>
//   <span slot="title">๋ฉ‹์ง„ ์ œํ’ˆ</span>
//   <img slot="image" src="product.jpg">
//   <p slot="description">์ด ์ œํ’ˆ์€ ์ •๋ง ์ข‹์•„์š”!</p>
//   <span slot="price">29,900์›</span>
//   <button slot="action">๊ตฌ๋งคํ•˜๊ธฐ</button>
// </product-card>

Named slot์„ ์‚ฌ์šฉํ•˜๋ฉด ์ปดํฌ๋„ŒํŠธ์˜ ์—ฌ๋Ÿฌ ๋ถ€๋ถ„์— ๊ฐ๊ฐ ๋‹ค๋ฅธ ์ฝ˜ํ…์ธ ๋ฅผ ๋„ฃ์„ ์ˆ˜ ์žˆ์–ด. ์ •๋ง ์œ ์—ฐํ•˜๊ณ  ๊ฐ•๋ ฅํ•˜์ง€? ๐Ÿ’ช

๐Ÿ“‹ HTML ํ…œํ”Œ๋ฆฟ์œผ๋กœ ํšจ์œจ์„ฑ ๋†’์ด๊ธฐ

HTML ํ…œํ”Œ๋ฆฟ์€ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋งˆํฌ์—… ์กฐ๊ฐ์„ ์ •์˜ํ•˜๋Š” ๋ฐฉ๋ฒ•์ด์•ผ. <template> ํƒœ๊ทธ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ํŽ˜์ด์ง€ ๋กœ๋“œ ์‹œ ๋ Œ๋”๋ง๋˜์ง€ ์•Š๋Š” HTML์„ ์ •์˜ํ•  ์ˆ˜ ์žˆ์–ด. ํ•„์š”ํ•  ๋•Œ๋งŒ ๋ณต์ œํ•ด์„œ ์‚ฌ์šฉํ•˜๋Š” ๊ฑฐ์ง€! ๐Ÿ“

// HTML ํŒŒ์ผ์— ํ…œํ”Œ๋ฆฟ ์ •์˜
<template id="user-card-template">
  <style>
    .user-card {
      display: flex;
      align-items: center;
      padding: 15px;
      border: 1px solid #ddd;
      border-radius: 8px;
      margin: 10px 0;
    }
    
    .avatar {
      width: 60px;
      height: 60px;
      border-radius: 50%;
      margin-right: 15px;
    }
    
    .info h3 {
      margin: 0 0 5px 0;
      color: #333;
    }
    
    .info p {
      margin: 0;
      color: #666;
      font-size: 14px;
    }
  </style>
  
  <div class="user-card">
    <img class="avatar" src="" alt="">
    <div class="info">
      <h3 class="name"></h3>
      <p class="email"></p>
    </div>
  </div>
</template>

// JavaScript์—์„œ ํ…œํ”Œ๋ฆฟ ์‚ฌ์šฉ
class UserCard extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    
    // ํ…œํ”Œ๋ฆฟ ๊ฐ€์ ธ์˜ค๊ธฐ
    const template = document.getElementById('user-card-template');
    const content = template.content.cloneNode(true);
    
    // ์„€๋„์šฐ DOM์— ์ถ”๊ฐ€
    this.shadowRoot.appendChild(content);
  }
  
  connectedCallback() {
    // ์†์„ฑ๊ฐ’์œผ๋กœ ๋‚ด์šฉ ์—…๋ฐ์ดํŠธ
    const name = this.getAttribute('name') || '์ด๋ฆ„ ์—†์Œ';
    const email = this.getAttribute('email') || 'email@example.com';
    const avatar = this.getAttribute('avatar') || 'default-avatar.png';
    
    this.shadowRoot.querySelector('.name').textContent = name;
    this.shadowRoot.querySelector('.email').textContent = email;
    this.shadowRoot.querySelector('.avatar').src = avatar;
    this.shadowRoot.querySelector('.avatar').alt = name;
  }
}

customElements.define('user-card', UserCard);

๐Ÿ’ก ํ…œํ”Œ๋ฆฟ ์‚ฌ์šฉ์˜ ์žฅ์ 

1๏ธโƒฃ ์„ฑ๋Šฅ ํ–ฅ์ƒ: ํ…œํ”Œ๋ฆฟ์€ ํŒŒ์‹ฑ๋˜์ง€๋งŒ ๋ Œ๋”๋ง๋˜์ง€ ์•Š์•„์„œ ์ดˆ๊ธฐ ๋กœ๋”ฉ์ด ๋นจ๋ผ
2๏ธโƒฃ ์ฝ”๋“œ ๋ถ„๋ฆฌ: HTML๊ณผ JavaScript๋ฅผ ๊น”๋”ํ•˜๊ฒŒ ๋ถ„๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด
3๏ธโƒฃ ์žฌ์‚ฌ์šฉ์„ฑ: ๊ฐ™์€ ํ…œํ”Œ๋ฆฟ์„ ์—ฌ๋Ÿฌ ์ปดํฌ๋„ŒํŠธ์—์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•ด
4๏ธโƒฃ ์œ ์ง€๋ณด์ˆ˜: ๋งˆํฌ์—… ๊ตฌ์กฐ๋ฅผ ํ•œ ๊ณณ์—์„œ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด

๐ŸŽฏ ์‹ค์ „ ์˜ˆ์ œ - ์™„์ „ํ•œ ์ปดํฌ๋„ŒํŠธ ๋งŒ๋“ค๊ธฐ

์ด์ œ ๋ฐฐ์šด ๋‚ด์šฉ์„ ๋ชจ๋‘ ํ™œ์šฉํ•ด์„œ ์‹ค์ „์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์™„์ „ํ•œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž! ํƒญ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ. ๐Ÿ—‚๏ธ

class TabsComponent extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this._activeTab = 0;
  }
  
  connectedCallback() {
    this.render();
    this.attachEventListeners();
  }
  
  render() {
    // ์Šฌ๋กฏ์œผ๋กœ ์ „๋‹ฌ๋œ ํƒญ ํŒจ๋„๋“ค ๊ฐ€์ ธ์˜ค๊ธฐ
    const panels = Array.from(this.querySelectorAll('[slot^="panel-"]'));
    
    this.shadowRoot.innerHTML = `
      <style>.tx-header{background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);
color:#fff;
padding:40px 30px;
border-radius:16px;
text-align:center;
margin-bottom:40px;
box-shadow:0 8px 24px rgba(102,126,234,0.25);}
.tx-intro{background:#f8f9fa;
padding:25px;
border-radius:12px;
margin:30px 0;
border-left:5px solid #667eea;}
.tx-section{margin:40px 0;}
.tx-highlight{background:linear-gradient(120deg,#fef6e4 0%,#f3e7ff 100%);
padding:20px;
border-radius:10px;
margin:25px 0;}
.tx-tip{background:#e8f5e9;
padding:18px;
border-radius:10px;
margin:20px 0;
border-left:4px solid #4caf50;}
.tx-warning{background:#fff3e0;
padding:18px;
border-radius:10px;
margin:20px 0;
border-left:4px solid #ff9800;}
.tx-point{background:#e3f2fd;
padding:18px;
border-radius:10px;
margin:20px 0;
border-left:4px solid #2196f3;}
.tx-table-wrap{overflow-x:auto;
margin:25px 0;}
.tx-table{width:100%;
border-collapse:collapse;
background:#fff;
border-radius:8px;
overflow:hidden;
box-shadow:0 2px 8px rgba(0,0,0,0.08);}
.tx-table th{background:#667eea;
color:#fff;
padding:12px;
text-align:left;
font-weight:600;}
.tx-table td{padding:12px;
border-bottom:1px solid #e0e0e0;}
.tx-table tr:last-child td{border-bottom:none;}
.tx-svg-container{text-align:center;
margin:40px 0;}
.tx-concept-box{background:#fff;
padding:20px;
border-radius:12px;
margin:20px 0;
box-shadow:0 4px 12px rgba(0,0,0,0.08);}
.tx-step{background:linear-gradient(135deg,#ffecd2 0%,#fcb69f 100%);
padding:15px 20px;
border-radius:10px;
margin:15px 0;}
.tx-benefit{background:linear-gradient(135deg,#a8edea 0%,#fed6e3 100%);
padding:15px 20px;
border-radius:10px;
margin:15px 0;}</style>
      
      <div class="tabs-container">
        <ul class="tab-list" role="tablist">
          ${panels.map((panel, index) => {
            const label = panel.getAttribute('label') || `ํƒญ ${index + 1}`;
            return `
              <li role="presentation">
                <button 
                  class="tab-button ${index === this._activeTab ? 'active' : ''}"
                  role="tab"
                  data-index="${index}"
                  aria-selected="${index === this._activeTab}"
                >
                  ${label}
                </button>
              </li>
            `;
          }).join('')}
        </ul>
        
        <div class="tab-panels">
          ${panels.map((panel, index) => `
            <div 
              class="tab-panel ${index === this._activeTab ? 'active' : ''}"
              role="tabpanel"
            >
              <slot name="panel-${index}"></slot>
            </div>
          `).join('')}
        </div>
      </div>
    `;
  }
  
  attachEventListeners() {
    const buttons = this.shadowRoot.querySelectorAll('.tab-button');
    buttons.forEach(button => {
      button.addEventListener('click', (e) => {
        const index = parseInt(e.target.dataset.index);
        this.switchTab(index);
      });
    });
  }
  
  switchTab(index) {
    if (index === this._activeTab) return;
    
    this._activeTab = index;
    
    // ๋ฒ„ํŠผ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
    const buttons = this.shadowRoot.querySelectorAll('.tab-button');
    buttons.forEach((btn, i) => {
      btn.classList.toggle('active', i === index);
      btn.setAttribute('aria-selected', i === index);
    });
    
    // ํŒจ๋„ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
    const panels = this.shadowRoot.querySelectorAll('.tab-panel');
    panels.forEach((panel, i) => {
      panel.classList.toggle('active', i === index);
    });
    
    // ์ปค์Šคํ…€ ์ด๋ฒคํŠธ ๋ฐœ์ƒ
    this.dispatchEvent(new CustomEvent('tab-changed', {
      detail: { index },
      bubbles: true,
      composed: true
    }));
  }
  
  // ์™ธ๋ถ€์—์„œ ํƒญ ๋ณ€๊ฒฝ ๊ฐ€๋Šฅํ•˜๋„๋ก
  set activeTab(index) {
    this.switchTab(index);
  }
  
  get activeTab() {
    return this._activeTab;
  }
}

customElements.define('tabs-component', TabsComponent);

// HTML์—์„œ ์‚ฌ์šฉ ์˜ˆ์‹œ:
// <tabs-component>
//   <div slot="panel-0" label="์ฒซ ๋ฒˆ์งธ ํƒญ">
//     <h3>์ฒซ ๋ฒˆ์งธ ํƒญ ๋‚ด์šฉ</h3>
//     <p>์—ฌ๊ธฐ์— ์›ํ•˜๋Š” ๋‚ด์šฉ์„ ๋„ฃ์œผ์„ธ์š”!</p>
//   </div>
//   <div slot="panel-1" label="๋‘ ๋ฒˆ์งธ ํƒญ">
//     <h3>๋‘ ๋ฒˆ์งธ ํƒญ ๋‚ด์šฉ</h3>
//     <p>๋‹ค๋ฅธ ๋‚ด์šฉ๋„ ์ž์œ ๋กญ๊ฒŒ!</p>
//   </div>
//   <div slot="panel-2" label="์„ธ ๋ฒˆ์งธ ํƒญ">
//     <h3>์„ธ ๋ฒˆ์งธ ํƒญ ๋‚ด์šฉ</h3>
//     <p>์ปดํฌ๋„ŒํŠธ๋„ ๋„ฃ์„ ์ˆ˜ ์žˆ์–ด์š”!</p>
//   </div>
// </tabs-component>

์™€! ๊ฝค ๋ณต์žกํ•ด ๋ณด์ด์ง€? ํ•˜์ง€๋งŒ ํ•˜๋‚˜์”ฉ ๋œฏ์–ด๋ณด๋ฉด ์šฐ๋ฆฌ๊ฐ€ ๋ฐฐ์šด ๋‚ด์šฉ๋“ค์ด ๋‹ค ๋“ค์–ด์žˆ์–ด! ๐Ÿ˜Š

์ด ์ปดํฌ๋„ŒํŠธ๋Š” ์„€๋„์šฐ DOM์œผ๋กœ ์Šคํƒ€์ผ์„ ๊ฒฉ๋ฆฌํ•˜๊ณ , ์Šฌ๋กฏ์œผ๋กœ ์œ ์—ฐํ•œ ์ฝ˜ํ…์ธ  ์‚ฝ์ž…์„ ์ง€์›ํ•˜๊ณ , ์ปค์Šคํ…€ ์ด๋ฒคํŠธ๋กœ ์™ธ๋ถ€์™€ ํ†ต์‹ ํ•˜๊ณ , getter/setter๋กœ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๋ฐฉ์‹์˜ ์ œ์–ด๋„ ๊ฐ€๋Šฅํ•ด. ์™„๋ฒฝํ•œ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ปดํฌ๋„ŒํŠธ์•ผ! ๐ŸŽ‰

๐Ÿ”„ ์ปดํฌ๋„ŒํŠธ ๊ฐ„ ํ†ต์‹ ํ•˜๊ธฐ

์‹ค์ œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ๋Š” ์ปดํฌ๋„ŒํŠธ๋“ค์ด ์„œ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ฃผ๊ณ ๋ฐ›์•„์•ผ ํ•  ๋•Œ๊ฐ€ ๋งŽ์•„. ์›น ์ปดํฌ๋„ŒํŠธ์—์„œ๋Š” ์–ด๋–ป๊ฒŒ ํ†ต์‹ ํ• ๊นŒ? ์—ฌ๋Ÿฌ ๋ฐฉ๋ฒ•์ด ์žˆ์–ด! ๐Ÿ“ก

1๏ธโƒฃ ์†์„ฑ(Attributes)์„ ํ†ตํ•œ ๋ถ€๋ชจ โ†’ ์ž์‹ ํ†ต์‹ 

๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ธ ๋ฐฉ๋ฒ•์ด์•ผ. ๋ถ€๋ชจ๊ฐ€ ์ž์‹ ์ปดํฌ๋„ŒํŠธ์— ์†์„ฑ์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ „๋‹ฌํ•˜๋Š” ๊ฑฐ์ง€.

// ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ์—์„œ
<user-profile 
  name="๊น€๊ฐœ๋ฐœ" 
  role="ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ž"
  level="5">
</user-profile>

// ์ž์‹ ์ปดํฌ๋„ŒํŠธ์—์„œ ๋ฐ›๊ธฐ
class UserProfile extends HTMLElement {
  static get observedAttributes() {
    return ['name', 'role', 'level'];
  }
  
  attributeChangedCallback(name, oldValue, newValue) {
    // ์†์„ฑ์ด ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค ์ž๋™์œผ๋กœ ํ˜ธ์ถœ๋จ
    this.render();
  }
}

2๏ธโƒฃ ์ปค์Šคํ…€ ์ด๋ฒคํŠธ๋ฅผ ํ†ตํ•œ ์ž์‹ โ†’ ๋ถ€๋ชจ ํ†ต์‹ 

์ž์‹ ์ปดํฌ๋„ŒํŠธ์—์„œ ๋ฌด์–ธ๊ฐ€ ์ผ์–ด๋‚ฌ์„ ๋•Œ ๋ถ€๋ชจ์—๊ฒŒ ์•Œ๋ ค์•ผ ํ•  ๋•Œ ์‚ฌ์šฉํ•ด.

// ์ž์‹ ์ปดํฌ๋„ŒํŠธ์—์„œ ์ด๋ฒคํŠธ ๋ฐœ์ƒ
class LikeButton extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }
  
  connectedCallback() {
    this.shadowRoot.innerHTML = `
      <button id="likeBtn">โค๏ธ ์ข‹์•„์š”</button>
    `;
    
    this.shadowRoot.getElementById('likeBtn').addEventListener('click', () => {
      // ์ปค์Šคํ…€ ์ด๋ฒคํŠธ ๋ฐœ์ƒ!
      this.dispatchEvent(new CustomEvent('like-clicked', {
        detail: { 
          timestamp: Date.now(),
          message: '์ข‹์•„์š”๋ฅผ ๋ˆŒ๋ €์–ด์š”!'
        },
        bubbles: true,    // ์ด๋ฒคํŠธ ๋ฒ„๋ธ”๋ง ํ—ˆ์šฉ
        composed: true    // ์„€๋„์šฐ DOM ๊ฒฝ๊ณ„๋ฅผ ๋„˜์–ด ์ „ํŒŒ
      }));
    });
  }
}

customElements.define('like-button', LikeButton);

// ๋ถ€๋ชจ์—์„œ ์ด๋ฒคํŠธ ์ˆ˜์‹ 
const likeBtn = document.querySelector('like-button');
likeBtn.addEventListener('like-clicked', (e) => {
  console.log('์ข‹์•„์š” ์ด๋ฒคํŠธ ๋ฐ›์Œ!', e.detail);
});

๐ŸŽฏ ์ด๋ฒคํŠธ ์˜ต์…˜ ์ดํ•ดํ•˜๊ธฐ

bubbles: true - ์ด๋ฒคํŠธ๊ฐ€ DOM ํŠธ๋ฆฌ๋ฅผ ๋”ฐ๋ผ ์œ„๋กœ ์ „ํŒŒ๋ผ
composed: true - ์„€๋„์šฐ DOM ๊ฒฝ๊ณ„๋ฅผ ๋„˜์–ด์„œ ์ „ํŒŒ๋ผ (์›น ์ปดํฌ๋„ŒํŠธ์—์„œ ์ค‘์š”!)
detail - ์ด๋ฒคํŠธ์™€ ํ•จ๊ป˜ ์ „๋‹ฌํ•  ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ด๋Š” ๊ฐ์ฒด

composed: true๋ฅผ ๋นผ๋จน์œผ๋ฉด ์„€๋„์šฐ DOM ๋ฐ–์—์„œ ์ด๋ฒคํŠธ๋ฅผ ๋ฐ›์„ ์ˆ˜ ์—†์œผ๋‹ˆ ์ฃผ์˜ํ•ด! โš ๏ธ

3๏ธโƒฃ ์ „์—ญ ์ƒํƒœ ๊ด€๋ฆฌ

์—ฌ๋Ÿฌ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๊ฐ™์€ ๋ฐ์ดํ„ฐ๋ฅผ ๊ณต์œ ํ•ด์•ผ ํ•  ๋•Œ๋Š” ์ „์—ญ ์ƒํƒœ ๊ด€๋ฆฌ๊ฐ€ ํ•„์š”ํ•ด. ๊ฐ„๋‹จํ•œ ์ƒํƒœ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค์–ด๋ณด์ž!

// ๊ฐ„๋‹จํ•œ ์ƒํƒœ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ
class StateManager {
  constructor() {
    this.state = {};
    this.listeners = {};
  }
  
  // ์ƒํƒœ ์„ค์ •
  setState(key, value) {
    this.state[key] = value;
    this.notify(key, value);
  }
  
  // ์ƒํƒœ ๊ฐ€์ ธ์˜ค๊ธฐ
  getState(key) {
    return this.state[key];
  }
  
  // ๋ณ€๊ฒฝ ๊ฐ์ง€ ๋“ฑ๋ก
  subscribe(key, callback) {
    if (!this.listeners[key]) {
      this.listeners[key] = [];
    }
    this.listeners[key].push(callback);
    
    // ๊ตฌ๋… ์ทจ์†Œ ํ•จ์ˆ˜ ๋ฐ˜ํ™˜
    return () => {
      this.listeners[key] = this.listeners[key].filter(cb => cb !== callback);
    };
  }
  
  // ๋ฆฌ์Šค๋„ˆ๋“ค์—๊ฒŒ ์•Œ๋ฆผ
  notify(key, value) {
    if (this.listeners[key]) {
      this.listeners[key].forEach(callback => callback(value));
    }
  }
}

// ์ „์—ญ ์ƒํƒœ ๊ด€๋ฆฌ์ž ์ƒ์„ฑ
const appState = new StateManager();

// ์ปดํฌ๋„ŒํŠธ์—์„œ ์‚ฌ์šฉ
class UserCounter extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }
  
  connectedCallback() {
    // ์ดˆ๊ธฐ ๋ Œ๋”๋ง
    this.render();
    
    // ์ƒํƒœ ๋ณ€๊ฒฝ ๊ตฌ๋…
    this.unsubscribe = appState.subscribe('userCount', (count) => {
      this.render();
    });
  }
  
  disconnectedCallback() {
    // ์ปดํฌ๋„ŒํŠธ ์ œ๊ฑฐ ์‹œ ๊ตฌ๋… ์ทจ์†Œ
    if (this.unsubscribe) {
      this.unsubscribe();
    }
  }
  
  render() {
    const count = appState.getState('userCount') || 0;
    this.shadowRoot.innerHTML = `
      <div>ํ˜„์žฌ ์‚ฌ์šฉ์ž: ${count}๋ช…</div>
    `;
  }
}

customElements.define('user-counter', UserCounter);

// ์–ด๋””์„œ๋“  ์ƒํƒœ ์—…๋ฐ์ดํŠธ ๊ฐ€๋Šฅ
appState.setState('userCount', 42);

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์—ฌ๋Ÿฌ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๊ฐ™์€ ๋ฐ์ดํ„ฐ๋ฅผ ๊ณต์œ ํ•˜๊ณ  ๋™๊ธฐํ™”ํ•  ์ˆ˜ ์žˆ์–ด! Redux๋‚˜ Vuex ๊ฐ™์€ ์ƒํƒœ ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ๊ฐ„๋‹จํ•œ ๋ฒ„์ „์ด๋ผ๊ณ  ๋ณด๋ฉด ๋ผ. ๐Ÿ˜Š

๐ŸŽจ ์Šคํƒ€์ผ๋ง ๊ณ ๊ธ‰ ํ…Œํฌ๋‹‰

์›น ์ปดํฌ๋„ŒํŠธ์˜ ์Šคํƒ€์ผ๋ง์€ ์ผ๋ฐ˜ HTML๊ณผ๋Š” ์กฐ๊ธˆ ๋‹ค๋ฅธ ํŠน์„ฑ์ด ์žˆ์–ด. ์„€๋„์šฐ DOM์˜ ์บก์Аํ™”๋ฅผ ํ™œ์šฉํ•˜๋ฉด์„œ๋„ ํ•„์š”ํ•  ๋•Œ๋Š” ์™ธ๋ถ€์™€ ์†Œํ†ตํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•ด. ๊ณ ๊ธ‰ ์Šคํƒ€์ผ๋ง ๊ธฐ๋ฒ•๋“ค์„ ์•Œ์•„๋ณด์ž! ๐ŸŽจ

๐ŸŽฏ CSS ์ปค์Šคํ…€ ํ”„๋กœํผํ‹ฐ (CSS Variables) ํ™œ์šฉ

CSS ๋ณ€์ˆ˜๋Š” ์„€๋„์šฐ DOM ๊ฒฝ๊ณ„๋ฅผ ๋„˜์–ด์„œ ์ „๋‹ฌ๋  ์ˆ˜ ์žˆ์–ด! ์ด๊ฑธ ํ™œ์šฉํ•˜๋ฉด ์ปดํฌ๋„ŒํŠธ์˜ ํ…Œ๋งˆ๋ฅผ ์™ธ๋ถ€์—์„œ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•ํ•  ์ˆ˜ ์žˆ์–ด.

class ThemedButton extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }
  
  connectedCallback() {
    this.shadowRoot.innerHTML = `
      <style>.tx-header{background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);
color:#fff;
padding:40px 30px;
border-radius:16px;
text-align:center;
margin-bottom:40px;
box-shadow:0 8px 24px rgba(102,126,234,0.25);}
.tx-intro{background:#f8f9fa;
padding:25px;
border-radius:12px;
margin:30px 0;
border-left:5px solid #667eea;}
.tx-section{margin:40px 0;}
.tx-highlight{background:linear-gradient(120deg,#fef6e4 0%,#f3e7ff 100%);
padding:20px;
border-radius:10px;
margin:25px 0;}
.tx-tip{background:#e8f5e9;
padding:18px;
border-radius:10px;
margin:20px 0;
border-left:4px solid #4caf50;}
.tx-warning{background:#fff3e0;
padding:18px;
border-radius:10px;
margin:20px 0;
border-left:4px solid #ff9800;}
.tx-point{background:#e3f2fd;
padding:18px;
border-radius:10px;
margin:20px 0;
border-left:4px solid #2196f3;}
.tx-table-wrap{overflow-x:auto;
margin:25px 0;}
.tx-table{width:100%;
border-collapse:collapse;
background:#fff;
border-radius:8px;
overflow:hidden;
box-shadow:0 2px 8px rgba(0,0,0,0.08);}
.tx-table th{background:#667eea;
color:#fff;
padding:12px;
text-align:left;
font-weight:600;}
.tx-table td{padding:12px;
border-bottom:1px solid #e0e0e0;}
.tx-table tr:last-child td{border-bottom:none;}
.tx-svg-container{text-align:center;
margin:40px 0;}
.tx-concept-box{background:#fff;
padding:20px;
border-radius:12px;
margin:20px 0;
box-shadow:0 4px 12px rgba(0,0,0,0.08);}
.tx-step{background:linear-gradient(135deg,#ffecd2 0%,#fcb69f 100%);
padding:15px 20px;
border-radius:10px;
margin:15px 0;}
.tx-benefit{background:linear-gradient(135deg,#a8edea 0%,#fed6e3 100%);
padding:15px 20px;
border-radius:10px;
margin:15px 0;}</style>
      
      <button>
        <slot>๋ฒ„ํŠผ</slot>
      </button>
    `;
  }
}

customElements.define('themed-button', ThemedButton);

// HTML/CSS์—์„œ ํ…Œ๋งˆ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•
// <style>
//   .primary-theme {
//     --button-bg: #667eea;
//     --button-hover-bg: #5568d3;
//   }
//   
//   .danger-theme {
//     --button-bg: #f44336;
//     --button-hover-bg: #d32f2f;
//   }
//   
//   .custom-theme {
//     --button-bg: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
//     --button-padding: 15px 30px;
//     --button-radius: 25px;
//   }
// </style>
//
// <themed-button class="primary-theme">๊ธฐ๋ณธ ๋ฒ„ํŠผ</themed-button>
// <themed-button class="danger-theme">์‚ญ์ œ</themed-button>
// <themed-button class="custom-theme">ํŠน๋ณ„ํ•œ ๋ฒ„ํŠผ</themed-button>

๐Ÿ’ก :host ์„ ํƒ์ž ํ™œ์šฉํ•˜๊ธฐ

:host๋Š” ์ปดํฌ๋„ŒํŠธ ์ž๊ธฐ ์ž์‹ ์„ ์„ ํƒํ•˜๋Š” ํŠน๋ณ„ํ•œ ์„ ํƒ์ž์•ผ. ์™ธ๋ถ€์—์„œ ์ ์šฉ๋œ ํด๋ž˜์Šค๋‚˜ ์†์„ฑ์— ๋”ฐ๋ผ ์Šคํƒ€์ผ์„ ๋‹ค๋ฅด๊ฒŒ ํ•  ์ˆ˜ ์žˆ์–ด!

class SmartCard extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }
  
  connectedCallback() {
    this.shadowRoot.innerHTML = `
      <style>.tx-header{background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);
color:#fff;
padding:40px 30px;
border-radius:16px;
text-align:center;
margin-bottom:40px;
box-shadow:0 8px 24px rgba(102,126,234,0.25);}
.tx-intro{background:#f8f9fa;
padding:25px;
border-radius:12px;
margin:30px 0;
border-left:5px solid #667eea;}
.tx-section{margin:40px 0;}
.tx-highlight{background:linear-gradient(120deg,#fef6e4 0%,#f3e7ff 100%);
padding:20px;
border-radius:10px;
margin:25px 0;}
.tx-tip{background:#e8f5e9;
padding:18px;
border-radius:10px;
margin:20px 0;
border-left:4px solid #4caf50;}
.tx-warning{background:#fff3e0;
padding:18px;
border-radius:10px;
margin:20px 0;
border-left:4px solid #ff9800;}
.tx-point{background:#e3f2fd;
padding:18px;
border-radius:10px;
margin:20px 0;
border-left:4px solid #2196f3;}
.tx-table-wrap{overflow-x:auto;
margin:25px 0;}
.tx-table{width:100%;
border-collapse:collapse;
background:#fff;
border-radius:8px;
overflow:hidden;
box-shadow:0 2px 8px rgba(0,0,0,0.08);}
.tx-table th{background:#667eea;
color:#fff;
padding:12px;
text-align:left;
font-weight:600;}
.tx-table td{padding:12px;
border-bottom:1px solid #e0e0e0;}
.tx-table tr:last-child td{border-bottom:none;}
.tx-svg-container{text-align:center;
margin:40px 0;}
.tx-concept-box{background:#fff;
padding:20px;
border-radius:12px;
margin:20px 0;
box-shadow:0 4px 12px rgba(0,0,0,0.08);}
.tx-step{background:linear-gradient(135deg,#ffecd2 0%,#fcb69f 100%);
padding:15px 20px;
border-radius:10px;
margin:15px 0;}
.tx-benefit{background:linear-gradient(135deg,#a8edea 0%,#fed6e3 100%);
padding:15px 20px;
border-radius:10px;
margin:15px 0;}</style>
      
      <slot></slot>
    `;
  }
}

customElements.define('smart-card', SmartCard);

๐ŸŽจ ::part์™€ ::slotted ํ™œ์šฉ

๋” ์„ธ๋ฐ€ํ•œ ์Šคํƒ€์ผ ์ œ์–ด๊ฐ€ ํ•„์š”ํ•  ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ๊ณ ๊ธ‰ ๊ธฐ๋ฒ•์ด์•ผ!

class AdvancedCard extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }
  
  connectedCallback() {
    this.shadowRoot.innerHTML = `
      <style>.tx-header{background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);
color:#fff;
padding:40px 30px;
border-radius:16px;
text-align:center;
margin-bottom:40px;
box-shadow:0 8px 24px rgba(102,126,234,0.25);}
.tx-intro{background:#f8f9fa;
padding:25px;
border-radius:12px;
margin:30px 0;
border-left:5px solid #667eea;}
.tx-section{margin:40px 0;}
.tx-highlight{background:linear-gradient(120deg,#fef6e4 0%,#f3e7ff 100%);
padding:20px;
border-radius:10px;
margin:25px 0;}
.tx-tip{background:#e8f5e9;
padding:18px;
border-radius:10px;
margin:20px 0;
border-left:4px solid #4caf50;}
.tx-warning{background:#fff3e0;
padding:18px;
border-radius:10px;
margin:20px 0;
border-left:4px solid #ff9800;}
.tx-point{background:#e3f2fd;
padding:18px;
border-radius:10px;
margin:20px 0;
border-left:4px solid #2196f3;}
.tx-table-wrap{overflow-x:auto;
margin:25px 0;}
.tx-table{width:100%;
border-collapse:collapse;
background:#fff;
border-radius:8px;
overflow:hidden;
box-shadow:0 2px 8px rgba(0,0,0,0.08);}
.tx-table th{background:#667eea;
color:#fff;
padding:12px;
text-align:left;
font-weight:600;}
.tx-table td{padding:12px;
border-bottom:1px solid #e0e0e0;}
.tx-table tr:last-child td{border-bottom:none;}
.tx-svg-container{text-align:center;
margin:40px 0;}
.tx-concept-box{background:#fff;
padding:20px;
border-radius:12px;
margin:20px 0;
box-shadow:0 4px 12px rgba(0,0,0,0.08);}
.tx-step{background:linear-gradient(135deg,#ffecd2 0%,#fcb69f 100%);
padding:15px 20px;
border-radius:10px;
margin:15px 0;}
.tx-benefit{background:linear-gradient(135deg,#a8edea 0%,#fed6e3 100%);
padding:15px 20px;
border-radius:10px;
margin:15px 0;}</style>
      
      <div class="card">
        <div class="header" part="header">
          <slot name="header"></slot>
        </div>
        <div class="content" part="content">
          <slot></slot>
        </div>
      </div>
    `;
  }
}

customElements.define('advanced-card', AdvancedCard);

// ์™ธ๋ถ€ CSS์—์„œ part ์Šคํƒ€์ผ๋ง
// advanced-card::part(header) {
//   background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
//   color: white;
// }

โšก ์„ฑ๋Šฅ ์ตœ์ ํ™” ํŒ

์›น ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค ๋•Œ ์„ฑ๋Šฅ๋„ ์ค‘์š”ํ•ด! ํŠนํžˆ ๋งŽ์€ ์ˆ˜์˜ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ๋Š” ๋”์šฑ ๊ทธ๋ ‡์ง€. ์‹ค์ „์—์„œ ๋ฐ”๋กœ ์“ธ ์ˆ˜ ์žˆ๋Š” ์ตœ์ ํ™” ํŒ๋“ค์„ ์•Œ๋ ค์ค„๊ฒŒ! ๐Ÿš€

1๏ธโƒฃ ๋ถˆํ•„์š”ํ•œ ๋ Œ๋”๋ง ๋ฐฉ์ง€

์†์„ฑ์ด ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค ์ „์ฒด๋ฅผ ๋‹ค์‹œ ๋ Œ๋”๋งํ•˜๋Š” ๊ฑด ๋น„ํšจ์œจ์ ์ด์•ผ. ๋ณ€๊ฒฝ๋œ ๋ถ€๋ถ„๋งŒ ์—…๋ฐ์ดํŠธํ•˜์ž!

class OptimizedComponent extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this._initialized = false;
  }
  
  static get observedAttributes() {
    return ['title', 'count'];
  }
  
  connectedCallback() {
    if (!this._initialized) {
      this.render();
      this._initialized = true;
    }
  }
  
  attributeChangedCallback(name, oldValue, newValue) {
    // ์ดˆ๊ธฐํ™” ์ „์ด๊ฑฐ๋‚˜ ๊ฐ’์ด ์‹ค์ œ๋กœ ๋ณ€๊ฒฝ๋˜์ง€ ์•Š์•˜์œผ๋ฉด ๋ฌด์‹œ
    if (!this._initialized || oldValue === newValue) return;
    
    // ๋ณ€๊ฒฝ๋œ ๋ถ€๋ถ„๋งŒ ์—…๋ฐ์ดํŠธ
    switch(name) {
      case 'title':
        this.updateTitle(newValue);
        break;
      case 'count':
        this.updateCount(newValue);
        break;
    }
  }
  
  render() {
    this.shadowRoot.innerHTML = `
      <style>.tx-header{background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);
color:#fff;
padding:40px 30px;
border-radius:16px;
text-align:center;
margin-bottom:40px;
box-shadow:0 8px 24px rgba(102,126,234,0.25);}
.tx-intro{background:#f8f9fa;
padding:25px;
border-radius:12px;
margin:30px 0;
border-left:5px solid #667eea;}
.tx-section{margin:40px 0;}
.tx-highlight{background:linear-gradient(120deg,#fef6e4 0%,#f3e7ff 100%);
padding:20px;
border-radius:10px;
margin:25px 0;}
.tx-tip{background:#e8f5e9;
padding:18px;
border-radius:10px;
margin:20px 0;
border-left:4px solid #4caf50;}
.tx-warning{background:#fff3e0;
padding:18px;
border-radius:10px;
margin:20px 0;
border-left:4px solid #ff9800;}
.tx-point{background:#e3f2fd;
padding:18px;
border-radius:10px;
margin:20px 0;
border-left:4px solid #2196f3;}
.tx-table-wrap{overflow-x:auto;
margin:25px 0;}
.tx-table{width:100%;
border-collapse:collapse;
background:#fff;
border-radius:8px;
overflow:hidden;
box-shadow:0 2px 8px rgba(0,0,0,0.08);}
.tx-table th{background:#667eea;
color:#fff;
padding:12px;
text-align:left;
font-weight:600;}
.tx-table td{padding:12px;
border-bottom:1px solid #e0e0e0;}
.tx-table tr:last-child td{border-bottom:none;}
.tx-svg-container{text-align:center;
margin:40px 0;}
.tx-concept-box{background:#fff;
padding:20px;
border-radius:12px;
margin:20px 0;
box-shadow:0 4px 12px rgba(0,0,0,0.08);}
.tx-step{background:linear-gradient(135deg,#ffecd2 0%,#fcb69f 100%);
padding:15px 20px;
border-radius:10px;
margin:15px 0;}
.tx-benefit{background:linear-gradient(135deg,#a8edea 0%,#fed6e3 100%);
padding:15px 20px;
border-radius:10px;
margin:15px 0;}</style>
      
      <div class="container">
        <div class="title">${this.getAttribute('title') || '์ œ๋ชฉ'}</div>
        <div class="count">์นด์šดํŠธ: ${this.getAttribute('count') || '0'}</div>
      </div>
    `;
  }
  
  // ๋ถ€๋ถ„ ์—…๋ฐ์ดํŠธ ๋ฉ”์„œ๋“œ
  updateTitle(title) {
    const titleEl = this.shadowRoot.querySelector('.title');
    if (titleEl) titleEl.textContent = title;
  }
  
  updateCount(count) {
    const countEl = this.shadowRoot.querySelector('.count');
    if (countEl) countEl.textContent = `์นด์šดํŠธ: ${count}`;
  }
}

customElements.define('optimized-component', OptimizedComponent);

2๏ธโƒฃ ํ…œํ”Œ๋ฆฟ ์žฌ์‚ฌ์šฉ๊ณผ ์บ์‹ฑ

๊ฐ™์€ ํ…œํ”Œ๋ฆฟ์„ ์—ฌ๋Ÿฌ ๋ฒˆ ํŒŒ์‹ฑํ•˜์ง€ ๋ง๊ณ  ํ•œ ๋ฒˆ๋งŒ ํŒŒ์‹ฑํ•ด์„œ ์žฌ์‚ฌ์šฉํ•˜์ž!

// ํ…œํ”Œ๋ฆฟ ์บ์‹ฑ ์œ ํ‹ธ๋ฆฌํ‹ฐ
class TemplateCache {
  constructor() {
    this.cache = new Map();
  }
  
  get(id) {
    if (!this.cache.has(id)) {
      const template = document.getElementById(id);
      if (template) {
        this.cache.set(id, template.content.cloneNode(true));
      }
    }
    return this.cache.get(id)?.cloneNode(true);
  }
}

const templateCache = new TemplateCache();

class CachedComponent extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }
  
  connectedCallback() {
    // ์บ์‹œ๋œ ํ…œํ”Œ๋ฆฟ ์‚ฌ์šฉ
    const template = templateCache.get('my-template');
    if (template) {
      this.shadowRoot.appendChild(template);
    }
  }
}

customElements.define('cached-component', CachedComponent);

3๏ธโƒฃ ์ง€์—ฐ ๋กœ๋”ฉ (Lazy Loading)

๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ฒ˜์Œ๋ถ€ํ„ฐ ๋กœ๋“œํ•  ํ•„์š”๋Š” ์—†์–ด. ํ•„์š”ํ•  ๋•Œ๋งŒ ๋กœ๋“œํ•˜์ž!

// ๋™์  ์ปดํฌ๋„ŒํŠธ ๋กœ๋”ฉ
class LazyLoader {
  static async loadComponent(name, path) {
    // ์ด๋ฏธ ์ •์˜๋˜์–ด ์žˆ์œผ๋ฉด ์Šคํ‚ต
    if (customElements.get(name)) {
      return;
    }
    
    try {
      // ๋™์ ์œผ๋กœ ๋ชจ๋“ˆ import
      await import(path);
      console.log(`${name} ์ปดํฌ๋„ŒํŠธ ๋กœ๋“œ ์™„๋ฃŒ!`);
    } catch (error) {
      console.error(`${name} ๋กœ๋“œ ์‹คํŒจ:`, error);
    }
  }
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
document.addEventListener('DOMContentLoaded', async () => {
  // ๋ทฐํฌํŠธ์— ๋“ค์–ด์˜ฌ ๋•Œ ๋กœ๋“œ
  const observer = new IntersectionObserver((entries) => {
    entries.forEach(async (entry) => {
      if (entry.isIntersecting) {
        const componentName = entry.target.tagName.toLowerCase();
        const componentPath = `./components/${componentName}.js`;
        
        await LazyLoader.loadComponent(componentName, componentPath);
        observer.unobserve(entry.target);
      }
    });
  });
  
  // ์•„์ง ์ •์˜๋˜์ง€ ์•Š์€ ์ปค์Šคํ…€ ์—˜๋ฆฌ๋จผํŠธ ๊ด€์ฐฐ
  document.querySelectorAll(':not(:defined)').forEach(el => {
    observer.observe(el);
  });
});

4๏ธโƒฃ ์ด๋ฒคํŠธ ์œ„์ž„ ํ™œ์šฉ

๋งŽ์€ ์ž์‹ ์š”์†Œ์— ๊ฐ๊ฐ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ๋ฅผ ๋‹ฌ์ง€ ๋ง๊ณ , ๋ถ€๋ชจ์— ํ•˜๋‚˜๋งŒ ๋‹ฌ์•„์„œ ์ฒ˜๋ฆฌํ•˜์ž!

class ListComponent extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }
  
  connectedCallback() {
    this.render();
    
    // ๊ฐ ๋ฒ„ํŠผ์— ๋ฆฌ์Šค๋„ˆ๋ฅผ ๋‹ค๋Š” ๋Œ€์‹  ์ปจํ…Œ์ด๋„ˆ์— ํ•˜๋‚˜๋งŒ!
    this.shadowRoot.querySelector('.list').addEventListener('click', (e) => {
      // ์ด๋ฒคํŠธ ์œ„์ž„์œผ๋กœ ์ฒ˜๋ฆฌ
      if (e.target.classList.contains('delete-btn')) {
        const id = e.target.dataset.id;
        this.deleteItem(id);
      } else if (e.target.classList.contains('edit-btn')) {
        const id = e.target.dataset.id;
        this.editItem(id);
      }
    });
  }
  
  render() {
    const items = this.getItems(); // ์•„์ดํ…œ ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ
    
    this.shadowRoot.innerHTML = `
      <style>.tx-header{background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);
color:#fff;
padding:40px 30px;
border-radius:16px;
text-align:center;
margin-bottom:40px;
box-shadow:0 8px 24px rgba(102,126,234,0.25);}
.tx-intro{background:#f8f9fa;
padding:25px;
border-radius:12px;
margin:30px 0;
border-left:5px solid #667eea;}
.tx-section{margin:40px 0;}
.tx-highlight{background:linear-gradient(120deg,#fef6e4 0%,#f3e7ff 100%);
padding:20px;
border-radius:10px;
margin:25px 0;}
.tx-tip{background:#e8f5e9;
padding:18px;
border-radius:10px;
margin:20px 0;
border-left:4px solid #4caf50;}
.tx-warning{background:#fff3e0;
padding:18px;
border-radius:10px;
margin:20px 0;
border-left:4px solid #ff9800;}
.tx-point{background:#e3f2fd;
padding:18px;
border-radius:10px;
margin:20px 0;
border-left:4px solid #2196f3;}
.tx-table-wrap{overflow-x:auto;
margin:25px 0;}
.tx-table{width:100%;
border-collapse:collapse;
background:#fff;
border-radius:8px;
overflow:hidden;
box-shadow:0 2px 8px rgba(0,0,0,0.08);}
.tx-table th{background:#667eea;
color:#fff;
padding:12px;
text-align:left;
font-weight:600;}
.tx-table td{padding:12px;
border-bottom:1px solid #e0e0e0;}
.tx-table tr:last-child td{border-bottom:none;}
.tx-svg-container{text-align:center;
margin:40px 0;}
.tx-concept-box{background:#fff;
padding:20px;
border-radius:12px;
margin:20px 0;
box-shadow:0 4px 12px rgba(0,0,0,0.08);}
.tx-step{background:linear-gradient(135deg,#ffecd2 0%,#fcb69f 100%);
padding:15px 20px;
border-radius:10px;
margin:15px 0;}
.tx-benefit{background:linear-gradient(135deg,#a8edea 0%,#fed6e3 100%);
padding:15px 20px;
border-radius:10px;
margin:15px 0;}</style>
      
      <div class="list">
        ${items.map(item => `
          <div class="item">
            ${item.name}
            <button class="edit-btn" data-id="${item.id}">์ˆ˜์ •</button>
            <button class="delete-btn" data-id="${item.id}">์‚ญ์ œ</button>
          </div>
        `).join('')}
      </div>
    `;
  }
  
  getItems() {
    // ์‹ค์ œ๋กœ๋Š” ๋ฐ์ดํ„ฐ ์†Œ์Šค์—์„œ ๊ฐ€์ ธ์˜ด
    return [
      { id: 1, name: '์•„์ดํ…œ 1' },
      { id: 2, name: '์•„์ดํ…œ 2' },
      { id: 3, name: '์•„์ดํ…œ 3' }
    ];
  }
  
  deleteItem(id) {
    console.log(`์•„์ดํ…œ ${id} ์‚ญ์ œ`);
    // ์‚ญ์ œ ๋กœ์ง...
  }
  
  editItem(id) {
    console.log(`์•„์ดํ…œ ${id} ์ˆ˜์ •`);
    // ์ˆ˜์ • ๋กœ์ง...
  }
}

customElements.define('list-component', ListComponent);

โš ๏ธ ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ์ฃผ์˜!

disconnectedCallback์—์„œ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ๋ฅผ ์ œ๊ฑฐํ•˜๊ณ , ํƒ€์ด๋จธ๋ฅผ ์ •๋ฆฌํ•˜๊ณ , ๊ตฌ๋…์„ ์ทจ์†Œํ•˜๋Š” ๊ฑธ ์žŠ์ง€ ๋งˆ! ์ด๊ฑฐ ์•ˆ ํ•˜๋ฉด ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์–ด.

class ProperCleanup extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.intervalId = null;
    this.resizeHandler = null;
  }
  
  connectedCallback() {
    this.render();
    
    // ๋ฆฌ์‚ฌ์ด์ฆˆ ํ•ธ๋“ค๋Ÿฌ ๋“ฑ๋ก
    this.resizeHandler = () => this.handleResize();
    window.addEventListener('resize', this.resizeHandler);
    
    // ์ธํ„ฐ๋ฒŒ ์‹œ์ž‘
    this.intervalId = setInterval(() => {
      this.updateTime();
    }, 1000);
  }
  
  disconnectedCallback() {
    // ์ •๋ฆฌ ์ž‘์—… ํ•„์ˆ˜!
    if (this.resizeHandler) {
      window.removeEventListener('resize', this.resizeHandler);
    }
    
    if (this.intervalId) {
      clearInterval(this.intervalId);
    }
    
    console.log('์ปดํฌ๋„ŒํŠธ ์ •๋ฆฌ ์™„๋ฃŒ!');
  }
  
  handleResize() {
    // ๋ฆฌ์‚ฌ์ด์ฆˆ ์ฒ˜๋ฆฌ...
  }
  
  updateTime() {
    // ์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ...
  }
  
  render() {
    this.shadowRoot.innerHTML = `
      <div>์‹œ๊ฐ„: <span id="time"></span></div>
    `;
  }
}

customElements.define('proper-cleanup', ProperCleanup);

๐Ÿ”ง ์‹ค์ „ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ

์ด๋ก ์€ ์ถฉ๋ถ„ํžˆ ๋ฐฐ์› ์œผ๋‹ˆ ์ด์ œ ์‹ค์ „ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ๋ฅผ ์•Œ์•„๋ณด์ž! ์›น ์ปดํฌ๋„ŒํŠธ๋ฅผ ์‚ฌ์šฉํ•œ ํ”„๋กœ์ ํŠธ๋Š” ์–ด๋–ป๊ฒŒ ๊ตฌ์„ฑํ•˜๋ฉด ์ข‹์„๊นŒ? ๐Ÿ“

๐Ÿ“‚ ์ถ”์ฒœ ํด๋” ๊ตฌ์กฐ

my-web-components-project/
โ”œโ”€โ”€ src/
โ”‚   โ”œโ”€โ”€ components/          # ์ปดํฌ๋„ŒํŠธ ํŒŒ์ผ๋“ค
โ”‚   โ”‚   โ”œโ”€โ”€ base/           # ๊ธฐ๋ณธ ์ปดํฌ๋„ŒํŠธ
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ Button.js
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ Input.js
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ Card.js
โ”‚   โ”‚   โ”œโ”€โ”€ layout/         # ๋ ˆ์ด์•„์›ƒ ์ปดํฌ๋„ŒํŠธ
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ Header.js
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ Footer.js
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ Sidebar.js
โ”‚   โ”‚   โ””โ”€โ”€ features/       # ๊ธฐ๋Šฅ๋ณ„ ์ปดํฌ๋„ŒํŠธ
โ”‚   โ”‚       โ”œโ”€โ”€ UserProfile.js
โ”‚   โ”‚       โ”œโ”€โ”€ ProductList.js
โ”‚   โ”‚       โ””โ”€โ”€ ShoppingCart.js
โ”‚   โ”œโ”€โ”€ utils/              # ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜
โ”‚   โ”‚   โ”œโ”€โ”€ state-manager.js
โ”‚   โ”‚   โ”œโ”€โ”€ template-cache.js
โ”‚   โ”‚   โ””โ”€โ”€ helpers.js
โ”‚   โ”œโ”€โ”€ styles/             # ๊ณตํ†ต ์Šคํƒ€์ผ
โ”‚   โ”‚   โ”œโ”€โ”€ variables.css   # CSS ๋ณ€์ˆ˜
โ”‚   โ”‚   โ””โ”€โ”€ themes.css      # ํ…Œ๋งˆ ์ •์˜
โ”‚   โ””โ”€โ”€ index.js            # ์ง„์ž…์ 
โ”œโ”€โ”€ public/
โ”‚   โ”œโ”€โ”€ index.html
โ”‚   โ””โ”€โ”€ assets/
โ””โ”€โ”€ package.json

๐ŸŽฏ ์ปดํฌ๋„ŒํŠธ ์ž‘์„ฑ ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค

์‹ค์ „์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์™„์„ฑ๋„ ๋†’์€ ์ปดํฌ๋„ŒํŠธ ํ…œํ”Œ๋ฆฟ์„ ๋งŒ๋“ค์–ด๋ดค์–ด!

// components/base/Button.js

/**
 * ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋ฒ„ํŠผ ์ปดํฌ๋„ŒํŠธ
 * 
 * @element custom-button
 * 
 * @attr {string} variant - ๋ฒ„ํŠผ ์Šคํƒ€์ผ (primary|secondary|danger)
 * @attr {string} size - ๋ฒ„ํŠผ ํฌ๊ธฐ (small|medium|large)
 * @attr {boolean} disabled - ๋น„ํ™œ์„ฑํ™” ์ƒํƒœ
 * @attr {boolean} loading - ๋กœ๋”ฉ ์ƒํƒœ
 * 
 * @fires button-click - ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ๋ฐœ์ƒ
 * 
 * @example
 * <custom-button variant="primary" size="large">
 *   ํด๋ฆญํ•˜์„ธ์š”
 * </custom-button>
 */
class CustomButton extends HTMLElement {
  // ๊ด€์ฐฐํ•  ์†์„ฑ ์ •์˜
  static get observedAttributes() {
    return ['variant', 'size', 'disabled', 'loading'];
  }
  
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    
    // ๋‚ด๋ถ€ ์ƒํƒœ
    this._variant = 'primary';
    this._size = 'medium';
    this._disabled = false;
    this._loading = false;
  }
  
  // ์†์„ฑ getter/setter
  get variant() { return this._variant; }
  set variant(value) {
    this.setAttribute('variant', value);
  }
  
  get size() { return this._size; }
  set size(value) {
    this.setAttribute('size', value);
  }
  
  get disabled() { return this._disabled; }
  set disabled(value) {
    if (value) {
      this.setAttribute('disabled', '');
    } else {
      this.removeAttribute('disabled');
    }
  }
  
  get loading() { return this._loading; }
  set loading(value) {
    if (value) {
      this.setAttribute('loading', '');
    } else {
      this.removeAttribute('loading');
    }
  }
  
  connectedCallback() {
    this.render();
    this.attachEventListeners();
  }
  
  disconnectedCallback() {
    this.removeEventListeners();
  }
  
  attributeChangedCallback(name, oldValue, newValue) {
    if (oldValue === newValue) return;
    
    switch(name) {
      case 'variant':
        this._variant = newValue || 'primary';
        break;
      case 'size':
        this._size = newValue || 'medium';
        break;
      case 'disabled':
        this._disabled = newValue !== null;
        break;
      case 'loading':
        this._loading = newValue !== null;
        break;
    }
    
    this.updateButton();
  }
  
  render() {
    this.shadowRoot.innerHTML = `
      <style>.tx-header{background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);
color:#fff;
padding:40px 30px;
border-radius:16px;
text-align:center;
margin-bottom:40px;
box-shadow:0 8px 24px rgba(102,126,234,0.25);}
.tx-intro{background:#f8f9fa;
padding:25px;
border-radius:12px;
margin:30px 0;
border-left:5px solid #667eea;}
.tx-section{margin:40px 0;}
.tx-highlight{background:linear-gradient(120deg,#fef6e4 0%,#f3e7ff 100%);
padding:20px;
border-radius:10px;
margin:25px 0;}
.tx-tip{background:#e8f5e9;
padding:18px;
border-radius:10px;
margin:20px 0;
border-left:4px solid #4caf50;}
.tx-warning{background:#fff3e0;
padding:18px;
border-radius:10px;
margin:20px 0;
border-left:4px solid #ff9800;}
.tx-point{background:#e3f2fd;
padding:18px;
border-radius:10px;
margin:20px 0;
border-left:4px solid #2196f3;}
.tx-table-wrap{overflow-x:auto;
margin:25px 0;}
.tx-table{width:100%;
border-collapse:collapse;
background:#fff;
border-radius:8px;
overflow:hidden;
box-shadow:0 2px 8px rgba(0,0,0,0.08);}
.tx-table th{background:#667eea;
color:#fff;
padding:12px;
text-align:left;
font-weight:600;}
.tx-table td{padding:12px;
border-bottom:1px solid #e0e0e0;}
.tx-table tr:last-child td{border-bottom:none;}
.tx-svg-container{text-align:center;
margin:40px 0;}
.tx-concept-box{background:#fff;
padding:20px;
border-radius:12px;
margin:20px 0;
box-shadow:0 4px 12px rgba(0,0,0,0.08);}
.tx-step{background:linear-gradient(135deg,#ffecd2 0%,#fcb69f 100%);
padding:15px 20px;
border-radius:10px;
margin:15px 0;}
.tx-benefit{background:linear-gradient(135deg,#a8edea 0%,#fed6e3 100%);
padding:15px 20px;
border-radius:10px;
margin:15px 0;}</style>
      
      <button 
        class="${this._variant} ${this._size} ${this._loading ? 'loading' : ''}"
        ?disabled="${this._disabled || this._loading}"
      >
        ${this._loading ? '<span class="spinner"></span>' : ''}
        <slot></slot>
      </button>
    `;
  }
  
  updateButton() {
    const button = this.shadowRoot.querySelector('button');
    if (!button) return;
    
    // ํด๋ž˜์Šค ์—…๋ฐ์ดํŠธ
    button.className = `${this._variant} ${this._size} ${this._loading ? 'loading' : ''}`;
    button.disabled = this._disabled || this._loading;
    
    // ๋กœ๋”ฉ ์Šคํ”ผ๋„ˆ ์—…๋ฐ์ดํŠธ
    const existingSpinner = button.querySelector('.spinner');
    if (this._loading && !existingSpinner) {
      const spinner = document.createElement('span');
      spinner.className = 'spinner';
      button.insertBefore(spinner, button.firstChild);
    } else if (!this._loading && existingSpinner) {
      existingSpinner.remove();
    }
  }
  
  attachEventListeners() {
    this._handleClick = this.handleClick.bind(this);
    this.shadowRoot.querySelector('button')
      ?.addEventListener('click', this._handleClick);
  }
  
  removeEventListeners() {
    this.shadowRoot.querySelector('button')
      ?.removeEventListener('click', this._handleClick);
  }
  
  handleClick(e) {
    if (this._disabled || this._loading) {
      e.preventDefault();
      e.stopPropagation();
      return;
    }
    
    // ์ปค์Šคํ…€ ์ด๋ฒคํŠธ ๋ฐœ์ƒ
    this.dispatchEvent(new CustomEvent('button-click', {
      detail: {
        variant: this._variant,
        size: this._size,
        timestamp: Date.now()
      },
      bubbles: true,
      composed: true
    }));
  }
}

// ์ปดํฌ๋„ŒํŠธ ๋“ฑ๋ก
customElements.define('custom-button', CustomButton);

// ๋ชจ๋“ˆ๋กœ export
export default CustomButton;

์ด ๋ฒ„ํŠผ ์ปดํฌ๋„ŒํŠธ๋Š” ์‹ค์ „์—์„œ ๋ฐ”๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์„ ์ •๋„๋กœ ์™„์„ฑ๋„๊ฐ€ ๋†’์•„! ๋‹ค์–‘ํ•œ ๋ณ€ํ˜•, ์ƒํƒœ ๊ด€๋ฆฌ, ์ ‘๊ทผ์„ฑ, ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ๊นŒ์ง€ ๋ชจ๋‘ ๊ณ ๋ คํ–ˆ์–ด. ๐Ÿ˜Š

๐ŸŒ ํ”„๋ ˆ์ž„์›Œํฌ์™€์˜ ํ†ตํ•ฉ

์›น ์ปดํฌ๋„ŒํŠธ์˜ ๊ฐ€์žฅ ํฐ ์žฅ์  ์ค‘ ํ•˜๋‚˜๋Š” ํ”„๋ ˆ์ž„์›Œํฌ์— ๋…๋ฆฝ์ ์ด๋ผ๋Š” ๊ฑฐ์•ผ! React, Vue, Angular ์–ด๋””์„œ๋“  ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด. ๊ฐ ํ”„๋ ˆ์ž„์›Œํฌ์—์„œ ์–ด๋–ป๊ฒŒ ์‚ฌ์šฉํ•˜๋Š”์ง€ ์•Œ์•„๋ณด์ž! ๐Ÿ”—

โš›๏ธ React์—์„œ ์›น ์ปดํฌ๋„ŒํŠธ ์‚ฌ์šฉํ•˜๊ธฐ

// React ์ปดํฌ๋„ŒํŠธ์—์„œ ์›น ์ปดํฌ๋„ŒํŠธ ์‚ฌ์šฉ
import React, { useRef, useEffect } from 'react';
import './components/CustomButton'; // ์›น ์ปดํฌ๋„ŒํŠธ import

function App() {
  const buttonRef = useRef(null);
  
  useEffect(() => {
    const button = buttonRef.current;
    
    // ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ๋“ฑ๋ก
    const handleButtonClick = (e) => {
      console.log('๋ฒ„ํŠผ ํด๋ฆญ!', e.detail);
    };
    
    button?.addEventListener('button-click', handleButtonClick);
    
    // ์ •๋ฆฌ
    return () => {
      button?.removeEventListener('button-click', handleButtonClick);
    };
  }, []);
  
  const handleLoadingToggle = () => {
    const button = buttonRef.current;
    button.loading = !button.loading;
  };
  
  return (
    <div className="App">
      <h1>React + ์›น ์ปดํฌ๋„ŒํŠธ</h1>
      
      {/* ์›น ์ปดํฌ๋„ŒํŠธ ์‚ฌ์šฉ */}
      <custom-button 
        ref={buttonRef}
        variant="primary"
        size="large"
      >
        ํด๋ฆญํ•˜์„ธ์š”
      </custom-button>
      
      <button onClick={handleLoadingToggle}>
        ๋กœ๋”ฉ ํ† ๊ธ€
      </button>
    </div>
  );
}

export default App;

๐Ÿ’ก React์—์„œ ์ฃผ์˜ํ•  ์ 

React๋Š” ์›น ์ปดํฌ๋„ŒํŠธ์˜ ์†์„ฑ์„ ๋ฌธ์ž์—ด๋กœ๋งŒ ์ „๋‹ฌํ•ด. ๊ฐ์ฒด๋‚˜ ๋ฐฐ์—ด์„ ์ „๋‹ฌํ•˜๋ ค๋ฉด ref๋ฅผ ์‚ฌ์šฉํ•ด์„œ ํ”„๋กœํผํ‹ฐ๋กœ ์ง์ ‘ ์„ค์ •ํ•ด์•ผ ํ•ด!

๐Ÿ’š Vue์—์„œ ์›น ์ปดํฌ๋„ŒํŠธ ์‚ฌ์šฉํ•˜๊ธฐ

<!-- Vue ์ปดํฌ๋„ŒํŠธ -->
<template>
  <div class="app">
    <h1>Vue + ์›น ์ปดํฌ๋„ŒํŠธ</h1>
    
    <!-- ์›น ์ปดํฌ๋„ŒํŠธ ์‚ฌ์šฉ -->
    <custom-button
      ref="customButton"
      :variant="buttonVariant"
      :size="buttonSize"
      :loading="isLoading"
      @button-click="handleButtonClick"
    >
      {{ buttonText }}
    </custom-button>
    
    <button @click="toggleLoading">
      ๋กœ๋”ฉ ํ† ๊ธ€
    </button>
  </div>
</template>

<script>
import { ref } from 'vue';
import './components/CustomButton';

export default {
  name: 'App',
  setup() {
    const customButton = ref(null);
    const buttonVariant = ref('primary');
    const buttonSize = ref('large');
    const isLoading = ref(false);
    const buttonText = ref('ํด๋ฆญํ•˜์„ธ์š”');
    
    const handleButtonClick = (e) => {
      console.log('๋ฒ„ํŠผ ํด๋ฆญ!', e.detail);
    };
    
    const toggleLoading = () => {
      isLoading.value = !isLoading.value;
    };
    
    return {
      customButton,
      buttonVariant,
      buttonSize,
      isLoading,
      buttonText,
      handleButtonClick,
      toggleLoading
    };
  }
};
</script>

<script>
// Vue ์„ค์ •์—์„œ ์ปค์Šคํ…€ ์—˜๋ฆฌ๋จผํŠธ ๋ฌด์‹œํ•˜๋„๋ก ์„ค์ •
// main.js ๋˜๋Š” vite.config.js
export default {
  compilerOptions: {
    isCustomElement: tag => tag.startsWith('custom-')
  }
}
</script>

๐ŸŽฏ Vue๋Š” ์›น ์ปดํฌ๋„ŒํŠธ์™€ ๊ถํ•ฉ์ด ์ข‹์•„!

Vue๋Š” ์›น ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋„ค์ดํ‹ฐ๋ธŒํ•˜๊ฒŒ ์ž˜ ์ง€์›ํ•ด. v-bind๋กœ ์†์„ฑ ์ „๋‹ฌ๋„ ์‰ฝ๊ณ , @๋กœ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋‹๋„ ๊ฐ„ํŽธํ•ด. Vue 3๋ถ€ํ„ฐ๋Š” ๋”์šฑ ๊ฐœ์„ ๋˜์—ˆ์–ด!

๐Ÿ…ฐ๏ธ Angular์—์„œ ์›น ์ปดํฌ๋„ŒํŠธ ์‚ฌ์šฉํ•˜๊ธฐ

// app.module.ts
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';

// ์›น ์ปดํฌ๋„ŒํŠธ import
import './components/CustomButton';

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule],
  providers: [],
  bootstrap: [AppComponent],
  schemas: [CUSTOM_ELEMENTS_SCHEMA] // ์ปค์Šคํ…€ ์—˜๋ฆฌ๋จผํŠธ ํ—ˆ์šฉ
})
export class AppModule { }

// app.component.ts
import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <div class="app">
      <h1>Angular + ์›น ์ปดํฌ๋„ŒํŠธ</h1>
      
      <custom-button
        #customButton
        [attr.variant]="buttonVariant"
        [attr.size]="buttonSize"
        [attr.loading]="isLoading ? '' : null"
      >
        {{ buttonText }}
      </custom-button>
      
      <button (click)="toggleLoading()">
        ๋กœ๋”ฉ ํ† ๊ธ€
      </button>
    </div>
  `
})
export class AppComponent implements AfterViewInit {
  @ViewChild('customButton') customButton!: ElementRef;
  
  buttonVariant = 'primary';
  buttonSize = 'large';
  isLoading = false;
  buttonText = 'ํด๋ฆญํ•˜์„ธ์š”';
  
  ngAfterViewInit() {
    // ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ๋“ฑ๋ก
    this.customButton.nativeElement.addEventListener('button-click', (e: any) => {
      console.log('๋ฒ„ํŠผ ํด๋ฆญ!', e.detail);
    });
  }
  
  toggleLoading() {
    this.isLoading = !this.isLoading;
  }
}

๋ณด๋‹ค์‹œํ”ผ ์–ด๋–ค ํ”„๋ ˆ์ž„์›Œํฌ์—์„œ๋“  ์›น ์ปดํฌ๋„ŒํŠธ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด! ์ด๊ฒŒ ๋ฐ”๋กœ ์›น ํ‘œ์ค€์˜ ํž˜์ด์•ผ. ํ”„๋ ˆ์ž„์›Œํฌ๊ฐ€ ๋ฐ”๋€Œ์–ด๋„ ์ปดํฌ๋„ŒํŠธ๋Š” ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์ง€! ๐ŸŽ‰

๐ŸŽ“ ์‹ค์ „ ํ™œ์šฉ ์‚ฌ๋ก€

์ด๋ก ๊ณผ ์˜ˆ์ œ๋งŒ์œผ๋กœ๋Š” ๋ถ€์กฑํ•˜์ง€! ์‹ค์ œ๋กœ ์›น ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์–ด๋–ป๊ฒŒ ํ™œ์šฉ๋˜๋Š”์ง€ ๊ตฌ์ฒด์ ์ธ ์‚ฌ๋ก€๋ฅผ ์‚ดํŽด๋ณด์ž. ๐Ÿ’ผ

๐Ÿข ๋””์ž์ธ ์‹œ์Šคํ…œ ๊ตฌ์ถ•

๋งŽ์€ ๊ธฐ์—…๋“ค์ด ์›น ์ปดํฌ๋„ŒํŠธ๋กœ ๋””์ž์ธ ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค๊ณ  ์žˆ์–ด. ํ•œ ๋ฒˆ ๋งŒ๋“ค๋ฉด ๋ชจ๋“  ํ”„๋กœ์ ํŠธ์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์œผ๋‹ˆ๊นŒ!

์˜ˆ์‹œ: Google์˜ Material Web Components, Microsoft์˜ FAST, Salesforce์˜ Lightning Web Components ๋“ฑ์ด ๋ชจ๋‘ ์›น ์ปดํฌ๋„ŒํŠธ ๊ธฐ๋ฐ˜์ด์•ผ.

๐Ÿ›๏ธ ์ „์ž์ƒ๊ฑฐ๋ž˜ ์œ„์ ฏ

์‡ผํ•‘๋ชฐ์—์„œ ์‚ฌ์šฉํ•˜๋Š” ์ œํ’ˆ ์นด๋“œ, ์žฅ๋ฐ”๊ตฌ๋‹ˆ, ๊ฒฐ์ œ ์œ„์ ฏ ๋“ฑ์„ ์›น ์ปดํฌ๋„ŒํŠธ๋กœ ๋งŒ๋“ค๋ฉด ์—ฌ๋Ÿฌ ํŽ˜์ด์ง€์—์„œ ์ผ๊ด€๋˜๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด.

// ์‹ค์ „ ์˜ˆ์ œ: ์ œํ’ˆ ์นด๋“œ ์ปดํฌ๋„ŒํŠธ
class ProductCard extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }
  
  static get observedAttributes() {
    return ['product-id', 'name', 'price', 'image', 'rating', 'in-stock'];
  }
  
  connectedCallback() {
    this.render();
    this.attachEventListeners();
  }
  
  render() {
    const productId = this.getAttribute('product-id');
    const name = this.getAttribute('name') || '์ œํ’ˆ๋ช…';
    const price = this.getAttribute('price') || '0';
    const image = this.getAttribute('image') || 'placeholder.jpg';
    const rating = parseFloat(this.getAttribute('rating')) || 0;
    const inStock = this.getAttribute('in-stock') !== 'false';
    
    this.shadowRoot.innerHTML = `
      <style>.tx-header{background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);
color:#fff;
padding:40px 30px;
border-radius:16px;
text-align:center;
margin-bottom:40px;
box-shadow:0 8px 24px rgba(102,126,234,0.25);}
.tx-intro{background:#f8f9fa;
padding:25px;
border-radius:12px;
margin:30px 0;
border-left:5px solid #667eea;}
.tx-section{margin:40px 0;}
.tx-highlight{background:linear-gradient(120deg,#fef6e4 0%,#f3e7ff 100%);
padding:20px;
border-radius:10px;
margin:25px 0;}
.tx-tip{background:#e8f5e9;
padding:18px;
border-radius:10px;
margin:20px 0;
border-left:4px solid #4caf50;}
.tx-warning{background:#fff3e0;
padding:18px;
border-radius:10px;
margin:20px 0;
border-left:4px solid #ff9800;}
.tx-point{background:#e3f2fd;
padding:18px;
border-radius:10px;
margin:20px 0;
border-left:4px solid #2196f3;}
.tx-table-wrap{overflow-x:auto;
margin:25px 0;}
.tx-table{width:100%;
border-collapse:collapse;
background:#fff;
border-radius:8px;
overflow:hidden;
box-shadow:0 2px 8px rgba(0,0,0,0.08);}
.tx-table th{background:#667eea;
color:#fff;
padding:12px;
text-align:left;
font-weight:600;}
.tx-table td{padding:12px;
border-bottom:1px solid #e0e0e0;}
.tx-table tr:last-child td{border-bottom:none;}
.tx-svg-container{text-align:center;
margin:40px 0;}
.tx-concept-box{background:#fff;
padding:20px;
border-radius:12px;
margin:20px 0;
box-shadow:0 4px 12px rgba(0,0,0,0.08);}
.tx-step{background:linear-gradient(135deg,#ffecd2 0%,#fcb69f 100%);
padding:15px 20px;
border-radius:10px;
margin:15px 0;}
.tx-benefit{background:linear-gradient(135deg,#a8edea 0%,#fed6e3 100%);
padding:15px 20px;
border-radius:10px;
margin:15px 0;}</style>
      
      <div class="product-card">
        <div class="image-container">
          <img class="product-image" src="${image}" alt="${name}">
          <span class="badge ${inStock ? 'in-stock' : 'out-of-stock'}">
            ${inStock ? '์žฌ๊ณ  ์žˆ์Œ' : 'ํ’ˆ์ ˆ'}
          </span>
        </div>
        
        <div class="content">
          <h3 class="product-name">${name}</h3>
          
          <div class="rating">
            <span class="stars">${this.renderStars(rating)}</span>
            <span class="rating-value">${rating.toFixed(1)}</span>
          </div>
          
          <div class="price-container">
            <span class="price">${this.formatPrice(price)}์›</span>
          </div>
          
          <div class="actions">
            <button 
              class="add-to-cart" 
              ${!inStock ? 'disabled' : ''}
              data-action="add-to-cart"
            >
              ${inStock ? '์žฅ๋ฐ”๊ตฌ๋‹ˆ ๋‹ด๊ธฐ' : 'ํ’ˆ์ ˆ'}
            </button>
            <button 
              class="wishlist" 
              data-action="toggle-wishlist"
              title="์œ„์‹œ๋ฆฌ์ŠคํŠธ"
            >
              โ™ก
            </button>
          </div>
        </div>
      </div>
    `;
  }
  
  renderStars(rating) {
    const fullStars = Math.floor(rating);
    const hasHalfStar = rating % 1 >= 0.5;
    let stars = '';
    
    for (let i = 0; i < fullStars; i++) {
      stars += 'โ˜…';
    }
    if (hasHalfStar) {
      stars += 'โ˜†';
    }
    while (stars.length < 5) {
      stars += 'โ˜†';
    }
    
    return stars;
  }
  
  formatPrice(price) {
    return parseInt(price).toLocaleString('ko-KR');
  }
  
  attachEventListeners() {
    this.shadowRoot.querySelector('.add-to-cart')
      ?.addEventListener('click', () => this.handleAddToCart());
    
    this.shadowRoot.querySelector('.wishlist')
      ?.addEventListener('click', (e) => this.handleToggleWishlist(e));
  }
  
  handleAddToCart() {
    const productId = this.getAttribute('product-id');
    const name = this.getAttribute('name');
    const price = this.getAttribute('price');
    
    this.dispatchEvent(new CustomEvent('add-to-cart', {
      detail: { productId, name, price },
      bubbles: true,
      composed: true
    }));
    
    // ์‹œ๊ฐ์  ํ”ผ๋“œ๋ฐฑ
    this.showFeedback('์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค!');
  }
  
  handleToggleWishlist(e) {
    const button = e.target;
    const isActive = button.classList.toggle('active');
    button.textContent = isActive ? 'โ™ฅ' : 'โ™ก';
    
    const productId = this.getAttribute('product-id');
    
    this.dispatchEvent(new CustomEvent('toggle-wishlist', {
      detail: { productId, isActive },
      bubbles: true,
      composed: true
    }));
  }
  
  showFeedback(message) {
    // ํ† ์ŠคํŠธ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ ๋กœ์ง
    console.log(message);
  }
}

customElements.define('product-card', ProductCard);

// ์‚ฌ์šฉ ์˜ˆ์‹œ:
// <product-card
//   product-id="12345"
//   name="๋ฉ‹์ง„ ์ œํ’ˆ"
//   price="29900"
//   image="product.jpg"
//   rating="4.5"
//   in-stock="true"
// ></product-card>

์ด ์ œํ’ˆ ์นด๋“œ๋Š” ์‹ค์ œ ์‡ผํ•‘๋ชฐ์—์„œ ๋ฐ”๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์„ ์ •๋„๋กœ ์™„์„ฑ๋„๊ฐ€ ๋†’์•„! ์žฌ๊ณ  ์ƒํƒœ, ํ‰์ , ์œ„์‹œ๋ฆฌ์ŠคํŠธ, ์žฅ๋ฐ”๊ตฌ๋‹ˆ ๊ธฐ๋Šฅ๊นŒ์ง€ ๋ชจ๋‘ ํฌํ•จ๋˜์–ด ์žˆ์ง€. ๐Ÿ›’

๐Ÿ“Š ๋ฐ์ดํ„ฐ ์‹œ๊ฐํ™” ์ปดํฌ๋„ŒํŠธ

์ฐจํŠธ๋‚˜ ๊ทธ๋ž˜ํ”„ ๊ฐ™์€ ๋ฐ์ดํ„ฐ ์‹œ๊ฐํ™” ์š”์†Œ๋„ ์›น ์ปดํฌ๋„ŒํŠธ๋กœ ๋งŒ๋“ค๋ฉด ์ •๋ง ์œ ์šฉํ•ด. ํ•œ ๋ฒˆ ๋งŒ๋“ค์–ด๋‘๋ฉด ์–ด๋””์„œ๋“  ์“ธ ์ˆ˜ ์žˆ์œผ๋‹ˆ๊นŒ!

๐Ÿ’ก ์žฌ๋Šฅ๋„ท์—์„œ ์›น ์ปดํฌ๋„ŒํŠธ ํ™œ์šฉํ•˜๊ธฐ

์žฌ๋Šฅ๋„ท(https://www.jaenung.net) ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ๋„ ์›น ์ปดํฌ๋„ŒํŠธ๋ฅผ ํ™œ์šฉํ•˜๋ฉด ์ •๋ง ์ข‹์•„! ์˜ˆ๋ฅผ ๋“ค์–ด ์žฌ๋Šฅ ์นด๋“œ, ์‚ฌ์šฉ์ž ํ”„๋กœํ•„, ๋ฆฌ๋ทฐ ์ปดํฌ๋„ŒํŠธ ๋“ฑ์„ ๋งŒ๋“ค์–ด์„œ ์ผ๊ด€๋œ UI๋ฅผ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์–ด. ํŠนํžˆ ์—ฌ๋Ÿฌ ๊ฐœ๋ฐœ์ž๊ฐ€ ํ˜‘์—…ํ•  ๋•Œ ์›น ์ปดํฌ๋„ŒํŠธ๋กœ ๋งŒ๋“ค์–ด๋‘๋ฉด ๋ˆ„๊ตฌ๋‚˜ ์‰ฝ๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์ง€! ๐Ÿ˜Š

๐Ÿ› ๋””๋ฒ„๊น…๊ณผ ํ…Œ์ŠคํŒ…

์›น ์ปดํฌ๋„ŒํŠธ๋„ ์ผ๋ฐ˜ ์ฝ”๋“œ์ฒ˜๋Ÿผ ๋ฒ„๊ทธ๊ฐ€ ์ƒ๊ธธ ์ˆ˜ ์žˆ์–ด. ํšจ๊ณผ์ ์œผ๋กœ ๋””๋ฒ„๊น…ํ•˜๊ณ  ํ…Œ์ŠคํŠธํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณด์ž! ๐Ÿ”

๐Ÿ” ํฌ๋กฌ ๊ฐœ๋ฐœ์ž ๋„๊ตฌ ํ™œ์šฉ

ํฌ๋กฌ ๊ฐœ๋ฐœ์ž ๋„๊ตฌ๋Š” ์›น ์ปดํฌ๋„ŒํŠธ ๋””๋ฒ„๊น…์— ์ •๋ง ์œ ์šฉํ•ด!

// ๋””๋ฒ„๊น…์„ ์œ„ํ•œ ๋กœ๊น… ์œ ํ‹ธ๋ฆฌํ‹ฐ
class ComponentLogger {
  constructor(componentName) {
    this.componentName = componentName;
    this.enabled = true; // ํ”„๋กœ๋•์…˜์—์„œ๋Š” false๋กœ
  }
  
  log(message, data = null) {
    if (!this.enabled) return;
    
    console.log(
      `%c[${this.componentName}]%c ${message}`,
      'color: #667eea; font-weight: bold',
      'color: inherit',
      data || ''
    );
  }
  
  warn(message, data = null) {
    if (!this.enabled) return;
    
    console.warn(`[${this.componentName}] ${message}`, data || '');
  }
  
  error(message, error = null) {
    if (!this.enabled) return;
    
    console.error(`[${this.componentName}] ${message}`, error || '');
  }
  
  group(label) {
    if (!this.enabled) return;
    console.group(`[${this.componentName}] ${label}`);
  }
  
  groupEnd() {
    if (!this.enabled) return;
    console.groupEnd();
  }
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
class DebuggableComponent extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.logger = new ComponentLogger('DebuggableComponent');
  }
  
  connectedCallback() {
    this.logger.group('Lifecycle: connectedCallback');
    this.logger.log('์ปดํฌ๋„ŒํŠธ๊ฐ€ DOM์— ์ถ”๊ฐ€๋จ');
    this.logger.log('์†์„ฑ๋“ค', {
      name: this.getAttribute('name'),
      value: this.getAttribute('value')
    });
    this.render();
    this.logger.groupEnd();
  }
  
  attributeChangedCallback(name, oldValue, newValue) {
    this.logger.log(`์†์„ฑ ๋ณ€๊ฒฝ: ${name}`, {
      old: oldValue,
      new: newValue
    });
  }
  
  render() {
    try {
      this.logger.log('๋ Œ๋”๋ง ์‹œ์ž‘');
      // ๋ Œ๋”๋ง ๋กœ์ง...
      this.logger.log('๋ Œ๋”๋ง ์™„๋ฃŒ');
    } catch (error) {
      this.logger.error('๋ Œ๋”๋ง ์‹คํŒจ', error);
    }
  }
}

๐Ÿงช ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ

์›น ์ปดํฌ๋„ŒํŠธ๋„ ํ…Œ์ŠคํŠธ๊ฐ€ ํ•„์š”ํ•ด! Jest๋‚˜ Mocha ๊ฐ™์€ ํ…Œ์ŠคํŠธ ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด.

// Jest๋ฅผ ์‚ฌ์šฉํ•œ ์›น ์ปดํฌ๋„ŒํŠธ ํ…Œ์ŠคํŠธ
import './CustomButton';

describe('CustomButton', () => {
  let button;
  
  beforeEach(() => {
    // ๊ฐ ํ…Œ์ŠคํŠธ ์ „์— ์ƒˆ ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ
    button = document.createElement('custom-button');
    document.body.appendChild(button);
  });
  
  afterEach(() => {
    // ํ…Œ์ŠคํŠธ ํ›„ ์ •๋ฆฌ
    document.body.removeChild(button);
  });
  
  test('์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ •์˜๋˜์–ด ์žˆ์–ด์•ผ ํ•จ', () => {
    expect(customElements.get('custom-button')).toBeDefined();
  });
  
  test('๊ธฐ๋ณธ variant๋Š” primary์—ฌ์•ผ ํ•จ', () => {
    expect(button.variant).toBe('primary');
  });
  
  test('variant ์†์„ฑ ๋ณ€๊ฒฝ์ด ๋ฐ˜์˜๋˜์–ด์•ผ ํ•จ', () => {
    button.variant = 'secondary';
    expect(button.getAttribute('variant')).toBe('secondary');
  });
  
  test('disabled ์ƒํƒœ์—์„œ ํด๋ฆญ ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š์•„์•ผ ํ•จ', () => {
    const clickHandler = jest.fn();
    button.addEventListener('button-click', clickHandler);
    
    button.disabled = true;
    button.shadowRoot.querySelector('button').click();
    
    expect(clickHandler).not.toHaveBeenCalled();
  });
  
  test('loading ์ƒํƒœ์—์„œ ์Šคํ”ผ๋„ˆ๊ฐ€ ํ‘œ์‹œ๋˜์–ด์•ผ ํ•จ', async () => {
    button.loading = true;
    
    // ๋‹ค์Œ ํ‹ฑ๊นŒ์ง€ ๋Œ€๊ธฐ
    await new Promise(resolve => setTimeout(resolve, 0));
    
    const spinner = button.shadowRoot.querySelector('.spinner');
    expect(spinner).toBeTruthy();
  });
  
  test('๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ์ปค์Šคํ…€ ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•ด์•ผ ํ•จ', () => {
    const clickHandler = jest.fn();
    button.addEventListener('button-click', clickHandler);
    
    button.shadowRoot.querySelector('button').click();
    
    expect(clickHandler).toHaveBeenCalled();
    expect(clickHandler.mock.calls[0][0].detail).toHaveProperty('timestamp');
  });
});

// ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ์˜ˆ์‹œ
describe('CustomButton Integration', () => {
  test('์—ฌ๋Ÿฌ ๋ฒ„ํŠผ์ด ๋…๋ฆฝ์ ์œผ๋กœ ๋™์ž‘ํ•ด์•ผ ํ•จ', () => {
    const button1 = document.createElement('custom-button');
    const button2 = document.createElement('custom-button');
    
    document.body.appendChild(button1);
    document.body.appendChild(button2);
    
    button1.variant = 'primary';
    button2.variant = 'danger';
    
    expect(button1.variant).toBe('primary');
    expect(button2.variant).toBe('danger');
    
    document.body.removeChild(button1);
    document.body.removeChild(button2);
  });
});

๐ŸŽญ E2E ํ…Œ์ŠคํŠธ

Playwright๋‚˜ Cypress๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์‹ค์ œ ๋ธŒ๋ผ์šฐ์ €์—์„œ ์›น ์ปดํฌ๋„ŒํŠธ๋ฅผ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ์–ด!

// Cypress๋ฅผ ์‚ฌ์šฉํ•œ E2E ํ…Œ์ŠคํŠธ
describe('Product Card E2E', () => {
  beforeEach(() => {
    cy.visit('/products');
  });
  
  it('์ œํ’ˆ ์นด๋“œ๊ฐ€ ๋ Œ๋”๋ง๋˜์–ด์•ผ ํ•จ', () => {
    cy.get('product-card').should('exist');
  });
  
  it('์žฅ๋ฐ”๊ตฌ๋‹ˆ ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•ด์•ผ ํ•จ', () => {
    cy.get('product-card')
      .shadow()
      .find('.add-to-cart')
      .click();
    
    cy.get('.toast-message')
      .should('contain', '์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค');
  });
  
  it('์œ„์‹œ๋ฆฌ์ŠคํŠธ ํ† ๊ธ€์ด ๋™์ž‘ํ•ด์•ผ ํ•จ', () => {
    cy.get('product-card')
      .shadow()
      .find('.wishlist')
      .click()
      .should('have.class', 'active');
  });
  
  it('ํ’ˆ์ ˆ ์ƒํƒœ์—์„œ ์žฅ๋ฐ”๊ตฌ๋‹ˆ ๋ฒ„ํŠผ์ด ๋น„ํ™œ์„ฑํ™”๋˜์–ด์•ผ ํ•จ', () => {
    cy.get('product-card[in-stock="false"]')
      .shadow()
      .find('.add-to-cart')
      .should('be.disabled');
  });
});

๐Ÿš€ ๋ฐฐํฌ์™€ ๋ฒˆ๋“ค๋ง

์›น ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค์—ˆ์œผ๋ฉด ์ด์ œ ๋ฐฐํฌํ•ด์•ผ๊ฒ ์ง€? ํšจ์œจ์ ์ธ ๋ฒˆ๋“ค๋ง๊ณผ ๋ฐฐํฌ ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณด์ž! ๐Ÿ“ฆ

๐Ÿ“ฆ Rollup์œผ๋กœ ๋ฒˆ๋“ค๋งํ•˜๊ธฐ

Rollup์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋ฒˆ๋“ค๋ง์— ์ตœ์ ํ™”๋˜์–ด ์žˆ์–ด์„œ ์›น ์ปดํฌ๋„ŒํŠธ ๋ฐฐํฌ์— ๋”ฑ์ด์•ผ!

// rollup.config.js
import resolve from '@rollup/plugin-node-resolve';
import { terser } from 'rollup-plugin-terser';
import postcss from 'rollup-plugin-postcss';

export default {
  input: 'src/index.js',
  output: [
    {
      file: 'dist/my-components.js',
      format: 'esm',
      sourcemap: true
    },
    {
      file: 'dist/my-components.min.js',
      format: 'esm',
      sourcemap: true,
      plugins: [terser()]
    },
    {
      file: 'dist/my-components.umd.js',
      format: 'umd',
      name: 'MyComponents',
      sourcemap: true
    }
  ],
  plugins: [
    resolve(),
    postcss({
      inject: false,
      extract: false
    })
  ]
};

// package.json
{
  "name": "my-web-components",
  "version": "1.0.0",
  "main": "dist/my-components.js",
  "module": "dist/my-components.js",
  "unpkg": "dist/my-components.min.js",
  "files": [
    "dist"
  ],
  "scripts": {
    "build": "rollup -c",
    "dev": "rollup -c -w"
  }
}

๐Ÿ“š NPM ํŒจํ‚ค์ง€๋กœ ๋ฐฐํฌํ•˜๊ธฐ

์›น ์ปดํฌ๋„ŒํŠธ๋ฅผ NPM์— ๋ฐฐํฌํ•˜๋ฉด ๋‹ค๋ฅธ ๊ฐœ๋ฐœ์ž๋“ค๋„ ์‰ฝ๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด!

// 1. package.json ์„ค์ •
{
  "name": "@mycompany/web-components",
  "version": "1.0.0",
  "description": "์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์›น ์ปดํฌ๋„ŒํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ",
  "main": "dist/index.js",
  "module": "dist/index.esm.js",
  "types": "dist/index.d.ts",
  "files": [
    "dist",
    "README.md"
  ],
  "keywords": [
    "web-components",
    "custom-elements",
    "ui-components"
  ],
  "repository": {
    "type": "git",
    "url": "https://github.com/mycompany/web-components"
  },
  "license": "MIT"
}

// 2. README.md ์ž‘์„ฑ
# My Web Components

์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์›น ์ปดํฌ๋„ŒํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

## ์„ค์น˜

```bash
npm install @mycompany/web-components
```

## ์‚ฌ์šฉ๋ฒ•

```javascript
import '@mycompany/web-components';

// HTML์—์„œ ์‚ฌ์šฉ
<custom-button variant="primary">ํด๋ฆญ</custom-button>
```

## ์ปดํฌ๋„ŒํŠธ ๋ชฉ๋ก

- CustomButton
- ProductCard
- UserProfile
- TabsComponent

// 3. NPM ๋ฐฐํฌ
npm login
npm publish --access public

๐Ÿ’ก CDN์œผ๋กœ ๋ฐฐํฌํ•˜๊ธฐ

NPM ์—†์ด๋„ CDN์„ ํ†ตํ•ด ๋ฐ”๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜๋ฉด ๋” ํŽธ๋ฆฌํ•ด!

// unpkg๋‚˜ jsdelivr์„ ํ†ตํ•œ CDN ์‚ฌ์šฉ
<script type="module">
  import 'https://unpkg.com/@mycompany/web-components@1.0.0/dist/index.js';
</script>

// ๋˜๋Š”
<script src="https://cdn.jsdelivr.net/npm/@mycompany/web-components@1.0.0/dist/index.umd.js"></script>

๐ŸŽฏ ๋ธŒ๋ผ์šฐ์ € ํ˜ธํ™˜์„ฑ๊ณผ ํด๋ฆฌํ•„

์›น ์ปดํฌ๋„ŒํŠธ๋Š” ๋น„๊ต์  ์ตœ์‹  ๊ธฐ์ˆ ์ด๋ผ ์˜ค๋ž˜๋œ ๋ธŒ๋ผ์šฐ์ €์—์„œ๋Š” ์ง€์›ํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์–ด. ํ˜ธํ™˜์„ฑ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณด์ž! ๐ŸŒ

๊ธฐ๋Šฅ Chrome Firefox Safari Edge
Custom Elements โœ… 54+ โœ… 63+ โœ… 10.1+ โœ… 79+
Shadow DOM โœ… 53+ โœ… 63+ โœ… 10.1+ โœ… 79+
HTML Templates โœ… 26+ โœ… 22+ โœ… 8+ โœ… 13+

โš ๏ธ IE11์€ ์ง€์›ํ•˜์ง€ ์•Š์•„!

Internet Explorer 11์€ ์›น ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋„ค์ดํ‹ฐ๋ธŒ๋กœ ์ง€์›ํ•˜์ง€ ์•Š์•„. ํด๋ฆฌํ•„์„ ์‚ฌ์šฉํ•ด์•ผ ํ•ด!

// ํด๋ฆฌํ•„ ์‚ฌ์šฉํ•˜๊ธฐ
// 1. ์„ค์น˜
npm install @webcomponents/webcomponentsjs

// 2. HTML์— ์ถ”๊ฐ€
<script src="node_modules/@webcomponents/webcomponentsjs/webcomponents-loader.js"></script>

// 3. ํด๋ฆฌํ•„ ๋กœ๋“œ ํ›„ ์ปดํฌ๋„ŒํŠธ ๋กœ๋“œ
<script type="module">
  // ํด๋ฆฌํ•„์ด ๋กœ๋“œ๋  ๋•Œ๊นŒ์ง€ ๋Œ€๊ธฐ
  window.WebComponents = window.WebComponents || {};
  window.WebComponents.waitFor = window.WebComponents.waitFor || function(cb) {
    if (window.WebComponents.ready) {
      cb();
    } else {
      window.addEventListener('WebComponentsReady', cb);
    }
  };
  
  // ํด๋ฆฌํ•„ ์ค€๋น„ ์™„๋ฃŒ ํ›„ ์ปดํฌ๋„ŒํŠธ ๋กœ๋“œ
  WebComponents.waitFor(() => {
    import('./my-components.js');
  });
</script>

// 4. ์กฐ๊ฑด๋ถ€ ํด๋ฆฌํ•„ ๋กœ๋“œ (๋” ํšจ์œจ์ )
<script>
  // ์›น ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ง€์›ํ•˜๋Š”์ง€ ํ™•์ธ
  if (!window.customElements) {
    // ์ง€์›ํ•˜์ง€ ์•Š์œผ๋ฉด ํด๋ฆฌํ•„ ๋กœ๋“œ
    document.write('<script src="webcomponents-loader.js"><\/script>');
  }
</script>

๐Ÿ” ๊ธฐ๋Šฅ ๊ฐ์ง€์™€ ๋Œ€์ฒด ๋ฐฉ์•ˆ

ํŠน์ • ๊ธฐ๋Šฅ์ด ์ง€์›๋˜์ง€ ์•Š์„ ๋•Œ๋ฅผ ๋Œ€๋น„ํ•œ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ๋„ ์ค‘์š”ํ•ด!

class CompatibleComponent extends HTMLElement {
  constructor() {
    super();
    
    // Shadow DOM ์ง€์› ํ™•์ธ
    if (this.attachShadow) {
      this.attachShadow({ mode: 'open' });
      this.usingShadowDOM = true;
    } else {
      // Shadow DOM ๋ฏธ์ง€์› ์‹œ ์ผ๋ฐ˜ DOM ์‚ฌ์šฉ
      this.usingShadowDOM = false;
      console.warn('Shadow DOM์ด ์ง€์›๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ผ๋ฐ˜ DOM์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.');
    }
  }
  
  connectedCallback() {
    this.render();
  }
  
  render() {
    const template = `
      <div class="component">
        <h3>ํ˜ธํ™˜ ๊ฐ€๋Šฅํ•œ ์ปดํฌ๋„ŒํŠธ</h3>
        <p>Shadow DOM ${this.usingShadowDOM ? '์‚ฌ์šฉ' : '๋ฏธ์‚ฌ์šฉ'}</p>
      </div>
    `;
    
    if (this.usingShadowDOM) {
      this.shadowRoot.innerHTML = `
        <style>.tx-header{background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);
color:#fff;
padding:40px 30px;
border-radius:16px;
text-align:center;
margin-bottom:40px;
box-shadow:0 8px 24px rgba(102,126,234,0.25);}
.tx-intro{background:#f8f9fa;
padding:25px;
border-radius:12px;
margin:30px 0;
border-left:5px solid #667eea;}
.tx-section{margin:40px 0;}
.tx-highlight{background:linear-gradient(120deg,#fef6e4 0%,#f3e7ff 100%);
padding:20px;
border-radius:10px;
margin:25px 0;}
.tx-tip{background:#e8f5e9;
padding:18px;
border-radius:10px;
margin:20px 0;
border-left:4px solid #4caf50;}
.tx-warning{background:#fff3e0;
padding:18px;
border-radius:10px;
margin:20px 0;
border-left:4px solid #ff9800;}
.tx-point{background:#e3f2fd;
padding:18px;
border-radius:10px;
margin:20px 0;
border-left:4px solid #2196f3;}
.tx-table-wrap{overflow-x:auto;
margin:25px 0;}
.tx-table{width:100%;
border-collapse:collapse;
background:#fff;
border-radius:8px;
overflow:hidden;
box-shadow:0 2px 8px rgba(0,0,0,0.08);}
.tx-table th{background:#667eea;
color:#fff;
padding:12px;
text-align:left;
font-weight:600;}
.tx-table td{padding:12px;
border-bottom:1px solid #e0e0e0;}
.tx-table tr:last-child td{border-bottom:none;}
.tx-svg-container{text-align:center;
margin:40px 0;}
.tx-concept-box{background:#fff;
padding:20px;
border-radius:12px;
margin:20px 0;
box-shadow:0 4px 12px rgba(0,0,0,0.08);}
.tx-step{background:linear-gradient(135deg,#ffecd2 0%,#fcb69f 100%);
padding:15px 20px;
border-radius:10px;
margin:15px 0;}
.tx-benefit{background:linear-gradient(135deg,#a8edea 0%,#fed6e3 100%);
padding:15px 20px;
border-radius:10px;
margin:15px 0;}</style>
        ${template}
      `;
    } else {
      // Shadow DOM ์—†์ด ์‚ฌ์šฉ
      this.innerHTML = template;
      // ์Šคํƒ€์ผ์€ ์™ธ๋ถ€ CSS๋กœ ์ฒ˜๋ฆฌ
    }
  }
}

// Custom Elements ์ง€์› ํ™•์ธ
if (window.customElements) {
  customElements.define('compatible-component', CompatibleComponent);
} else {
  console.error('Custom Elements๊ฐ€ ์ง€์›๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ํด๋ฆฌํ•„์„ ๋กœ๋“œํ•˜์„ธ์š”.');
}

๐Ÿ’ก ๊ณ ๊ธ‰ ํŒจํ„ด๊ณผ ํŒ

๋งˆ์ง€๋ง‰์œผ๋กœ ์›น ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋” ํšจ๊ณผ์ ์œผ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๊ณ ๊ธ‰ ํŒจํ„ด๋“ค์„ ์•Œ์•„๋ณด์ž! ์ด๊ฒƒ๋“ค์„ ์•Œ๋ฉด ์ง„์งœ ํ”„๋กœ ๊ฐœ๋ฐœ์ž๊ฐ€ ๋  ์ˆ˜ ์žˆ์–ด! ๐ŸŽ“

1๏ธโƒฃ Mixin ํŒจํ„ด์œผ๋กœ ๊ธฐ๋Šฅ ์žฌ์‚ฌ์šฉ

์—ฌ๋Ÿฌ ์ปดํฌ๋„ŒํŠธ์—์„œ ๊ณตํ†ต์œผ๋กœ ์‚ฌ์šฉํ•˜๋Š” ๊ธฐ๋Šฅ์„ Mixin์œผ๋กœ ๋งŒ๋“ค์–ด๋ณด์ž!

// ์ด๋ฒคํŠธ ๋ฐœ์ƒ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋Š” Mixin
const EventEmitterMixin = (Base) => class extends Base {
  emit(eventName, detail = {}) {
    this.dispatchEvent(new CustomEvent(eventName, {
      detail,
      bubbles: true,
      composed: true
    }));
  }
};

// ๋กœ๋”ฉ ์ƒํƒœ ๊ด€๋ฆฌ Mixin
const LoadingMixin = (Base) => class extends Base {
  constructor() {
    super();
    this._loading = false;
  }
  
  get loading() {
    return this._loading;
  }
  
  set loading(value) {
    this._loading = value;
    this.updateLoadingState();
  }
  
  updateLoadingState() {
    // ๋กœ๋”ฉ UI ์—…๋ฐ์ดํŠธ
    const loadingEl = this.shadowRoot?.querySelector('.loading-indicator');
    if (loadingEl) {
      loadingEl.style.display = this._loading ? 'block' : 'none';
    }
  }
};

// Mixin ์‚ฌ์šฉ
class MyComponent extends LoadingMixin(EventEmitterMixin(HTMLElement)) {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }
  
  async fetchData() {
    this.loading = true;
    
    try {
      const response = await fetch('/api/data');
      const data = await response.json();
      
      // ์ด๋ฒคํŠธ ๋ฐœ์ƒ
      this.emit('data-loaded', { data });
    } catch (error) {
      this.emit('error', { error });
    } finally {
      this.loading = false;
    }
  }
}

customElements.define('my-component', MyComponent);

2๏ธโƒฃ ์ปดํฌ๋„ŒํŠธ ์ปดํฌ์ง€์…˜

์ž‘์€ ์ปดํฌ๋„ŒํŠธ๋“ค์„ ์กฐํ•ฉํ•ด์„œ ํฐ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“œ๋Š” ํŒจํ„ด์ด์•ผ!

// ์ž‘์€ ์ปดํฌ๋„ŒํŠธ๋“ค
class IconComponent extends HTMLElement {
  connectedCallback() {
    const type = this.getAttribute('type') || 'default';
    this.innerHTML = this.getIcon(type);
  }
  
  getIcon(type) {
    const icons = {
      'check': 'โœ“',
      'close': 'โœ•',
      'heart': 'โ™ฅ',
      'star': 'โ˜…'
    };
    return icons[type] || 'โ€ข';
  }
}

class BadgeComponent extends HTMLElement {
  connectedCallback() {
    this.style.cssText = `
      display: inline-block;
      padding: 4px 8px;
      background: #667eea;
      color: white;
      border-radius: 12px;
      font-size: 12px;
    `;
  }
}

// ํฐ ์ปดํฌ๋„ŒํŠธ์—์„œ ์กฐํ•ฉ
class NotificationCard extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }
  
  connectedCallback() {
    const type = this.getAttribute('type') || 'info';
    const message = this.getAttribute('message') || '';
    const badge = this.getAttribute('badge') || '';
    
    this.shadowRoot.innerHTML = `
      <style>.tx-header{background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);
color:#fff;
padding:40px 30px;
border-radius:16px;
text-align:center;
margin-bottom:40px;
box-shadow:0 8px 24px rgba(102,126,234,0.25);}
.tx-intro{background:#f8f9fa;
padding:25px;
border-radius:12px;
margin:30px 0;
border-left:5px solid #667eea;}
.tx-section{margin:40px 0;}
.tx-highlight{background:linear-gradient(120deg,#fef6e4 0%,#f3e7ff 100%);
padding:20px;
border-radius:10px;
margin:25px 0;}
.tx-tip{background:#e8f5e9;
padding:18px;
border-radius:10px;
margin:20px 0;
border-left:4px solid #4caf50;}
.tx-warning{background:#fff3e0;
padding:18px;
border-radius:10px;
margin:20px 0;
border-left:4px solid #ff9800;}
.tx-point{background:#e3f2fd;
padding:18px;
border-radius:10px;
margin:20px 0;
border-left:4px solid #2196f3;}
.tx-table-wrap{overflow-x:auto;
margin:25px 0;}
.tx-table{width:100%;
border-collapse:collapse;
background:#fff;
border-radius:8px;
overflow:hidden;
box-shadow:0 2px 8px rgba(0,0,0,0.08);}
.tx-table th{background:#667eea;
color:#fff;
padding:12px;
text-align:left;
font-weight:600;}
.tx-table td{padding:12px;
border-bottom:1px solid #e0e0e0;}
.tx-table tr:last-child td{border-bottom:none;}
.tx-svg-container{text-align:center;
margin:40px 0;}
.tx-concept-box{background:#fff;
padding:20px;
border-radius:12px;
margin:20px 0;
box-shadow:0 4px 12px rgba(0,0,0,0.08);}
.tx-step{background:linear-gradient(135deg,#ffecd2 0%,#fcb69f 100%);
padding:15px 20px;
border-radius:10px;
margin:15px 0;}
.tx-benefit{background:linear-gradient(135deg,#a8edea 0%,#fed6e3 100%);
padding:15px 20px;
border-radius:10px;
margin:15px 0;}</style>
      
      <div class="notification">
        <icon-component type="${type}"></icon-component>
        <span>${message}</span>
        ${badge ? `<badge-component>${badge}</badge-component>` : ''}
      </div>
    `;
  }
}

customElements.define('icon-component', IconComponent);
customElements.define('badge-component', BadgeComponent);
customElements.define('notification-card', NotificationCard);

3๏ธโƒฃ ๋™์  ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ

๋Ÿฐํƒ€์ž„์— ๋™์ ์œผ๋กœ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ์„ค์ •ํ•˜๋Š” ํŒจํ„ด!

// ์ปดํฌ๋„ŒํŠธ ํŒฉํ† ๋ฆฌ
class ComponentFactory {
  static create(tagName, attributes = {}, children = []) {
    const element = document.createElement(tagName);
    
    // ์†์„ฑ ์„ค์ •
    Object.entries(attributes).forEach(([key, value]) => {
      if (typeof value === 'boolean') {
        if (value) element.setAttribute(key, '');
      } else {
        element.setAttribute(key, value);
      }
    });
    
    // ์ž์‹ ์š”์†Œ ์ถ”๊ฐ€
    children.forEach(child => {
      if (typeof child === 'string') {
        element.appendChild(document.createTextNode(child));
      } else {
        element.appendChild(child);
      }
    });
    
    return element;
  }
  
  static createButton(text, variant = 'primary', onClick = null) {
    const button = this.create('custom-button', { variant }, [text]);
    
    if (onClick) {
      button.addEventListener('button-click', onClick);
    }
    
    return button;
  }
  
  static createCard(title, content, actions = []) {
    const titleEl = this.create('h3', {}, [title]);
    const contentEl = this.create('p', {}, [content]);
    const actionsContainer = this.create('div', { class: 'actions' }, actions);
    
    return this.create('product-card', {}, [
      titleEl,
      contentEl,
      actionsContainer
    ]);
  }
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
const saveButton = ComponentFactory.createButton(
  '์ €์žฅ',
  'primary',
  (e) => console.log('์ €์žฅ ํด๋ฆญ!', e.detail)
);

const cancelButton = ComponentFactory.createButton(
  '์ทจ์†Œ',
  'secondary',
  () => console.log('์ทจ์†Œ ํด๋ฆญ!')
);

const card = ComponentFactory.createCard(
  '์ œํ’ˆ ์ •๋ณด',
  '์ด ์ œํ’ˆ์€ ์ •๋ง ์ข‹์Šต๋‹ˆ๋‹ค!',
  [saveButton, cancelButton]
);

document.body.appendChild(card);

4๏ธโƒฃ ๋ฐ˜์‘ํ˜• ์†์„ฑ ์‹œ์Šคํ…œ

์†์„ฑ ๋ณ€๊ฒฝ์„ ์ž๋™์œผ๋กœ ๊ฐ์ง€ํ•˜๊ณ  UI๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๊ณ ๊ธ‰ ํŒจํ„ด!

// ๋ฐ˜์‘ํ˜• ์†์„ฑ ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ (TypeScript ์Šคํƒ€์ผ)
function reactive(target, propertyKey) {
  const privateKey = `_${propertyKey}`;
  
  Object.defineProperty(target, propertyKey, {
    get() {
      return this[privateKey];
    },
    set(value) {
      const oldValue = this[privateKey];
      this[privateKey] = value;
      
      // ๊ฐ’์ด ์‹ค์ œ๋กœ ๋ณ€๊ฒฝ๋˜์—ˆ์„ ๋•Œ๋งŒ ์—…๋ฐ์ดํŠธ
      if (oldValue !== value) {
        this.requestUpdate(propertyKey, oldValue, value);
      }
    },
    enumerable: true,
    configurable: true
  });
}

class ReactiveComponent extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this._updateScheduled = false;
    this._changedProperties = new Map();
  }
  
  requestUpdate(property, oldValue, newValue) {
    this._changedProperties.set(property, { oldValue, newValue });
    
    if (!this._updateScheduled) {
      this._updateScheduled = true;
      
      // ๋‹ค์Œ ํ”„๋ ˆ์ž„์— ์—…๋ฐ์ดํŠธ (๋ฐฐ์นญ)
      requestAnimationFrame(() => {
        this.performUpdate();
        this._updateScheduled = false;
      });
    }
  }
  
  performUpdate() {
    // ๋ณ€๊ฒฝ๋œ ์†์„ฑ๋“ค์— ๋Œ€ํ•ด ์—…๋ฐ์ดํŠธ ์ˆ˜ํ–‰
    this._changedProperties.forEach((change, property) => {
      this.propertyChanged(property, change.oldValue, change.newValue);
    });
    
    this._changedProperties.clear();
    this.render();
  }
  
  propertyChanged(property, oldValue, newValue) {
    // ๊ฐœ๋ณ„ ์†์„ฑ ๋ณ€๊ฒฝ ์ฒ˜๋ฆฌ
    console.log(`${property} ๋ณ€๊ฒฝ: ${oldValue} โ†’ ${newValue}`);
  }
  
  render() {
    // ๋ Œ๋”๋ง ๋กœ์ง
  }
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
class CounterComponent extends ReactiveComponent {
  constructor() {
    super();
    this._count = 0;
    this._step = 1;
  }
  
  // reactive ์†์„ฑ๋“ค
  get count() { return this._count; }
  set count(value) {
    const oldValue = this._count;
    this._count = value;
    this.requestUpdate('count', oldValue, value);
  }
  
  get step() { return this._step; }
  set step(value) {
    const oldValue = this._step;
    this._step = value;
    this.requestUpdate('step', oldValue, value);
  }
  
  connectedCallback() {
    this.render();
    this.attachEventListeners();
  }
  
  render() {
    this.shadowRoot.innerHTML = `
      <style>.tx-header{background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);
color:#fff;
padding:40px 30px;
border-radius:16px;
text-align:center;
margin-bottom:40px;
box-shadow:0 8px 24px rgba(102,126,234,0.25);}
.tx-intro{background:#f8f9fa;
padding:25px;
border-radius:12px;
margin:30px 0;
border-left:5px solid #667eea;}
.tx-section{margin:40px 0;}
.tx-highlight{background:linear-gradient(120deg,#fef6e4 0%,#f3e7ff 100%);
padding:20px;
border-radius:10px;
margin:25px 0;}
.tx-tip{background:#e8f5e9;
padding:18px;
border-radius:10px;
margin:20px 0;
border-left:4px solid #4caf50;}
.tx-warning{background:#fff3e0;
padding:18px;
border-radius:10px;
margin:20px 0;
border-left:4px solid #ff9800;}
.tx-point{background:#e3f2fd;
padding:18px;
border-radius:10px;
margin:20px 0;
border-left:4px solid #2196f3;}
.tx-table-wrap{overflow-x:auto;
margin:25px 0;}
.tx-table{width:100%;
border-collapse:collapse;
background:#fff;
border-radius:8px;
overflow:hidden;
box-shadow:0 2px 8px rgba(0,0,0,0.08);}
.tx-table th{background:#667eea;
color:#fff;
padding:12px;
text-align:left;
font-weight:600;}
.tx-table td{padding:12px;
border-bottom:1px solid #e0e0e0;}
.tx-table tr:last-child td{border-bottom:none;}
.tx-svg-container{text-align:center;
margin:40px 0;}
.tx-concept-box{background:#fff;
padding:20px;
border-radius:12px;
margin:20px 0;
box-shadow:0 4px 12px rgba(0,0,0,0.08);}
.tx-step{background:linear-gradient(135deg,#ffecd2 0%,#fcb69f 100%);
padding:15px 20px;
border-radius:10px;
margin:15px 0;}
.tx-benefit{background:linear-gradient(135deg,#a8edea 0%,#fed6e3 100%);
padding:15px 20px;
border-radius:10px;
margin:15px 0;}</style>
      
      <div class="counter">
        <div class="count">${this.count}</div>
        <button id="decrease">-${this.step}</button>
        <button id="increase">+${this.step}</button>
      </div>
    `;
  }
  
  attachEventListeners() {
    this.shadowRoot.getElementById('increase')
      ?.addEventListener('click', () => {
        this.count += this.step;
      });
    
    this.shadowRoot.getElementById('decrease')
      ?.addEventListener('click', () => {
        this.count -= this.step;
      });
  }
}

customElements.define('counter-component', CounterComponent);

๐ŸŽฌ ๋งˆ๋ฌด๋ฆฌํ•˜๋ฉฐ

์™€! ์ •๋ง ๊ธด ์—ฌ์ •์ด์—ˆ์ง€? ๐Ÿ˜… ์›น ์ปดํฌ๋„ŒํŠธ์™€ ์ปค์Šคํ…€ ์—˜๋ฆฌ๋จผํŠธ์— ๋Œ€ํ•ด ์ •๋ง ๋งŽ์€ ๊ฑธ ๋ฐฐ์› ์–ด!

์ฒ˜์Œ์—๋Š” ๋ณต์žกํ•ด ๋ณด์ผ ์ˆ˜ ์žˆ์ง€๋งŒ, ํ•˜๋‚˜์”ฉ ์ฐจ๊ทผ์ฐจ๊ทผ ์ตํžˆ๋‹ค ๋ณด๋ฉด ์ •๋ง ๊ฐ•๋ ฅํ•œ ๋„๊ตฌ๋ผ๋Š” ๊ฑธ ์•Œ๊ฒŒ ๋  ๊ฑฐ์•ผ. ํ•œ ๋ฒˆ ๋งŒ๋“ค์–ด๋‘๋ฉด ์–ด๋””์„œ๋“  ์žฌ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ณ , ํ”„๋ ˆ์ž„์›Œํฌ์— ์ข…์†๋˜์ง€ ์•Š์œผ๋ฉฐ, ์›น ํ‘œ์ค€์ด๋ผ๋Š” ๋“ ๋“ ํ•œ ๊ธฐ๋ฐ˜ ์œ„์— ์žˆ๋‹ค๋Š” ๊ฒŒ ํฐ ์žฅ์ ์ด์ง€! ๐Ÿ’ช

๐ŸŽฏ ํ•ต์‹ฌ ์š”์•ฝ

1. ์›น ์ปดํฌ๋„ŒํŠธ์˜ 3์š”์†Œ
โ€ข ์ปค์Šคํ…€ ์—˜๋ฆฌ๋จผํŠธ: ๋‚˜๋งŒ์˜ HTML ํƒœ๊ทธ ๋งŒ๋“ค๊ธฐ
โ€ข ์„€๋„์šฐ DOM: ์Šคํƒ€์ผ๊ณผ ๊ตฌ์กฐ ์บก์Аํ™”
โ€ข HTML ํ…œํ”Œ๋ฆฟ: ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋งˆํฌ์—…

2. ์‹ค์ „ ํ™œ์šฉ ํฌ์ธํŠธ
โ€ข ๋””์ž์ธ ์‹œ์Šคํ…œ ๊ตฌ์ถ•์— ์ตœ์ 
โ€ข ํ”„๋ ˆ์ž„์›Œํฌ ๋…๋ฆฝ์ 
โ€ข ์ง„์ •ํ•œ ์žฌ์‚ฌ์šฉ์„ฑ ํ™•๋ณด

3. ์ฃผ์˜ํ•  ์ 
โ€ข ๋ธŒ๋ผ์šฐ์ € ํ˜ธํ™˜์„ฑ ์ฒดํฌ
โ€ข ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€
โ€ข ์„ฑ๋Šฅ ์ตœ์ ํ™” ๊ณ ๋ ค

4. ๋ฏธ๋ž˜ ์ „๋ง
โ€ข ์›น ํ‘œ์ค€์œผ๋กœ ๊ณ„์† ๋ฐœ์ „ ์ค‘
โ€ข ๋” ๋งŽ์€ ๋ธŒ๋ผ์šฐ์ € ์ง€์›
โ€ข ์ƒํƒœ๊ณ„ ์ง€์† ์„ฑ์žฅ

์žฌ๋Šฅ๋„ท์—์„œ๋„ ์ด๋Ÿฐ ์›น ์ปดํฌ๋„ŒํŠธ ๊ธฐ์ˆ ์„ ํ™œ์šฉํ•œ ํ”„๋กœ์ ํŠธ๋“ค์„ ๋งŽ์ด ๋ณผ ์ˆ˜ ์žˆ์„ ๊ฑฐ์•ผ. ์›น ๊ฐœ๋ฐœ ์žฌ๋Šฅ์„ ๊ณต์œ ํ•˜๊ฑฐ๋‚˜ ๋ฐฐ์šฐ๊ณ  ์‹ถ๋‹ค๋ฉด ์žฌ๋Šฅ๋„ท์„ ํ™œ์šฉํ•ด๋ณด๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์•ผ! ๐Ÿ˜Š

์ด์ œ ์—ฌ๋Ÿฌ๋ถ„๋„ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ UI ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋Š” ๋Šฅ๋ ฅ์„ ๊ฐ–์ถ”๊ฒŒ ๋˜์—ˆ์–ด. ์ž‘์€ ๋ฒ„ํŠผ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด์„œ ์ ์  ๋ณต์žกํ•œ ์ปดํฌ๋„ŒํŠธ๋กœ ํ™•์žฅํ•ด๋‚˜๊ฐ€๋ณด์ž. ์‹ค์ „ ํ”„๋กœ์ ํŠธ์— ์ ์šฉํ•˜๋ฉด์„œ ๊ฒฝํ—˜์„ ์Œ“๋‹ค ๋ณด๋ฉด ์–ด๋А์ƒˆ ์›น ์ปดํฌ๋„ŒํŠธ ๋งˆ์Šคํ„ฐ๊ฐ€ ๋˜์–ด ์žˆ์„ ๊ฑฐ์•ผ! ๐Ÿš€

๊ถ๊ธˆํ•œ ์ ์ด ์žˆ๊ฑฐ๋‚˜ ๋ง‰ํžˆ๋Š” ๋ถ€๋ถ„์ด ์žˆ๋‹ค๋ฉด ๊ฐœ๋ฐœ์ž ์ปค๋ฎค๋‹ˆํ‹ฐ์— ์งˆ๋ฌธํ•˜๊ฑฐ๋‚˜, ๊ณต์‹ ๋ฌธ์„œ๋ฅผ ์ฐธ๊ณ ํ•˜๋Š” ๊ฒƒ๋„ ์žŠ์ง€ ๋งˆ! ํ•จ๊ป˜ ์„ฑ์žฅํ•˜๋Š” ๊ฐœ๋ฐœ์ž๊ฐ€ ๋˜์ž! ๐Ÿ’ปโœจ

๐Ÿ”— ์œ ์šฉํ•œ ๋ฆฌ์†Œ์Šค

โ€ข MDN Web Components ๋ฌธ์„œ
โ€ข Web Components ๊ณต์‹ ์‚ฌ์ดํŠธ (webcomponents.org)
โ€ข Open Web Components ํ”„๋กœ์ ํŠธ
โ€ข Lit (์›น ์ปดํฌ๋„ŒํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ)
โ€ข Stencil (์›น ์ปดํฌ๋„ŒํŠธ ์ปดํŒŒ์ผ๋Ÿฌ)

์ž, ์ด์ œ ์ง์ ‘ ๋งŒ๋“ค์–ด๋ณผ ์‹œ๊ฐ„์ด์•ผ! ์—ฌ๋Ÿฌ๋ถ„๋งŒ์˜ ๋ฉ‹์ง„ ์›น ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž! ํ™”์ดํŒ…! ๐ŸŽ‰

์›น ์ปดํฌ๋„ŒํŠธ๋กœ ๋” ๋‚˜์€ ์›น์„ ๋งŒ๋“ค์–ด์š”!
๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.