바닐라 자바스크립트로 웹 컴포넌트 만들기 - 1. 문제점 발견하기

ChoiYongHyeun·2024년 1월 24일
0

프로그래밍 공부

목록 보기
2/17
post-thumbnail
post-custom-banner

공부하게 된 이유

리액트 공부를 시작하기 전 문득

"아 내가 리액트가 필요하다고 느낄 만큼 자바스크립트를 충분히 썼나?"

라는 생각이 들었다.

나는 겨우 자바스크립트 서적을 1회독 했을 뿐인데 건방지게 모듈을 사용해도 되나 라는 걱정이 들어

바닐라 자바스크립트로 블로그를 만들어보자고 생각했다.

예전에 아~주 야매스럽게 토이프로젝트들을 해봤으니 이번에는

모던 프론트엔드 스타일로 웹 컴포넌트들을 만들어서 하려고 한다.

컴포넌트를 만들어서 하는거를 모던 프론트 엔드 개발이라고 하는 기사를 봤다.

그!래!서!

바닐라 자바스크립트로 구성한 웹 컴포넌트 글을 보고

코드들을 해석하고 체득하며 웹 컴포넌트를 생성하는 방법들을 리뷰해보려고 한다.

이 글을 참고했어요

Vanilla Javascript로 웹 컴포넌트 만들기 - 개발자 황준일

바닐라 자바스크립트로 웹 컴포넌트 만들기 하면 나오는 많은 블로그들이 참고한 블로그이다.
엄청나게 정리가 잘 되어있고 설명도 매우 친절하다

블로그 글 원작자분이 친절하게도 출처만 남기면 재사용해도 무방하다고 한다 ><


컴포넌트 지향 개발

예전에는 서버 상에서 DOM 을 새로 구성하여 페이지 자체를 넘겨주었다면
(Sever Side Rendering)

이제는 클라이언트 사이드에서 클라이언트 페이지의 상태가 변경됨에 따라 DOM 을 변경하는 Client Side Rendering 방법이 부상했다.

이 때 컴포넌트 지향 개발은 상태가 변경된 컴포넌트들을 새로 렌더링 하기 위해 조작하는데 있어 큰 도움이 되었다.

이에 컴포넌트 지향 개발을 위한 다양한 라이브러리들이 출시 되었으며

현재까지도 많이 이용되고 있다. (React , Vue ... etc)

컴포넌트란 페이지를 구성하는 단위 요소들을 의미한다.
자세한 내용은 다른 게시글에서 다루도록 해야겠다.

장점은 이뿐만이 아니며 동일한 패턴을 가진 컴포넌트들을 재사용함으로서 컴포넌트의 수정 및 관리가 편하다.


컴포넌트 지향이 아닌 일반적인 방법

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div class="app">
      <ul>
        <li>item1</li>
        <li>item2</li>
      </ul>
      <button onclick="addItem()">추가</button>
    </div>
  </body>
  <script>
    const $app = document.querySelector('.app');
    const $ul = $app.querySelector('ul');
    const $button = $app.querySelector('button');
    let items = [...$ul.children].map((li) => li.textContent);

    function addItem() {
      items = [...items, `item${items.length + 1}`];
      console.log(items);

      $app.innerHTML = `
      <ul>
        ${items.map((item) => `<li>${item}</li>`).join('')}
      </ul>
      <button>추가</button>
      `;
    }
  </script>
</html>

다음처럼 추가 버튼을 누르면 ul 태그 내의 li 태그가 하나씩 올라가는 코드를

컴포넌트 지향 개발 방법으로 개발한다고 해보자

개발하기 전에, 컴포넌트 지향 개발이 왜 필요한지 생각해보자

일반적인 이 방법이 뭐가 문제일까 ?

lacks modularity and resuabilty

모듈성이 부족하고 재사용성이 낮다는 것이 가장 큰 문제이다.

만약 해당 태그에서 추가 버튼이 아니라 다른 버튼들을 해당 태그에 추가하거나

