vanillaJS Todo App refactoring - 웹 컴포넌트화하기

hyoon·2022년 3월 12일
post-thumbnail

들어가기에 앞서며

이전에 블랙커피 Vanilla JS Lv1. 문벅스 카페 메뉴 앱 만들기 강의를 수강하면서 VanillaJS Black Coffee App 프로젝트를 통해 vanillaJS로 상태관리가 가능한 app을 만들어 볼 수 있었다. react나 vue와 같은 프레임워크 없이 JS 만으로도 상태관리가 가능하게 구현할 수 있다는 점이 흥미로웠고 그렇다면 vanillaJS로 얼추 react와 비슷하게 컴포넌트 단위로 개발해볼 수도 있지 않을까? 싶었다. 그렇게 자료를 찾던 중 VanillaJs로 웹 컴포넌트 만들기 라는 너무 멋진 레퍼런스를 발견했다! 이 자료를 참고하여 이전에 기능별로 모듈화하는 방식으로 구현했던 노마드코더 vanillaJS Chrome App을 react 처럼 state를 기준으로 dom이 랜더링될 수 있도록 웹 컴포넌트화하는 방식의 리팩토링을 진행해보았다.


리팩토링 요구사항

  • 상태관리가 가능한 어플리케이션

    • react의 라이프사이클 방식을 고려하여 구현
  • 컴포넌트 단위로 분리하자.

    • 상위 컴포넌트에서 state를 관리하자. with 불변성 유지
    • 하위 컴포넌트들은 state와 state를 수정하는 메소드를 전달받을 뿐 직접 state를 변경할 수 없다.
  • 추가 기능 구현

    • todo 전체 삭제
    • todo 수정
    • todo 전체 개수 나타내기

1. 컴포넌트 추상화

class Component 정의

  1. constructor 메소드

    • 객체 초기화 과정
      • 컴포넌트에서 사용할 props 초기화
      • 객체 생성 시 초기화 과정, render 과정, render 후 그려진 dom에 event 등록 과정 수행
  2. template 메소드

    • 화면에 나타낼 html 지정하기
  3. setState 메소드

    • state 변경하기 (App 컴포넌트에서 진행)
      • state가 변경되었다면 컴포넌트 랜더링하기
      • 랜더링 후 처리할 작업은 componentDidMount 메소드에서 작업하기
  4. event 메소드

    • dom 요소에 이벤트 등록하기

class Component 코드

export default class Component {
        state
    constructor({ target, props } ){
        this.$target = target
        this.props = props
        this.init()
        this.render()
        this.event()
    }

    init(){}

    template(){
        return '';
    }
    componentDidMount(){}

    render(){
        this.$target.innerHTML = this.template();
        this.componentDidMount();
    }
    
    event(){}

    setState(newData){
        this.state = {...this.state, ...newData};
        this.render();
    }
}

target 프로퍼티는 어떤 dom에 컴포넌트를 동적으로 랜더할 것인지를 나타내기 위해 지정하는 프로퍼티이다.


2. App컴포넌트에서 state와 메소드 정의

state 정의

constructor 메소드를 통해서 App 객체를 생성할 시점에 state를 초기화해준다.
사용자 이름 정보, todo 정보, 날씨 정보를 담을 state를 props로 넘겨주며 init()시에 state 필드에 설정해준다.

new App({target: $('#root'),
        props: {
            user: getStorage(USER_LS) ? getStorage(USER_LS) : '', 
            todos: getStorage(TODO_LS) ? getStorage(TODO_LS) : [], 
            weather: {}
        }
});

state를 변경하는 메소드 정의

state가 변경되면 render 함수가 호출되어야 하며, state를 기준으로 dom은 render 되게 작성해준다.


3. mount 시점에 하위 컴포넌트 생성

App 컴포넌트가 랜더된 후 하위 컴포넌트들을 생성해주기 위하여 componentDidMount 메소드에서 생성자 함수를 호출한다. 이때 초기화를 위하여 props를 넘겨준다.

