[Design Pattern] MVC Pattern

Main·2024년 8월 17일
0

Design Pattern

목록 보기
4/7
post-thumbnail

MVC Pattern ?

MVC 패턴이란 Model, View, Controller라는 계층 구조로 나뉘어진 패턴입니다.

Model

애플리케이션의 데이터와 비즈니스 로직을 담당하며, 데이터베이스와 상호작용하여 데이터를 저장하고, 데이터를 처리하는 역할을 합니다.

Modal의 규칙

  • 사용자가 사용하는 모든 데이터를 가지고 있어야 합니다.
  • view, controller의 어떤 정보도 포함되지 않아야 합니다.
  • 변경이 발생하면, 변경에 대한 알림 처리에 대해 구현해야합니다.

View

사용자에게 데이터를 표시하는 역할을 하며, 사용자가 볼 수 있는 인터페이스를 제공합니다.

View의 규칙

  • model이 가지고 있는 정보를 따로 저장하면 안됩니다.
  • Model 혹은 Controller와 같이 다른 구성요소들을 몰라야 합니다.
  • 변경이 발생하면, 변경에 대한 알림 처리에 대해 구현해야 합니다.

Controller

Modal View 사이의 상호작용을 관리하며, 사용자의 입력을 처리하고, 모델을 업데이트 및 업데이트된 모델을 기반으로 뷰를 다시 렌더링합니다.

Controller의 규칙

  • Model과 View에 대해 알아야합니다.
  • Model과 View의 변경사항을 구독해야합니다.

MVC 패턴의 동작 방식

MVC 패턴의 작동 원리는 다음과 같습니다:

  • 사용자 요청: 사용자가 애플리케이션에 특정 요청을 합니다(예: 버튼 클릭).
  • 컨트롤러 처리: 이 요청은 컨트롤러로 전달됩니다. 컨트롤러는 사용자의 요청을 해석하고, 필요한 데이터를 모델에서 가져오거나 모델의 데이터를 업데이트합니다.
  • 모델 갱신: 모델이 데이터의 상태를 변경하거나 갱신합니다.
  • 뷰 업데이트: 모델이 변경되면 뷰가 이를 감지하고 화면에 새로운 데이터를 반영합니다. 사용자는 변경된 데이터를 화면에서 확인할 수 있습니다.

MVC 패턴의 장점

  • 유지보수성: 각 구성 요소가 독립적이기 때문에 코드의 수정이 쉬워집니다.
  • 재사용성: 모델, 뷰, 컨트롤러를 각각 독립적으로 개발하고 재사용할 수 있습니다.
  • 유연성: 다양한 UI로 쉽게 확장할 수 있으며, MVC 패턴은 웹 애플리케이션뿐만 아니라 데스크톱 애플리케이션에서도 활용됩니다.

MVC 패턴의 단점

  • 복잡성 증가: 애플리케이션이 단순한 경우에도 MVC 구조를 적용하면 코드가 불필요하게 복잡해질 수 있습니다. 특히, 작은 프로젝트에서는 과도한 설계로 인해 개발 속도가 느려질 수 있습니다.
  • 의존성 문제: MVC 패턴을 제대로 구현하지 않으면, 모델, 뷰, 컨트롤러 간의 의존성이 강해질 수 있습니다. 특히, 뷰와 모델 사이의 데이터 전달이나, 컨트롤러의 역할이 모호해지는 경우 코드가 복잡해지고 유지보수가 어려워질 수 있습니다.
  • 데이터 흐름의 어려움: MVC 구조에서 데이터의 흐름이 명확하지 않으면, 특히 복잡한 애플리케이션에서 데이터가 어떻게 전달되고 처리되는지 이해하기 어려울 수 있습니다. 이는 디버깅을 어렵게 만들 수 있습니다.
  • 이벤트 처리의 복잡성: 사용자 인터페이스가 복잡한 경우, 다양한 이벤트를 처리하기 위해 컨트롤러가 복잡해질 수 있습니다. 이벤트 처리 로직이 증가하면서 컨트롤러 코드가 장황해지고 관리가 어려워질 수 있습니다.