이런 로직을 페이지의 다른 부분에 또 추가 할 것이라고 생각해보자

그렇게 되면 동일한 코드들을 또 ~ 작성해야 한다.

이런 것을 모듈성이 부족하고 재사용성이 낮다고 한다.


컴포넌트 방식으로 바꿔보자

html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div class="app"></div>
  </body>
  <script src="index.js"></script>
</html>

우선 html 부분이 매우 간단해졌다.

해당 로직들을 담는 컨테이너 역할을 할 div 태그 하나만 만들어두자

컴포넌트 형식으로 만들기 전, 상태 지향 렌더링에 대해 한 번 더 생각해보자

추가 버튼을 누르면 ul 태그 내의 li 태그가 늘어나고 해당 부분이 새롭게 렌더링 된다.

그러니 추가 버튼을 누르기 전 상태에서 , 추가 버튼을 누른 후 상태가 변경되었기 때문에 클라이언트는 새로운 페이지를 볼 필요가 있고

이에 클라이언트 지향 개발에서는 상태가 변경되면 변경된 부분만 새롭게 렌더링 한다.

// 여기서 상태는 해당 배열의 변화일 것이다.
items = [...$ul.children].map((li) => li.textContent);

이렇게 컴포넌트의 상태를 관리하고 , 상태가 변경되면 컴포넌트를 새롭게 렌더링 하는

컴포넌트 클래스를 만들어보자

class Component {
  $target;
  $state;

  constructor($target, $state) {
    this.$target = $target;
    this.$state = $state;
    this.setup();
    this.render();
  }
  /**
  비어있는 메소드들은 모두 상속 후 오버라이딩 대상이다.
  */

  templet() {
    return '';
  }

  setup() {}

  render() {
    this.$target.innerHTML = this.templet();
    this.setEvent(); // 재 렌더링 이후에는 이벤트 핸들러를 새롭게 등록해줘야 한다.
    	// 새롭게 렌더링 된 태그에는 이벤트 핸들러가 등록되어 있지 않기 때문이다.
  }

  setState(newState) {
    /**
    newState 도 객체 형태이며
    스프레드 문법을 이용하여 this.state 객체를 새로 생성 
    newState 에도 동일한 프로퍼티가 있을 경우 
    newState 의 프로퍼티가 덮어씌워짐 (OverRiding)
     */
    this.state = { ...this.state, ...newState };
    this.render(); // 상태가 변화되면 새롭게 렌더링 되도록 
  }

  setEvent() {}
}

Component 클래스는 생성되는 즉시 this.setup()this.render() 가 호출되며 페이지에 렌더링 된다.

templet() , setup() , setState() , setEvent() 메소드가 비어있는 이유는

해당 클래스를 상속받아 작성될 컴포넌트들에서 오버라이딩 할 것이기 때문이다.

Component 클래스에서 templet$target 내부에 들어갈 기본적인 태그의 형태들을 담는다.

정리

Component 의 기본 개념은 호출되는 순간 기본 templet 에 정의된 내용으로 상태 설정 , 렌더링 및 이벤트 핸들러가 등록되며
기본적으로 이벤트 핸들러는 상태를 변화 시킨다.

상태가 변화되면 렌더링은 새롭게 된다.

그럼 위에서 말했던 양상을 상속 받아 작성해보자

class App extends Component {
  /**
    상속을 통해 인스턴스와 메소드 상속 
    일부 메소드는 오버라이딩하자
   */
  templet() {
    // App 컴포넌트의 기본 템플릿
    const { items } = this.state;

    return `
      <ul>
        ${items.map((item) => `<li>${item}</li>`).join('')}
      </ul>
      <button>추가</button>`;
  }

  setup() {
    this.state = { items: ['item1', 'item2'] };
  }

  setEvent() {
    const $button = this.$target.querySelector('button');
    $button.addEventListener('click', () => {
      const { items } = this.state;
      this.setState({
        items: [...items, `item${items.length + 1}`],
      });
    });
  }
}

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

