Vanila JavaScript로 웹 컴포넌트 만들기(2)

LOOPY·2022년 5월 18일
0

3. 이벤트 처리

(1) 삭제 기능 추가

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

(2) 이벤트 버블링

export default class Items extends Component {
  setup () {/* 생략 */}
  template () { /* 생략 */}
  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 });
      }

    });
  }
}

이벤트 버블링을 사용하면 훨씬 직관적으로 처리할 수 있는데, 다만 기존의 setEvent는 render를 할 때 마다 실행하기 때문에 core/Component.js에 라이프 사이클을 변경해야 한다.

 export default class Component {
   $target;
   $state;
   constructor ($target) {
     this.$target = $target;
     this.setup();
+    this.setEvent(); // constructor에서 한 번만 실행한다.
     this.render();
   }
   setup () {};
   template () { return ''; }
   render () {
     this.$target.innerHTML = this.template();
-    this.setEvent(); 
   }
   setEvent () {}
   setState (newState) {
     this.$state = { ...this.$state, ...newState };
     this.render();
   }
 }

이는 event를 각각의 하위 요소가 아니라 component의 target 자체에 등록하는 것이고, 따라서 component가 생성되는 시점에만 이벤트 등록을 해두면 추가로 등록할 필요가 없어진다.

(3) 이벤트 버블링 추상화

이벤트 버블링을 통한 등록 과정을 메소드로 만들어 사용하면 코드가 더 깔끔해진다.

export default class Component {
 $target;
 $state;
 constructor ($target) { /* 생략 */ }
 setup () { /* 생략 */ }
 template () { /* 생략 */ }
 render () { /* 생략 */ }
 setEvent () { /* 생략 */ }
 setState (newState) { /* 생략 */ }

 addEvent (eventType, selector, callback) {
   const children = [ ...this.$target.querySelectorAll(selector) ]; 
   // selector에 명시한 것 보다 더 하위 요소가 선택되는 경우가 있을 땐
   // closest를 이용하여 처리한다.
   const isTarget = (target) => children.includes(target)
                                || target.closest(selector);
   this.$target.addEventListener(eventType, event => {
     if (!isTarget(event.target)) return false;
     callback(event);
   })
 }

}
export default class Items extends Component {
  setup () { /* 생략 */ }
  template () {/* 생략 */ }
  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 });
    });
  }
}
profile
1.5년차 프론트엔드 개발자의 소소한 기록을 담습니다 :-)

0개의 댓글