MVC 패턴으로 TodoList 구현하기

todoList-model.ts
애플리케이션의 데이터와 비즈니스 로직을 관리합니다.

Todo 항목을 추가, 삭제하거나 완료 상태를 토글하는 기능을 제공합니다.

export type Todo = {
  id: string;
  content: string;
  isDone: boolean;
};

export class TodoListModel {
  todoList: Todo[];

  constructor() {
    this.todoList = [];
  }

  addTodo(content: string) {
    const newTodo: Todo = {
      id: new Date().getTime().toString(),
      content,
      isDone: false,
    };
    this.todoList.push(newTodo);
  }

  removeTodo(id: string) {
    this.todoList = this.todoList.filter((todo) => todo.id !== id);
  }

  toggleDone(id: string) {
    const todo = this.todoList.find((todo) => todo.id === id);
    if (todo) {
      todo.isDone = !todo.isDone;
    }
  }
}

todoList-view.ts
사용자에게 화면에 보여지는 View를 담당합니다.

TodoList를 렌더링하고, 사용자 입력을 처리하는 이벤트를 모델이나 컨트롤러에 전달합니다.

import { Todo } from './todoList-model';

export class TodoListView {
  formElement: HTMLFormElement;
  todoListElement: HTMLUListElement;

  constructor() {
    this.formElement = document.getElementById('todo-form')! as HTMLFormElement;
    this.todoListElement = document.getElementById(
      'todos-ul',
    )! as HTMLUListElement;
  }

  bindAddTodo(handler: (content: string) => void) {
    this.formElement.addEventListener('submit', (e) => {
      e.preventDefault();
      const input = this.formElement.querySelector('input') as HTMLInputElement;

      const inputValue = input.value;

      if (inputValue.trim()) {
        handler(inputValue);
        input.value = '';
      }
    });
  }

  bindRemoveTodo(handler: (id: string) => void) {
    this.todoListElement.addEventListener('click', (event) => {
      const target = event.target as HTMLButtonElement;
      if (target && target.classList.contains('delete')) {
        const id = target.dataset.id!;
        handler(id);
      }
    });
  }

  bindToggleDone(handler: (id: string) => void) {
    this.todoListElement.addEventListener('click', (event) => {
      const target = event.target as HTMLInputElement;
      if (target && target.classList.contains('check-box')) {
        const id = target.dataset.id!;
        handler(id);
      }
    });
  }

  renderTodoList(todoList: Todo[]) {
    this.todoListElement.innerHTML = '';
    todoList.forEach((todo) => {
      const li = `
      <li id=${todo.id}} style=display:flex;align-items:center;gap:8px>
        <input class=check-box ${todo.isDone && 'checked'} data-id=${todo.id} type=checkbox />
        <p style=${todo.isDone && 'text-decoration:line-through;opacity:0.5;'}>${todo.content}</p>
        <button class=delete data-id=${todo.id}>remove</button>
        </li>
      `;
      this.todoListElement.insertAdjacentHTML('afterbegin', li);
    });
  }
}

todoList-controller.ts

Model과 View 사이의 중개 역할을 합니다.

사용자의 입력을 받아서 Model을 업데이트하고, 업데이트된 Model의 상태를 View에 반영합니다.

import { TodoListModel } from './todoList-model';
import { TodoListView } from './todoList-view';

export class TodoListController {
  model: TodoListModel; // 모델 인스턴스
  view: TodoListView; // 뷰 인스턴스