Component 클래스를 상속 받은 App 이란 클래스를 만들어 호출시켜주자.

그러면 templet() 내에 있는 태그들이 .app 태그 내부에 기본적으로 들어가지며, 내부 태그에 이벤트 핸들러도 등록된다.

더 복잡해진 것처럼 보이는데 장점이 뭔가요 ?

우선 Component 라는 클래스를 상속 시킴으로서

모든 태그 생성 방식들을 통일 시켜 , 생성법이 관리하기가 수월해졌다는 것이 큰 장점이다.

또한 재사용성이 높아졌다. 얼마나 높아졌는지 체감해보자

상황 : .app 태그 밑에 똑같은 로직을 갖는 태그를 추가하고 싶음

하지만 item1 이런식이 아니라 이번엔 한국말로 아이템1 , 아이템2 ..

    <div class="app"></div>
    <div class="app-korean"></div> <!--새롭게 추가-->
class AppKorean extends App {
  setup() {
    this.state = { items: ['아이템1', '아이템2'] };
  }

  setEvent() {
    const $button = this.$target.querySelector('button');
    $button.addEventListener('click', () => {
      const { items } = this.state;
      this.setState({
        items: [...items, `아이템${items.length + 1}`],
      });
    });
  }
}

new App(document.querySelector('.app'));
new AppKorean(document.querySelector('.app-korean'));

불필요한 태그들을 동일하게 적을 필요 없이 몇 가지 메소드만 필요에 따라 오버라이딩 하고 생성하고 호출만 해주면 된다.

오 마이 갓

태그 작성법에 Component 라는 클래스를 이용하도록 강제성을 넣어줌으로서 일관적이며 재사용성 높은 태그 관리가 가능하다.


컴포넌트 리팩토링

문제점 1. 이벤트 핸들러 등록을 꼭 오버라이딩 해야 할까 ?


class Component {
	
  .
  .
    render() {
    this.$target.innerHTML = this.templet();
    this.setEvent();
  }
  .
  .
  
  setEvent() {} // Component 에서 오버라이딩을 권장하고 있다.
}

class App extends Component {
	.
    .
    .

  setEvent() {
    const $button = this.$target.querySelector('button');
    $button.addEventListener('click', () => {
      const { items } = this.state;
      this.setState({
        items: [...items, `item${items.length + 1}`],
      });
    });
  }
}

setEvent() 메소드를 살펴보면 Component 클래스 내 메소드에서는

생성되는 컴포넌트마다 개별적으로 setEvent() 메소드를 오버라이딩 하도록 권장하고 있다.

이벤트 핸들러를 등록하는 일은

  1. 이벤트 핸들러를 등록할 태그를 식별자를 이용해 찾는다.
  2. 해당 태그에 이벤트 핸들러 프로퍼티와 콜백 함수를 등록한다.

이 두가지 로직만을 갖는다.

그런데 이 로직들은 이벤트 핸들러를 갖는 컴포넌트 마다 동일하기 때문에 Component 단에서 정의해주자

class Component {
	
  .
  .
    render() {
    this.$target.innerHTML = this.templet();
    this.setEvent();
  }
  .
  .
  

  setEvent() {}

	// 새로운 메소드 추가 
	// 특정 식별자를 가진 태그에 이벤트 핸들러를 등록하는 메소드
  addEvent(eventType, selector, callback) {
    this.$target.addEventListener(eventType, (event) => {
      if (!event.target.closest(selector)) return false; 
      /**
      컴포넌트 전체에 이벤트 핸들러를 위임한 후 
      이벤트 타겟이 매개변수로 전달한 셀렉터가 아니면 종료
      */

      callback(event); // 셀렉터면 콜백함수 실행
    });
  }
}

Component 단에서 해당 컴포넌트에 이벤트 핸들링을 위임해주고

해당 selector 가 아니라면 이벤트 핸들러를 호출하지 않도록 하자

class App extends Component {
  .
  .
  .
  setEvent() {
    const addItem = () => {
      const { items } = this.state;
      this.setState({
        items: [...items, `item${items.length + 1}`],
      });
    };

    this.addEvent('click', 'button', addItem);
  }
.
.
.
}

