VanillaJS

원동환·2021년 9월 4일
0

VanillaJS로 웹컴포넌트 만들기

컴포넌트 단위로 구현을 해보니 React의 클래스형 컴포넌트와 거의 흡사한 형태로 짜여지는거같다.
기본 문법들이 조금씩 다른부분도 있지만 전체적인 데이터 흐름방식이나 단위단위로 쪼개서 구현하고 상위컴포넌트에서 구현하고 props를 내려주는 방식이 거의 일치한다.
전체적으로 간략히 설명하면 core 즉 component클래스에서 메소드들을 정의하고 각각의 컴포넌트들이 component 클래스를 상속받아서 메소드를 자체적으로 정의해서 쓰는 방식이다.
그리고 app.js에서 컴포넌트들을 합치고 클래스로 정의 한다음 main.js에서 실행을 한다.

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8"/>
    <title>VanillaJs Practice</title>
  </head>
<body>
    <h1>Example</h1>
    <div id="app"></div>
    <script src="./src/main.js" type="module"></script>
</body>
</html>

main.js

import app from './app.js';

new app(document.querySelector('#app'));

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 });
  }

}

app.js에서 state를 정의하고 template함수에서 컴포넌트들을 정의한 후에 mounted에서 자식 컴포넌트들을 마운트해준다.
그리고 컴포넌트당 하나의 객체로 만들고 bind를 사용해서 this를 변경하거나 혹은 새로운 함수를 만들어줘야한다.

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 (){}; 
    render () {
      this.$target.innerHTML = this.template();
      this.mounted();
    }
    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);
      })
    }
  }

위처럼 추상화한 클래스에서 함수들을 정의 해놓거나 addEvent이벤트같은 경우는 이벤트버블링을 설정해서 좀 더 알아보기 쉽게 처리할수 있다.

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));
    });

  }

}

이 컴포넌트는 Component 클래스를 상속받아서 각각의 요소들을 정의한다.

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));
    });
  }
}

ItemAppender.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);
    });
  }
}

위 두 컴포넌트도 마찬가지로 component클래스를 상속받아서 메소드를 자체적으로 정의해서 사용하고 있다. 이런식으로 구현을 하면 각각의 이벤트들을 좀더 유동적으로 가져갈 수 있고
컴포넌트들이 바꼈을때도 쉽게 대응을 할 수 있다.

profile
Mojittoba

0개의 댓글