  constructor(model: TodoListModel, view: TodoListView) {
    this.model = model;
    this.view = view;

    // 뷰에서 발생한 이벤트를 처리하기 위해 핸들러를 바인딩
    this.view.bindAddTodo(this.handleAddTodo);
    this.view.bindRemoveTodo(this.handleRemoveTodo);
    this.view.bindToggleDone(this.handleToggleDone);

    // 초기 렌더링
    this.view.renderTodoList(this.model.todoList);
  }

  handleAddTodo = (content: string) => {
    // 새로운 Todo 항목을 추가하고 리스트를 다시 렌더링
    this.model.addTodo(content);
    this.view.renderTodoList(this.model.todoList);
  };

  handleRemoveTodo = (id: string) => {
    // 특정 Todo 항목을 삭제하고 리스트를 다시 렌더링
    this.model.removeTodo(id);
    this.view.renderTodoList(this.model.todoList);
  };

  handleToggleDone = (id: string) => {
    // 특정 Todo 항목의 완료 상태를 토글하고 리스트를 다시 렌더링
    this.model.toggleDone(id);
    this.view.renderTodoList(this.model.todoList);
  };
}

index.html

<!doctype html>
<html lang="ko-KR">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>MVC-ToodoList</title>
  </head>
  <body>
    <div id="app">
      <form id="todo-form">
        <input id="todo-input" placeholder="Please enter the task." />
        <button id="submit-btn" type="submit">Add</button>
      </form>
      <ul id="todos-ul"></ul>
    </div>
  </body>
</html>

index.ts

import { TodoListModel } from './src/todoList-model';
import { TodoListView } from './src/todoList-view';
import { TodoListController } from './src/todolist-controller';

document.addEventListener('DOMContentLoaded', () => {
  // model 생성
  const model = new TodoListModel();
  // view 생성
  const view = new TodoListView();
  // controller 생성 인자 값으로 model 인스턴스와 view 인스턴스 전달
  new TodoListController(model, view);
});

MVC 패턴을 이용하여 간단한 TodoList를 구현해 보았습니다.

위의 TodoList 예시에서 알 수 있듯이, 프론트엔드에서 MVC 패턴을 사용할 경우 코드의 복잡성이 증가하고, 컨트롤러가 비대해지며, 데이터와 UI 간의 동기화를 효율적으로 처리하기 어려울 수 있습니다. 특히, 현대적인 프레임워크들이 등장하면서 MVC 패턴의 단점을 보완하고, 더 직관적이고 효율적인 방법으로 프론트엔드 애플리케이션을 구축할 수 있게 되었습니다. 따라서, 오늘날 프론트엔드 개발에서는 MVC 패턴 대신 컴포넌트 기반 아키텍처, 단방향 데이터 흐름 패턴(Flux 패턴) 등이 더 많이 사용됩니다.


정리

MVC 패턴은 Model, View, Controller 계층 구조로 나뉘어진 패턴입니다.

Model은 데이터 및 비즈니스 로직을 관리하며, 데이터의 상태를 유지하고, 데이터 변경 시 뷰에 통지하는 역할도 포함됩니다.

View는 사용자에게 제공할 화면을 표시하며, 사용자의 입력을 받을 수 있는 UI 요소를 포함합니다.

Controller는 사용자와 상호작용하며, 모델과 뷰 간의 데이터 흐름을 조정하는 역할을 합니다. 사용자 입력에 따라 모델을 업데이트하고, 변경된 데이터를 뷰에 반영합니다.

MVC 패턴은 유지보수성, 재사용성, 유연성 등의 장점이 존재하지만, 현재의 프론트엔드에서는 ModelView의 복잡성 증가 및 데이터 흐름 파악이 어려워 MVC 패턴을 잘 사용하지 않으며, 대신 컴포넌트 기반 아키텍처, 단방향 데이터 흐름 패턴(Flux 패턴) 등이 더 많이 사용됩니다.


참고 사이트

https://developer.mozilla.org/ko/docs/Glossary/MVC

profile
함께 개선하는 프론트엔드 개발자

0개의 댓글