그 다음 setEvent() 메소드에서 Component 단에서 설정한 this.addEvent 메소드를 호출함으로서 위에서 말했던

이벤트 지정 당할 태그 선택 , 콜백 함수 전달과 같이 반복적으로 행해지는 일을 함수를 이용해 간략하게 할 수 있었다.

문제점 2. 렌더링 시 마다 이벤트 핸들러를 등록해야 하나 ?

경우에 따라 다르겠지만 내가 원하는 컴포넌트의 경우에는 이벤트 핸들러가 담당 될 button 은 상태 변화와 전혀 상관이 없다.

그리고 대부분의 경우도 그렇다.

그럼에도 불구하고 Component 클래스를 보면

class Component {
	
  .
  .
    render() {
    this.$target.innerHTML = this.templet();
    this.setEvent();
  }
  .
  .
}

처럼 새로 렌더링 될 때 마다 이벤트를 설정하고 있다.

이를 바꿔주자

class Component {
  $target;
  $state;

  constructor($target, $state) {
    this.$target = $target;
    this.$state = $state;
    this.setup();
    this.render();
    this.setEvent();
  }
	.
    .
}

constructor 안에서 호출 시켜줌으로서 렌더링 될 때 마다 이벤트 핸들러를 등록해주는 것이 아니라 처음 호출 될 때 등록시켜주도록 바꿔주었다.

문제점 3. 부족한 모듈화

모듈화란 시스템을 더 작고 독립적이며 상호 교환 가능한 모듈이나 구성 요소로 나누는것과 관련된 소프트웨어 설계 기술이다.

소프트웨어를 관리하기 편하게 하기 위해 독립적인 단위로 구성하여 유지 관리성 및 확장성 및 재사용성을 향상 시키는 것이다.

마치 커다란 비행기를 조립하기 위해, 머리 , 몸통 , 날개 , 꼬리 분으로 나누고

또 몸통 부분은 창문, 좌석, 천장 .. 등등으로 나누고

또 좌석은 쿠션 , 팔걸이, 머리받이 등으로 나누고

더 잘게 나누면 사용하는 천, 사용하는 플라스틱 , 못 등으로 나누는 것이다.

만약 한 파일 내에서 다양한 컴포넌트들을 모아둔다면

이는 조립에 사용되는 모든 물품을 분류해두지 않고 한 통 안에 모두 모아둔 것과 같다.

모듈화를 위해 적합하게 폴더 구조를 나눠보자

index.html 과 연동 될 script 파일과 해당 스크립트 파일에서 사용 할 다양한 자바스크립트 파일들을 나누자

<!DOCTYPE html>
<html lang="en">
  .
  .
  <body>
    <div class="app"></div>
  </body>
  <script src="/src/app.js" type="module"></script>
</html>

index.html 에서 모듈화 시킨 자바스크립트 파일을 연동 할 때는 type = "module" 로 항상 명시해줘야 한다.

그 이유는 밑 설명을 참고하자

브라우저 모듈

├── index.html
├── core/
│   └── Component.js /* Component 클래스 */
└── src/
    ├── Components/
    │   └── Items.js /* 생성한 컴포넌트들이 담긴 폴더*/
    └── app.js /* index.html 과 연동 될 엔트리 파일 */

파일 구조를 보면 다음처럼 파일들을 나눠놔주자

각 파일은 모듈화를 위해 적절하게 export 시켜주고 import 를 시켜주자

APP 으로 정의됐던 클래스 명은 Items 로 변경되었다.

이제 앞으로 새로운 컴포넌트가 생성 될 때 마다 components 폴더 내에서 컴포넌트 별로 생성해주고 사용하면 된다.


기능 추가

만약 해당 컴포넌트에 다양한 기능을 추가하고 싶다고 해보자

각 아이템들은 추가 버튼이 아닌 input 태그를 이용해 아이템이 추가되고

