๋ฐ๋๋ผ JS๋ก ์น ์ปดํฌ๋ํธ ๋ง๋ค๊ธฐ
๐ฝ ์ปดํฌ๋ํธ๋?
ํ๋ก๊ทธ๋๋ฐ์ ์์ด ์ฌ์ฌ์ฉ์ด ๊ฐ๋ฅํ ๊ฐ๊ฐ์ ๋
๋ฆฝ๋ ๋ชจ๋์ ๋ปํ๋ค. ๋ ๊ณ ๋ธ๋ก์ฒ๋ผ ์ด๋ฏธ ๋ง๋ค์ด์ง ์ปดํฌ๋ํธ๋ค์ ์กฐํฉํ์ฌ ํ๋ฉด์ ๊ตฌ์ฑํ ์ ์๋ค. ์น ์ปดํฌ๋ํธ๋ฅผ ์ด์ฉํ์ฌ ์ฝ๋๋ฅผ ์์ฑํ๋ฉด Vue๋ React์ ๊ฐ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ, ํ๋ ์์ํฌ์ ์์กดํ์ง ์๊ณ ์ํธ ์ด์ฉ์ด ๊ฐ๋ฅํ๊ฒ๋ ์์ฑํ ์ ์๋ค.
๐ฝ ๋ฐ๋๋ผ JS๋ก ์น ์ปดํฌ๋ํธ ๊ตฌํ
- index.html
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<title>Component EX</title>
</head>
<body>
<div id="app"></div>
<script src="./src/app.js" type="module"></script>
</body>
</html>
- src > app.js
import Items from "./components/Items.js";
class App {
constructor() {
const $app = document.querySelector("#app");
new Items($app);
}
}
new App();
- src > component > Item.js
import Component from "../core/Component.js";
export default class Items extends Component {
setup() {
this.$state = { items: ["item1", "item2"] };
}
template() {
const { items } = this.$state;
return `
<ul>
${items.map((item) => `<li>${item}</li>`).join("")}
</ul>
<button>์ถ๊ฐ</button>
`;
}
setEvent() {
this.$target.querySelector("button").addEventListener("click", () => {
const { items } = this.$state;
this.setState({ items: [...items, `item${items.length + 1}`] });
});
}
}
- src > core > Components.js
export default class Component {
$target;
$state;
constructor($target) {
this.$target = $target;
this.setup();
this.render();
}
setup() {}
template() {
return "";
}
render() {
this.$target.innerHTML = this.template();
this.setEvent();
}
setEvent() {}
setState(newState) {
this.$state = { ...this.$state, ...newState };
this.render();
}
}
์์ ์์ฑํ ์ฝ๋๋ฅผ ์ฌ์ฉํ๋ฉด render ์ ์ด๋ฒคํธ๊ฐ ์๋ก ๋ฑ๋ก๋๋ค. ๊ฐ๊ฐ์ ์์ดํ ์ ๋ํด ์ญ์ ๊ธฐ๋ฅ์ ์ถ๊ฐํ๋ค๊ณ ํ๋ฉด ๋ค์๊ณผ ๊ฐ๋ค.
- src > component > Item.js
import Component from "../core/Component.js";
export default class Items extends Component {
setup() {
this.$state = { items: ["item1", "item2"] };
}
template() {
const { items } = this.$state;
return `
<ul>
${items
.map(
(item, key) => `
<li>
${item}
<button class="deleteBtn" data-index="${key}">์ญ์ </button>
</li>
`
)
.join("")}
</ul>
<button class="addBtn">์ถ๊ฐ</button>
`;
}
setEvent() {
this.$target.querySelector(".addBtn").addEventListener("click", () => {
const { items } = this.$state;
this.setState({ items: [...items, `item${items.length + 1}`] });
});
this.$target.querySelectorAll(".deleteBtn").forEach((deleteBtn) =>
deleteBtn.addEventListener("click", ({ target }) => {
const items = [...this.$state.items];
items.splice(target.dataset.index, 1);
this.setState({ items });
})
);
}
}
๋ค์๊ณผ ๊ฐ์ด ์ด๋ฒคํธ ๋ฒ๋ธ๋ง์ ์ฌ์ฉํ๋ค๋ฉด ํจ์ฌ ์ง๊ด์ ์ผ๋ก ์ฒ๋ฆฌํ ์ ์๋ค.
- src > core > Components.js
import Component from "../core/Component.js";
export default class Items extends Component {
setup() {
this.$state = { items: ["item1", "item2"] };
}
template() {
const { items } = this.$state;
return `
<ul>
${items
.map(
(item, key) => `
<li>
${item}
<button class="deleteBtn" data-index="${key}">์ญ์ </button>
</li>
`
)
.join("")}
</ul>
<button class="addBtn">์ถ๊ฐ</button>
`;
}
setEvent() {
// ๋ชจ๋ ์ด๋ฒคํธ๋ฅผ this.$target์ ๋ฑ๋กํ์ฌ ์ฌ์ฉํ๋ฉด ๋๋ค.
this.$target.addEventListener("click", ({ target }) => {
const items = [...this.$state.items];
if (target.classList.contains("addBtn")) {
this.setState({ items: [...items, `item${items.length + 1}`] });
}
if (target.classList.contains("deleteBtn")) {
items.splice(target.dataset.index, 1);
this.setState({ items });
}
});
}
}
- src > component > Item.js
- ๊ธฐ์กด์ setEvent๋ render๋ฅผ ํ ๋๋ง๋ค ์คํ๋๋ฏ๋ก ๋ผ์ดํ ์ฌ์ดํด์ ๋ณ๊ฒฝํด์ผ ํ๋ค. event๋ฅผ ๊ฐ๊ฐ์ ํ์ ์์๊ฐ ์๋๋ผ component์ target ์์ฒด์ ๋ฑ๋กํ๋ ๊ฒ์ด๋ค. component๊ฐ ์์ฑ๋๋ ์์ ์๋ง ์ด๋ฒคํธ ๋ฑ๋ก์ ํด๋์ผ๋ฉด ์ถ๊ฐ๋ก ๋ฑ๋กํ ํ์๊ฐ ์์ด์ง๋ค.
export default class Component {
$target;
$state;
constructor($target) {
this.$target = $target;
this.setup();
this.setEvent();
this.render();
}
setup() {}
template() {
return "";
}
render() {
this.$target.innerHTML = this.template();
}
setEvent() {}
setState(newState) {
this.$state = { ...this.$state, ...newState };
this.render();
}
}
๐ก ์ด๋ฒคํธ ๋ฒ๋ธ๋ง
์ด๋ฒคํธ ๋ฒ๋ธ๋ง์ ํน์ ํ๋ฉด ์์์์ ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ์ ๋ ํด๋น ์ด๋ฒคํธ๊ฐ ๋ ์์์ ํ๋ฉด ์์๋ค๋ก ์ ๋ฌ๋์ด๊ฐ๋ ํน์ฑ์ ์๋ฏธํ๋ค.
- src > core > Components.js
- ์ด๋ฒคํธ ๋ฒ๋ธ๋ง์ ํตํ ๋ฑ๋ก ๊ณผ์ ์ ๋ฉ์๋๋ก ๋ง๋ค์ด์ ์ฌ์ฉํ๋ฉด ์ฝ๋๊ฐ ๋ ๊น๋ํด์ง๋ค.
export default class Component {
$target;
$state;
constructor($target) {
this.$target = $target;
this.setup();
this.setEvent();
this.render();
}
setup() {}
template() {
return "";
}
render() {
this.$target.innerHTML = this.template();
}
setEvent() {}
setState(newState) {
this.$state = { ...this.$state, ...newState };
this.render();
}
addEvent(eventType, selector, callback) {
const children = [...this.$target.querySelectorAll(selector)];
const isTarget = (target) => children.includes(target) || target.closest(selector);
this.$target.addEventListener(eventType, (event) => {
if (!isTarget(event.target)) return false;
callback(event);
});
}
}
- ์ด๋ ๊ฒ ์์ฑํ ๋ฉ์๋๋ ๋ค์๊ณผ ๊ฐ์ด ์ฌ์ฉํ๋ค.
- src > component > Item.js
import Component from "../core/Component.js";
export default class Items extends Component {
setup() {
this.$state = { items: ["item1", "item2"] };
}
template() {
const { items } = this.$state;
return `
<ul>
${items
.map(
(item, key) => `
<li>
${item}
<button class="deleteBtn" data-index="${key}">์ญ์ </button>
</li>
`
)
.join("")}
</ul>
<button class="addBtn">์ถ๊ฐ</button>
`;
}
setEvent() {
this.addEvent("click", ".addBtn", ({ target }) => {
const { items } = this.$state;
this.setState({ items: [...items, `item${items.length + 1}`] });
});
this.addEvent("click", ".deleteBtn", ({ target }) => {
const items = [...this.$state.items];
items.splice(target.dataset.index, 1);
this.setState({ items });
});
}
}
๐ฝ ์ปดํฌ๋ํธ ๋ถํ
- index.html
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<title>Simple Component 8</title>
</head>
<body>
<div id="app"></div>
<script src="src/main.js" type="module"></script>
</body>
</html>
- src > main.js (๊ธฐ์กด์ app.js ํ์ผ)
import App from "./App.js";
new App(document.querySelector("#app"));
- src > App.js
import Component from "./core/Component.js";
import Items from "./components/Items.js";
import ItemAppender from "./components/ItemAppender.js";
import ItemFilter from "./components/ItemFilter.js";
export default class App extends Component {
setup() {
this.$state = {
isFilter: 0,
items: [
{
seq: 1,
contents: "item1",
active: false,
},
{
seq: 2,
contents: "item2",
active: true,
},
],
};
}
template() {
return `
<header data-component="item-appender"></header>
<main data-component="items"></main>
<footer data-component="item-filter"></footer>
`;
}
mounted() {
const { filteredItems, addItem, deleteItem, toggleItem, filterItem } = this;
const $itemAppender = this.$target.querySelector('[data-component="item-appender"]');
const $items = this.$target.querySelector('[data-component="items"]');
const $itemFilter = this.$target.querySelector('[data-component="item-filter"]');
new ItemAppender($itemAppender, {
addItem: addItem.bind(this),
});
new Items($items, {
filteredItems,
deleteItem: deleteItem.bind(this),
toggleItem: toggleItem.bind(this),
});
new ItemFilter($itemFilter, {
filterItem: filterItem.bind(this),
});
}
get filteredItems() {
const { isFilter, items } = this.$state;
return items.filter(({ active }) => (isFilter === 1 && active) || (isFilter === 2 && !active) || isFilter === 0);
}
addItem(contents) {
const { items } = this.$state;
const seq = Math.max(0, ...items.map((v) => v.seq)) + 1;
const active = false;
this.setState({
items: [...items, { seq, contents, active }],
});
}
deleteItem(seq) {
const items = [...this.$state.items];
items.splice(
items.findIndex((v) => v.seq === seq),
1
);
this.setState({ items });
}
toggleItem(seq) {
const items = [...this.$state.items];
const index = items.findIndex((v) => v.seq === seq);
items[index].active = !items[index].active;
this.setState({ items });
}
filterItem(isFilter) {
this.setState({ isFilter });
}
}
- src > components > ItemAppener.js
import Component from "../core/Component.js";
export default class ItemAppender extends Component {
template() {
return `<input type="text" class="appender" placeholder="์์ดํ
๋ด์ฉ ์
๋ ฅ" />`;
}
setEvent() {
const { addItem } = this.$props;
this.addEvent("keyup", ".appender", ({ key, target }) => {
if (key !== "Enter") return;
addItem(target.value);
});
}
}
- src > components > ItemFilter.js
import Component from "../core/Component.js";
export default class ItemFilter extends Component {
template() {
return `
<button class="filterBtn" data-is-filter="0">์ ์ฒด ๋ณด๊ธฐ</button>
<button class="filterBtn" data-is-filter="1">ํ์ฑ ๋ณด๊ธฐ</button>
<button class="filterBtn" data-is-filter="2">๋นํ์ฑ ๋ณด๊ธฐ</button>
`;
}
setEvent() {
const { filterItem } = this.$props;
this.addEvent("click", ".filterBtn", ({ target }) => {
filterItem(Number(target.dataset.isFilter));
});
}
}
- src > components > Items.js
import Component from "../core/Component.js";
export default class Items extends Component {
template() {
const { filteredItems } = this.$props;
return `
<ul>
${filteredItems
.map(
({ contents, active, seq }) => `
<li data-seq="${seq}">
${contents}
<button class="toggleBtn" style="color: ${active ? "#09F" : "#F09"}">
${active ? "ํ์ฑ" : "๋นํ์ฑ"}
</button>
<button class="deleteBtn">์ญ์ </button>
</li>
`
)
.join("")}
</ul>
`;
}
setEvent() {
const { deleteItem, toggleItem } = this.$props;
this.addEvent("click", ".deleteBtn", ({ target }) => {
deleteItem(Number(target.closest("[data-seq]").dataset.seq));
});
this.addEvent("click", ".toggleBtn", ({ target }) => {
toggleItem(Number(target.closest("[data-seq]").dataset.seq));
});
}
}
- src > core > Component.js
export default class Component {
$target;
$props;
$state;
constructor($target, $props) {
this.$target = $target;
this.$props = $props;
this.setup();
this.setEvent();
this.render();
}
setup() {}
mounted() {}
template() {
return "";
}
render() {
this.$target.innerHTML = this.template();
this.mounted();
}
setEvent() {}
setState(newState) {
this.$state = { ...this.$state, ...newState };
this.render();
}
addEvent(eventType, selector, callback) {
this.$target.addEventListener(eventType, (event) => {
if (!event.target.closest(selector)) return false;
callback(event);
});
}
}
๐ฝ Reference
Vanilla Javascript๋ก ์น ์ปดํฌ๋ํธ ๋ง๋ค๊ธฐ | ๊ฐ๋ฐ์ ํฉ์ค์ผ
Vanilla Javascript๋ก ์น ์ปดํฌ๋ํธ ๋ง๋ค๊ธฐ 9์์ ๋ฅ์คํธ ์คํ open in new window์์ ์งํํ๋ ๋ธ๋์ปคํผ ์คํฐ๋open in new window์ ์ฐธ์ฌํ๋ค. ์ด ํฌ์คํธ๋ ์คํฐ๋ ๊ธฐ๊ฐ๋์ ๊ณ์ ๊ณ ๋ฏผํ๋ฉฐ ๋ง๋ค์๋ ์ปดํฌ๋ํธ
junilhwang.github.io
https://github.com/JunilHwang/simple-component
GitHub - JunilHwang/simple-component: ๊ฐ๋จํ ์ปดํฌ๋ํธ๋ฅผ ๋ง๋๋ ์์
๊ฐ๋จํ ์ปดํฌ๋ํธ๋ฅผ ๋ง๋๋ ์์ . Contribute to JunilHwang/simple-component development by creating an account on GitHub.
github.com
https://developer.mozilla.org/ko/docs/Web/Web_Components
์น ์ปดํฌ๋ํธ | MDN
์น ์ปดํฌ๋ํธ๋ ๊ทธ ๊ธฐ๋ฅ์ ๋๋จธ์ง ์ฝ๋๋ก๋ถํฐ ์บก์ํํ์ฌ ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ์ปค์คํ ์๋ฆฌ๋จผํธ๋ฅผ ์์ฑํ๊ณ ์น ์ฑ์์ ํ์ฉํ ์ ์๋๋ก ํด์ฃผ๋ ๋ค์ํ ๊ธฐ์ ๋ค์ ๋ชจ์์ ๋๋ค.
developer.mozilla.org
์ด๋ฒคํธ ๋ฒ๋ธ๋ง, ์ด๋ฒคํธ ์บก์ฒ ๊ทธ๋ฆฌ๊ณ ์ด๋ฒคํธ ์์๊น์ง
(๊ธฐ๋ณธ) ์ด๋ฒคํธ ๋ฒ๋ธ๋ง, ์ด๋ฒคํธ ์บก์ฒ๋ง, ๊ทธ๋ฆฌ๊ณ ์ด๋ฒคํธ ์์๊น์ง ์ด๋ฒคํธ ์ ๋ฌ ๋ฐฉ์๊ณผ ๊ด๋ จ๋ ๋ชจ๋ ๊ฒ์ ํํค์ณ ๋ด ๋๋ค.
joshua1988.github.io
https://hanamon.kr/%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-component%EB%9E%80/
์ปดํฌ๋ํธ(Component)๋? - ํ๋๋ชฌ
์ปดํฌ๋ํธ(Component)๋ ํ๋ก๊ทธ๋๋ฐ์ ์์ด ์ฌ์ฌ์ฉ์ด ๊ฐ๋ฅํ ๊ฐ๊ฐ์ ๋ ๋ฆฝ๋ ๋ชจ๋์ ๋ปํ๋ค. ์ปดํฌ๋ํธ ๊ธฐ๋ฐ ํ๋ก๊ทธ๋๋ฐ์ ํ๋ฉด ๋ง์น ๋ ๊ณ ๋ธ๋ก์ฒ๋ผ ์ด๋ฏธ ๋ง๋ค์ด์ง ์ปดํฌ๋๋ค์ ์กฐํฉํ์ฌ ํ๋ฉด์ ๊ตฌ์ฑ
hanamon.kr