JavaScript

๋ฐ”๋‹๋ผ JS๋กœ ์›น ์ปดํฌ๋„ŒํŠธ ๋งŒ๋“ค๊ธฐ

kohi โ˜• 2022. 12. 11. 15:17

๐Ÿ‘ฝ ์ปดํฌ๋„ŒํŠธ๋ž€?


 

ํ”„๋กœ๊ทธ๋ž˜๋ฐ์— ์žˆ์–ด ์žฌ์‚ฌ์šฉ์ด ๊ฐ€๋Šฅํ•œ ๊ฐ๊ฐ์˜ ๋…๋ฆฝ๋œ ๋ชจ๋“ˆ์„ ๋œปํ•œ๋‹ค. ๋ ˆ๊ณ  ๋ธ”๋ก์ฒ˜๋Ÿผ ์ด๋ฏธ ๋งŒ๋“ค์–ด์ง„ ์ปดํฌ๋„ŒํŠธ๋“ค์„ ์กฐํ•ฉํ•˜์—ฌ ํ™”๋ฉด์„ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค. ์›น ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ด์šฉํ•˜์—ฌ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋ฉด 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


https://junilhwang.github.io/TIL/Javascript/Design/Vanilla-JS-Component/#_3-component-core-%E1%84%87%E1%85%A7%E1%86%AB%E1%84%80%E1%85%A7%E1%86%BC

 

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

https://joshua1988.github.io/web-development/javascript/event-propagation-delegation/#%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%B2%84%EB%B8%94%EB%A7%81---event-bubbling

 

์ด๋ฒคํŠธ ๋ฒ„๋ธ”๋ง, ์ด๋ฒคํŠธ ์บก์ฒ˜ ๊ทธ๋ฆฌ๊ณ  ์ด๋ฒคํŠธ ์œ„์ž„๊นŒ์ง€

(๊ธฐ๋ณธ) ์ด๋ฒคํŠธ ๋ฒ„๋ธ”๋ง, ์ด๋ฒคํŠธ ์บก์ฒ˜๋ง, ๊ทธ๋ฆฌ๊ณ  ์ด๋ฒคํŠธ ์œ„์ž„๊นŒ์ง€ ์ด๋ฒคํŠธ ์ „๋‹ฌ ๋ฐฉ์‹๊ณผ ๊ด€๋ จ๋œ ๋ชจ๋“  ๊ฒƒ์„ ํŒŒํ—ค์ณ ๋ด…๋‹ˆ๋‹ค.

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