각 아이템마다 활성화 , 비활성화 버튼을 가지고 있다고 해보자

다음 버튼들을 통해 전체 보기, 활성화 보기, 비활성화보기 등으로 조건에 따라 렌더링 되어야 한다.

그리고 각 버튼들은 삭제 버튼을 가지고 있으며 삭제 버튼이 눌리면

해당 아이템을 제외한 컴포넌트가 렌더링 되어야 한다.

관리해야 하는 상태

컴포넌트 지향 개발에서 컴포넌트가 렌더링 되는 조건은 상태에 종속된다.

관리해야 하는 상태가 어떻게 구성되어야 하는지 생각해보자

기능이 하나밖에 없었을 때는 관리해야 하는 상태는 오로지 item 들을 담은 items 배열뿐이였다.

추가 버튼을 누르면 배열에 item 이 하나 추가되어 items 배열의 상태가 변경되고

배열의 상태가 변경되면 렌더링을 새롭게 하면 된다.

여기서는 관리해야 하는 상태가 더 늘어난다.

우선 items 배열에서 관리해야 하는 상태는 item의 내용 뿐이 아니라 활성화여부 상태도 관리해야 한다.

또한 삭제 버튼이 눌렸을 때 삭제 해야 할 아이템을 관리하기 위해 인덱스 역할을 할 seq 도 관리해야 한다.

items : [{
	seq : 1,
    content : item1,
    active : true
},
..
]

그러니 items 배열을 다음과 같이 item의 상태를 담은 객체들을 담은 관리하자

그리고 전체 보기, 활성화 보기, 비활성화 보기 버튼은 items 배열을 필터링 할 상위단계의 상태에 영향을 미친다.

상위의 상태가 올바른 표현인지는 모르겠지만
item 들의 상태를 담은 배열에서 관리해야 하는 것이 아닌
items 배열에 영향을 미치는 상위 단계의 상태이다.

그러니 isFilter 라는 상태 프로퍼티를 생성해주고

버튼이 눌릴 때 마다 isFilter 프로퍼티의 값이 변경되도록 하자

    this.state = {
      isFilter: 0,
      items: [
        {
          seq: 1,
          content: 'item1',
          active: false,
        },
        { seq: 2, content: 'item2', active: true },
      ],
    };
  }
  /**
  전체보기 : 0,
  활성화보기 : 1,
  비활성화보기 : 2
  */

그러니 상태는 이런식으로 변경되어야 한다.

잘못된 컴포넌트 추가

그럼 수정해야 하는 요소들을 기존 Item 컴포넌트에 추가해보자

import Component from '../core/Component.js';

export default class Items extends Component {
  /**
    상속을 통해 인스턴스와 메소드 상속
    일부 메소드는 오버라이딩하자
   */

  get filteredItems() {
    /*
    조건에 따른 아이템 필터링
    전체보기일 경우엔 모든 아이템을 보고 
    활성화 보기 일때에는 활성화된 아이템만 보고 
    비활성화 보기 일 때에는 비활성화된 아이템만 보기
    */
    const { isFilter, items } = this.state;
    return items.filter(
      ({ active }) =>
        isFilter === 0 ||
        (isFilter === 1 && active === true) ||
        (isFilter === 2 && active === false),
    );
  }