componentDidMount(){
...
	new TodoList({target: $('.todo-list-wrapper'),
          props: {
              todoList: this.state.todos,
              completeTodo: this.completeTodo.bind(this),
              editTodo: this.editTodo.bind(this),
              deleteTodo: this.deleteTodo.bind(this),
              deleteAllTodo: this.deleteAllTodo.bind(this)
         }
    })
}

이때 아래와 같이 메소드의 참조값을 넘기게 될 경우, 메소드를 호출하는 시점에는 기본 바인딩이 적용되어 전역을 가리키게 된다.

completeTodo: this.completeTodo

따라서 bind함수를 통해서 바인딩할 this를 매개변수로 넘겨 해당 this로 바인딩된 새로운 함수를 생성해주어야 한다.

completeTodo: this.completeTodo.bind(this)

4. 하위 컴포넌트에서 state와 메소드 사용

하위 컴포넌트들은 props로 전달받은 메소드를 사용할 뿐, 직접 state를 변경하지 않는다.

event(){
       const { completeTodo, editTodo, deleteTodo, deleteAllTodo } = this.props;
       
       this.$target.addEventListener('click', (e)=> {
       
           if(e.target.classList.contains('completeBtn')) {
               completeTodo(todoId); //전달받은 메소드 호출
               return;
           }
       }
}

5. 이벤트 위임

dom 요소에 이벤트를 등록할 당시에 토글버튼, 삭제버튼 등등의 element가 dom에 아직 존재하지 않는다. 이렇게 동적으로 dom 요소가 생성될 때는 존재하는 상위 요소에 이벤트를 등록하면 된다.

하위요소 각각에 이벤트를 등록하지 않고, 상위 요소에 이벤트를 등록하여 하위 요소들의 이벤트를 제어하는 것을 이벤트 위임이라 한다.

event(){
       const { completeTodo, editTodo, deleteTodo, deleteAllTodo } = this.props;

       this.$target.addEventListener('click', (e)=> {
       
           if(!e.target.classList.contains('btn')) return;

           if(e.target.classList.contains('deleteAllBtn')){
            	deleteAllTodo();
            	return;
            }

           const $li = e.target.closest('li');
           const todoId = $li.dataset.todoLiId;
           const prevValue = $li.querySelector('label').innerText;
           
           if(e.target.classList.contains('completeBtn')) {
               completeTodo(todoId);
               return;
           }
           
           if(e.target.classList.contains('editBtn')){
               const newValue = window.prompt('내용을 입력해주세요!', prevValue);
               if(!newValue || newValue == prevValue) return;
               editTodo(todoId, newValue);
               return;
           }
           
           if(e.target.classList.contains('deleteBtn')) {
               deleteTodo(todoId);
               return;
           }
       })
   }

느낀 점

처음에 컴포넌트를 추상화하는 작업이 생각보다 어려웠다. 객체들의 공통적인 특징을 정의해야 하는데 class형 컴포넌트를 많이 작성해보지 못해서 힘들었던 것 같다. 그래서 이번 기회에 객체지향 개념을 확실히 학습하자! 해서 class와 상속에 대해 어렴풋이 알았던 개념을 확실하게 학습을 하는 계기가 되었다. 또 이렇게 컴포넌트로 구조화하여 작성하니 코드를 구조적으로 작성할 수 있고 유지보수하기에도 훨씬 수월해진 것 같다고 느꼈다.

vanillaJS로만 상태를 관리하고 state가 변경되었을 때만 랜더링이 일어날 수 있게끔 해야 하는 점이나 이벤트를 등록하고 렌더링하는 시점들을 깊게 고려하며 코드를 짜다보니 정말 라이브러리가 얼마나 편리함을 제공하는 지를 매우 매우 느낄 수 있는 과정이였다.

profile
FE Developer

0개의 댓글