JS로 MVC 패턴 구현하기

박찬욱·2023년 9월 18일

TIL

목록 보기
13/21

MVC 패턴이란 영역을 Model, View, Controller로 나누어 작업하는 디자인 패턴을 의미한다. 중요한 포인트는 각각의 영역에 맞는 역할을 명확하게 부여해야한다는 점이다.

Model은 데이터와 비즈니스로직에 관련된 영역이고 View는 레이아웃과 화면을 다루는 역할이다. 그리고 Controller는 사용자의 명령을 받고 Model과 View를 이어주는 부분이다.

Model과 View는 반드시 Controller를 통해서 소통해야한다. 직접적인 소통을 하는 경우에는 각각의 역할에 의미가 없어질 수 있고 Model에 있어야하는 데이터가 View에 있는 등 문제가 발생할 수 있다.

[참고 블로그]

구현 코드

간단한 Todo App을 MVC 패턴을 활용해서 구현해보았다.

Model

export default class TodoModel {
  constructor() {
    this.todoList = [
      {
        id: 0,
        text: 'todo',
      },
    ];
  }

  addTodo(todo) {
    this.todoList = [...this.todoList, todo];
  }

  get reverseTodoList() {
    return this.todoList.reverse();
  }

  deleteTodo(id) {
    this.todoList.splice(
      this.todoList.findIndex((todo) => String(todo.id) === id),
      1
    );
    return this.todoList;
  }
}

데이터와 데이터를 처리하는 로직들이 들어있다.

View

View는 꼭 가지고 있어야하는 기능들을 추상화시켜서 상속을 통해 구현했다.

// View
export default class View {
  constructor(target) {
    this.$newEl = target.cloneNode(true);
    this.$newEl.innerHTML = this.getTemplate();
    target.replaceWith(this.$newEl);
  }

  getTemplate() {
    return `
    <ul></ul>
    <form>
      <input type='text' />
      <button type='submit' class="add_button">Add</button>
    </form>
    <button class="reverse_button">Reverse</button>`;
  }

  displayTodo(todoList) {
    const ul = this.$newEl.querySelector('ul');
    ul.innerHTML = `
    ${todoList
      .map(
        (todo) =>
          `<li data-id='${todo.id}'><p>${todo.text}</p><button data-id='${todo.id}' class='deleteBtn'>삭제</button></li>`
      )
      .join('')}
    `;
  }

  addEvent() {}
  runDomEvents() {}
}
import View from '../core/View.js';

export default class TodoView extends View {
  constructor() {
    super(document.querySelector('main'));
    this.$input = document.querySelector('input');
  }
  addEvent(handlers) {
    this.$newEl.addEventListener('click', this.runDomEvents(handlers), true);
    this.$newEl.addEventListener('submit', this.runDomEvents(handlers), true);
  }

  runDomEvents({ handleAddTodo, handleReverseTodo, handleDeleteTodo }) {
    return (e) => {
      if (e.type === 'submit') {
        const $lastElement = this.$newEl.querySelector('li:last-child');
        e.preventDefault();
        if ($lastElement === null) {
          const newTodo = {
            id: 0,
            text: e.target[0].value,
          };
          handleAddTodo(newTodo);
        } else {
          const newTodoId = Number($lastElement.dataset.id) + 1;
          const newTodo = {
            id: newTodoId,
            text: e.target[0].value,
          };
          handleAddTodo(newTodo);
        }
        this.$input.value = '';
        this.$input.focus();
      }

      if (e.type === 'click' && e.target.classList.contains('reverse_button')) {
        handleReverseTodo();
      }

      if (e.type === 'click' && e.target.classList.contains('deleteBtn')) {
        handleDeleteTodo(e.target.dataset.id);
      }
    };
  }
}

추상화를 시켜두니 TodoView에 작성해야할 코드가 많이 줄었다.

그리고 Controller에서 이벤트 핸들러 함수를 전달하면 addEvent 메서드를 통해 해당 노드들에게 이벤트 핸들러를 달아주고 있다.
이렇게 Controller에서 이벤트 핸들러를 전달해주는 이유는 Model과 View가 직접적인 소통을 하지 않기위한 Controller의 역할이다.

예를 들어 사용자가 입력버튼을 클릭하면 Todo가 추가가 되는 기능이 있다. 사용자가 클릭이벤트를 발생시키면 addEvent에 걸어둔 클릭 이벤트 핸들러가 작동할 것이고 이것은 Controller에 정의되어있다. 곧바로 Model에 있는 addTodo를 실행시키는 것이 아니다. Controller를 한번 거쳐가는 것이다.

Controller

import { bindingMethods } from '../utils/eventUtils.js';

export default class TodoController {
  constructor(view, model) {
    this.view = view;
    this.model = model;
    this.#render();
    bindingMethods(this, 'handle');
  }

  handleAddTodo(todo) {
    this.model.addTodo(todo);
    this.#render();
  }

  handleReverseTodo() {
    this.model.reverseTodoList;
    this.#render();
  }

  handleDeleteTodo(id) {
    this.model.deleteTodo(id);
    this.#render();
  }

  #render() {
    const { todoList } = this.model;
    this.view.displayTodo(todoList);
  }
}

유저의 action을 받는 부분은 Controller이기 때문에 이벤트 핸들러를 등록해야되는 부분은 Controller가 되어야 옳지만 View에서 화면의 직접적인 랜더링이 이루어지기때문에 노드에 핸들러를 다는 코드를 View에 적어두었다.

현재 Controller는 Model과 View의 징검다리 역할로써 두 영역을 연결해주고 있다. 두 영역은 반드시 Controller를 통해 소통해야한다.

// main
import TodoController from './Todo/TodoController.js';
import TodoModel from './Todo/TodoModel.js';
import TodoView from './Todo/TodoView.js';

new TodoController(new TodoView(), new TodoModel());

main파일에서는 TodoController인스턴스를 생성하고 Model과 View를 주입받아 사용한다.

개선할 것들

  • View영역의 TodoList와 TodoItem을 컴포넌트로 나눠보기
  • localstorage를 활용해서 Todo 데이터를 저장해볼 것
profile
대체불가능한 사람이다

0개의 댓글