  templet() {
    // App 컴포넌트의 기본 템플릿

    return `
    <header><input type = 'text' class = 'appender' placeholder = '아이템 내용 입력'></header>
    <main>  
    <ul>
        ${this.filteredItems
          .map(
            ({ seq, content, active }) =>
              `<li data-seq = ${seq}>${content}
              <button class = 'toggleBtn' style = 'color : ${
                active ? 'red' : 'blue'
              }'>${active ? '활성화' : '비활성화'}</button>
              <button class = 'deleteBtn'>삭제</button>
              </li>`,
          )
          .join('')}
      </ul></main>
      <footer>
      <button class = 'filterBtn' data-is-filter = '0'>전체보기</button>
      <button class = 'filterBtn' data-is-filter = '1'>활성화보기</button>
      <button class = 'filterBtn' data-is-filter = '2'>비활성화보기</button>

      </footer>
      `;
  }

  setup() {
    this.state = {
      isFilter: 0,
      items: [
        {
          seq: 1,
          content: 'item1',
          active: false,
        },
        { seq: 2, content: 'item2', active: true },
      ],
    };
  }

  setEvent() {
    // input 에 대한 이벤트 핸들러 등록
    this.addEvent('keyup', '.appender', ({ key, target }) => {
      if (key !== 'Enter') return;

      const { items } = this.state;
      // 다음 seq 넘버는 현재 아이템의 seq 넘버들의 최대값보다 1커야함
      const seq = Math.max(...items.map((v) => v.seq)) + 1;
      const content = target.value;
      const active = false; // active 상태의 디폴트 값은 false

      this.setState({
        items: [...items, { seq, content, active }],
      });
    });
    // deleteButton 에 대한 이벤트 핸들러 등록
    this.addEvent('click', '.deleteBtn', ({ target }) => {
      const { items } = this.state;
      const targetSeq = Number(target.closest('[data-seq]').dataset.seq);
      const targetIndex = items.findIndex((v) => v.seq === targetSeq);

      items.splice(targetIndex, 1);
      this.setState({ items });
    });
    // togleBtn 에 대한 이벤트 핸들러 등록
    this.addEvent('click', '.toggleBtn', ({ target }) => {
      const { items } = this.state;
      const targetSeq = Number(target.closest('[data-seq]').dataset.seq);
      const targetIndex = items.findIndex((v) => v.seq === targetSeq);
      items[targetIndex].active = !items[targetIndex].active;

      this.setState({ items });
    });
    // filterBtn 에 대한 이벤트 핸들러 등록
    this.addEvent('click', '.filterBtn', ({ target }) => {
      this.setState({ isFilter: Number(target.dataset.isFilter) });
    });
  }
}

으악

컴포넌트가 너무 무거워졌다.

위에서 컴포넌트는 페이지를 구성하는 요소들 중 최소한의 단위로 관리하는 것이

재사용성에서 좋다고 하였는데

하나의 컴포넌트에 모든 기능들을 넣다 보니 컴포넌트가 더 복잡하고 무거워졌다.

그래도 로직을 살펴보자

export default class Items extends Component {
  /**
    상속을 통해 인스턴스와 메소드 상속
    일부 메소드는 오버라이딩하자
   */

  get filteredItems() {
    /*
    조건에 따른 아이템 필터링
    전체보기일 경우엔 모든 아이템을 보고 
    활성화 보기 일때에는 활성화된 아이템만 보고 
    비활성화 보기 일 때에는 비활성화된 아이템만 보기
    */
    const { isFilter, items } = this.state;
    return items.filter(
      ({ active }) =>
        isFilter === 0 ||
        (isFilter === 1 && active === true) ||
        (isFilter === 2 && active === false),
    );
  }
  ...

우선 getter 메소드로 filteredItems 를 정의해주자

해당 메소드는 items 배열에서 isFilter 상태값에 따라 전체보기일 때는 모든 배열을 반환하고, 활성화 보기일때는 활성화 상태인 아이템만 담긴 배열을 반환, 비활성화보기일 때는 비활성화 아이템만 담긴 배열을 반환한다.

  templet() {
    // App 컴포넌트의 기본 템플릿

    return `
    <header><input type = 'text' class = 'appender' placeholder = '아이템 내용 입력'></header>
    <main>  
    <ul>
        ${this.filteredItems
          .map(
            ({ seq, content, active }) =>
              `<li data-seq = ${seq}>${content} // 커스텀 어트리뷰트
              <button class = 'toggleBtn' style = 'color : ${
                active ? 'red' : 'blue'
              }'>${active ? '활성화' : '비활성화'}</button>
              <button class = 'deleteBtn'>삭제</button>
              </li>`,
          )
          .join('')}
      </ul></main>
      <footer>
      <button class = 'filterBtn' data-is-filter = '0'>전체보기</button>
      <button class = 'filterBtn' data-is-filter = '1'>활성화보기</button>
      <button class = 'filterBtn' data-is-filter = '2'>비활성화보기</button>

      </footer>
      `;
  }

