바닐라 JS로 MVC To-Do List 구현하기

December 23, 2021

Vanilla JS MVC ToDoList

구조

Model–view–controller (MVC) is a software design pattern commonly used for developing user interfaces that divide the related program logic into three interconnected elements. This is done to separate internal representations of information from the ways information is presented to and accepted from the user.

구조는 다음과 같다. 중요한 것은 모델과 뷰는 서로를 모른다는 것이다. 모델과 뷰를 알고 있는 컨트롤러가 모델과 뷰의 메서드를 이용해 프로그램 로직을 정의한다.

class

데이터 흐름

data-flow

MVC에서 데이터 흐름은 유저가 뷰의 화면을 보고 입력을 하면 컨트롤러가 모델을 변경시키고 변경된 모델이 뷰를 업데이트한다. To-Do 아이템 추가 기능을 살펴보며 MVC의 데이터 흐름을 살펴보자.

뷰: 사용자 입력

먼저 뷰를 보자. 뷰의 bindClickAddItem 메서드는 함수를 받아 아이템 추가 인풋 요소의 엔터키 입력 이벤트에 바인딩하는 메서드이다. id가 add-item인 요소의 keyup 이벤트 발생 시 키가 엔터키라면 add-item 요소의 입력값 문자열을 받아 양단의 공백을 제거한 후 인자로 받은 콜백 함수에 인자로 전달 후 실행한다.

export default class View {
  ...

  bindEventListener(type, selector, callback) {
    const children = [...$all(selector, this.$app)];
    const isTarget = (target) =>
      children.includes(target) || target.closest(selector);

    this.$app.addEventListener(type, (e) => {
      if (!isTarget(e.target)) return;

      e.preventDefault();
      callback(e);
    });
  }

  bindClickAddItem(callback) {
    this.bindEventListener('keyup', '#add-item', (e) => {
      if (e.key !== ENTER_KEY) return;

      const item = $('#add-item').value.trim();

      if (item === '') return;callback(item);
    });
  }

  ...
}

컨트롤러: 모델, 뷰 메서드 바인딩

addItem 메서드는 컨트롤러가 생성될 때 뷰의 bindClickAddItem 메서드에 콜백 함수로 전달됨으로써 뷰의 To-Do 아이템 추가 버튼 클릭 이벤트 발생 시 실행된다. 뷰에서 전달받은 To-Do 아이템 문자열을 모델의 addItem 메서드에 전달한다. 그리고 모델의 메서드를 실행 후 결과를 result라는 객체에 담아 뷰의 render 메서드에 다시 전달한다.

export default class Controller {
  constructor(model, view) {
    this.model = model
    this.view = view

    this.view.bindClickAddItem(this.addItem.bind(this))
    ...
  }

  ...

  addItem(item) {
    this.model.addItem(item, result => {
      this.view.clearInput()
      this.view.render(result)
    })
  }

  ...
}

모델: 아이템 추가

앞에서 컨트롤러로부터 To-Do 아이템 문자열을 전달받은 모델의 addItem 메서드는 스토어에서 To-Do 아이템 리스트를 불러온 뒤 새로운 아이템을 추가하고 저장한다. 그리고 변경된 전체 To-Do 아이템 리스트를 콜백 함수로 받은 뷰의 render 메소드에 전달한다.

export default class Model {
  ...

  addItem(item, callback) {
    const todos = this.store.load();
    const newItem = {
      text: item,
      isCompleted: false,
    };

    todos.push(newItem);
    this.store.save(todos);
    callback(this._getResult(todos));
  }

  _getResult(todos) {
    return {
      todos: [...todos],
    };
  }

  ...
}

뷰: 렌더링

모델로부터 전달받은 To-Do 아이템 리스트에서 전체 항목과 완료된 항목 수를 카운트한 뒤 템플릿에 전달하여 화면을 생성하고 렌더링한다.

export default class View {
  ...

  render({ todos }) {
    this.$app.innerHTML = this.template.getHTML(todos, this._getCount(todos));
  }

  _getCount(todos) {
    const total = todos.length;
    const completed = todos.filter((todo) => todo.isCompleted).length;
    const active = total - completed;

    return { total, completed, active };
  }

  ...
}

추가 읽기


우정민

웹 개발, 프론트엔드