Ver3.0 ๐จ ์น ์ปดํฌ๋ํธ์ ์ปค์คํ ์๋ฆฌ๋จผํธ๋ก ๋ง๋๋ ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ UI ์ธ์

๐จ ์น ์ปดํฌ๋ํธ์ ์ปค์คํ ์๋ฆฌ๋จผํธ๋ก ๋ง๋๋ ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ UI ์ธ์
ํ ๋ฒ ๋ง๋ค์ด์ ์ด๋์๋ ์ฐ๋ ๋ง๋ฒ ๊ฐ์ ๊ฐ๋ฐ ์ด์ผ๊ธฐ
์๋
! ๐ ์ค๋์ ์น ๊ฐ๋ฐ์๋ผ๋ฉด ๊ผญ ์์์ผ ํ ์น ์ปดํฌ๋ํธ์ ์ปค์คํ
์๋ฆฌ๋จผํธ์ ๋ํด ์ด์ผ๊ธฐํด๋ณผ๊ฒ. ํน์ ๊ฐ์ UI๋ฅผ ์ฌ๋ฌ ๋ฒ ๋ง๋ค๋ฉด์ "์, ์ด๊ฑฐ ๋ณต์ฌ ๋ถ์ฌ๋ฃ๊ธฐ ํ๊ธฐ๋ ๊ท์ฐฎ์๋ฐ ํ ๋ฒ๋ง ๋ง๋ค์ด์ ๊ณ์ ์ธ ์ ์๋?" ํ๊ณ ์๊ฐํด๋ณธ ์ ์์ด? ๋ฐ๋ก ๊ทธ ๊ณ ๋ฏผ์ ํด๊ฒฐํด์ฃผ๋ ๊ฒ ์น ์ปดํฌ๋ํธ์ผ! ๐
์์ฆ ํ๋ก ํธ์๋ ๊ฐ๋ฐ ํธ๋ ๋๋ฅผ ๋ณด๋ฉด ์ปดํฌ๋ํธ ๊ธฐ๋ฐ ๊ฐ๋ฐ์ด ๋์ธ์์? React, Vue, Angular ๊ฐ์ ํ๋ ์์ํฌ๋ค์ด ์ธ๊ธฐ๋ฅผ ๋๋ ์ด์ ๋ ๋ฐ๋ก ์ด ์ปดํฌ๋ํธ ๊ฐ๋
๋๋ถ์ด์ผ. ๊ทธ๋ฐ๋ฐ ์น ํ์ค ์์ฒด์์๋ ์ด๋ฐ ์ปดํฌ๋ํธ๋ฅผ ๋ง๋ค ์ ์๋ค๋ ๊ฑฐ, ์๊ณ ์์์ด? ๐
๐ฏ ์น ์ปดํฌ๋ํธ๊ฐ ๋ญ๋ฐ?
์น ์ปดํฌ๋ํธ๋ ์ฝ๊ฒ ๋งํด์ ๋ ๊ณ ๋ธ๋ก ๊ฐ์ ๊ฑฐ์ผ. ๋ ๊ณ ๋ก ์ง์ ๋ง๋ค ๋ ์ฐฝ๋ฌธ ๋ธ๋ก, ๋ฌธ ๋ธ๋ก, ์ง๋ถ ๋ธ๋ก์ ์กฐ๋ฆฝํ๋ฏ์ด, ์นํ์ด์ง๋ ์ฌ๋ฌ ๊ฐ์ ๋
๋ฆฝ์ ์ธ ์ปดํฌ๋ํธ๋ฅผ ์กฐ๋ฆฝํด์ ๋ง๋๋ ๊ฑฐ์ง. ๐๏ธ
๋ ์ ํํ๊ฒ ๋งํ๋ฉด, ์น ์ปดํฌ๋ํธ๋ 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
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.