컴포넌트의 구성이 변경되었으니 templet() 메소드도 변경해줘야 한다.

아이템들의 인덱스 역할을 해주기 위해 Custom-Attributedata-seq = ${seq}li 태그 안에 정의해주자

이렇게 되면 li.dataset 에서 dataSeq : ${seq}seq 값에 접근 가능하다.

그리고 활성화/비활성화 상태를 변경 시킬 버튼과 필터링 상태를 변경할 버튼들을 하단에 추가해주었다.

이 때도 커스텀 어트리뷰트를 사용한다.

그럼 이렇게 태그는 템플릿으로 생성되었으니 이벤트 핸들러들을 장착해주자

  setEvent() {
    // input 에 대한 이벤트 핸들러 등록
    this.addEvent('keyup', '.appender', ({ key, target }) => {
      if (key !== 'Enter') return;

      const { items } = this.state;
      // 다음 seq 넘버는 현재 아이템의 seq 넘버들의 최대값보다 1커야함
      const seq = Math.max(...items.map((v) => v.seq)) + 1;
      const content = target.value;
      const active = false; // active 상태의 디폴트 값은 false

      this.setState({
        items: [...items, { seq, content, active }],
      });
    });
    // deleteButton 에 대한 이벤트 핸들러 등록
    this.addEvent('click', '.deleteBtn', ({ target }) => {
      const { items } = this.state;
      const targetSeq = Number(target.closest('[data-seq]').dataset.seq);
      const targetIndex = items.findIndex((v) => v.seq === targetSeq);

      items.splice(targetIndex, 1); // items 에서 해당 아이템 삭제
      this.setState({ items });
    });
    // togleBtn 에 대한 이벤트 핸들러 등록
    this.addEvent('click', '.toggleBtn', ({ target }) => {
      const { items } = this.state;
      const targetSeq = Number(target.closest('[data-seq]').dataset.seq);
      const targetIndex = items.findIndex((v) => v.seq === targetSeq);
      items[targetIndex].active = !items[targetIndex].active;
      // active 상태 반대로 변경
      // 이렇게 되면 재렌더링 될 때 active 상태에 따라 버튼의 값이 달라짐

      this.setState({ items });
    });
    // filterBtn 에 대한 이벤트 핸들러 등록
    this.addEvent('click', '.filterBtn', ({ target }) => {
      /*
      ifFilter 의 상태를 변경한다. 
      이 때도 버튼에 담긴 Custom Attribute 값을 이용해 설정한다.
      */
      this.setState({ isFilter: Number(target.dataset.isFilter) });
    });
  }

.closest 를 이욯애 이벤트가 일어난 태그를 선택하고

해당 태그의 커스텀 어트리뷰트에 지정되어 있는 값들을 이용해 다양한 이벤트를 핸들링한다.

.closest 에 대한 내용과 DOM 관련 내용은 추가로 더 공부해서 다뤄봐야겠다.

여기서 포인트는 모든 이벤트 핸들러는 this.setState() 메소드로 상태를 변경하고 재 렌더링 한다는 것이다.

컴포넌트 구성의 문제점 확인

컴포넌트가 너무나도 무겁고 복잡해졌다.

재사용성과 관리성을 높이기 위해서는 컴포넌트는 최소한의 일만 해야 한다.

단일 책임 원칙을 따라야 한다.

그러니 이 무거운 컴포넌트를 가볍게 덜어내보자


완성된 컴포넌트

완성된 컴포넌트에 대한 내용은 글이 너무 길어져서 다음 글로 ~ !

다음 글은 스스로 만들어 본 내용을 정리해야겠다.

profile
빨리 가는 유일한 방법은 제대로 가는 것이다
post-custom-banner

0개의 댓글