소프트웨어를 Model(모델), View(뷰), Controller(컨트롤러)라는 개념으로 역할을 나누어 일련의 작업을 처리하는 개발 방법론
Controller
가 받음Controller
는 요청한 결과를 처리하기 위해 Model
을 호출Model
은 비즈니스 로직을 통해 데이터를 처리한 후 결과를 Controller
에게 반환.Controller
는 반환받은 결과를 View
에 반영View
는 데이터를 받아온 화면을 사용자에게 보여줌데이터 로직 처리 부분, 데이터와 관련된 부분 담당하며 값과 기능을 가지는 객체
[규칙]
사용자 인터페이스 요소(화면), Controller에게 받은 Model의 데이터를 사용자에게 시각적으로 보여주는 역할
[규칙]
Model과 View 사이의 데이터 흐름 제어, Model과 View의 역할을 분리하는 중요한 요소
[규칙]
뼈대가 되는 HTML 파일 생성
main 태그를 타겟으로 JS로 화면을 동적으로 생성할 예정
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script defer type="module" src="./main.js"></script>
<title>JS로 MVC 패턴 적용하기</title>
</head>
<body>
<div id="app">
<main>
</main>
</div>
</body>
</html>
main.js에서 Model과 View를 선언하고, Controller에 주입시킴
// main.js
import Controller from './Controller.js';
import Model from './Model.js';
import View from './View.js';
new Controller(new View(), new Model());
Controller 정의
export default class Controller {
constructor(view, model) {
// view와 model 주입 받아 사용
this.view = view;
this.model = model;
}
}
Model 정의
// 애플리케이션에서 사용할 데이터 소지
export default class Model {
constructor() {
this.todoList = [
{
id: 0,
text: 'todo',
},
];
}
}
View 정의
export default class View {
constructor() {
// mvc.html에 있는 main을 target으로 설정
const target = document.querySelector('main');
// target 복사
this.$newEl = target.cloneNode(true);
// 복사한 Node에 innerHTML로 템플릿코드 넣어 DOM 형태로 만들기
this.$newEl.innerHTML = this.getTemplate();
// target에 새로 만든 DOM으로 교체
target.replaceWith(this.$newEl);
}
getTemplate() {
return `<ul></ul>`;
}
}
// View.js
export default class View {
(위의 코드와 동일)
// todoList 배열을 받아 ul 하위로 text 뿌리기
displayTodo(todoList) {
const ul = this.$newEl.querySelector('ul');
ul.innerHTML = `${todoList
.map(
(todo) => `
<li data-id="${todo.id}">
${todo.text} ${todo.id}
</li>
`
)
.join('')}`;
}
}
// Controller.js
export default class Controller {
constructor(view, model) {
// view와 model 주입 받아 사용
this.view = view;
this.model = model;
this.#render();
}
#render() {
// model로 부터 todoList 받아오기
const { todoList } = this.model;
// view에 todoList 전달
this.view.displayTodo(todoList);
}
}
<div id=’app’><main></main></div>
밖에 작성 안했지만, JS 코드로 DOM 생성여기서 View.js 파일의
$newEl
은 react와 vue에서 사용하는 Virtual DOM임!
Virtual DOM이란, 가상의 DOM으로 메모리에 저장되어 있는, 아직 실체화되지 않은 DOM이다. 따라서, JavaScript를 통해 DOM을 만들었으며 DOM을 실제로 브라우저에 부착하기 전까지는 메모리상에 존재하기만 한다.
// Model.js
export default class Model {
constructor() {
this.todoList = [
{
id: 0,
text: 'todo',
},
];
}
// 투두 리스트에 할일 추가 기능
addTodo(todo) {
this.todoList = [...this.todoList, todo];
}
// 투두 리스트 순서 뒤집기
get reverseTodoList() {
return [...this.todoList].reverse();
}
}
export default class Controller {
constructor(view, model) {
// view와 model 주입 받아 사용
this.view = view;
this.model = model;
this.#render();
// view의 AddEvent 메서드 실행
// AddEvent: DOM에서 일어나는 이벤트 리스너를 초기화하는 함수
this.view.addEvent({
// .bind(this): handleAddTodo와 handleReverseTodo 메서드가 View에서 실행될때,
// View에서 실행되는 this와 Controller의 this는 다르기 때문에 명시적 바인딩해주기 위해 사용
handleAddTodo: this.handleAddTodo.bind(this),
handleReverseTodo: this.handleReverseTodo.bind(this),
});
}
#render() {
// model로 부터 todoList 받아오기
const { todoList } = this.model;
// view에 todoList 전달
this.view.displayTodo(todoList);
}
/**
* 사용자 이벤트가 일어날 경우 실행되는 함수는 관례적으로 handle.. 또는 on.. 으로 짓는다.
* react에서는 handle 이라는 prefix 사용
*/
// 할일 추가
handleAddTodo(todo) {
this.model.addTodo(todo);
this.#render();
}
// 투두 리스트 뒤집기
handleReverseTodo() {
const { reverseTodoList } = this.model;
this.view.displayTodo(reverseTodoList);
}
}
export default class View {
(위와 동일)
// Controller에서 바인딩된 메서드들이 넘어옴
addEvent(handlers) {
this.$newEl.addEventListener('click', this.#runDomEvents(handlers), true);
}
// click 이벤트가 일어나면 실행될 메서드
#runDomEvents({ handleAddTodo, handleReverseTodo }) {
return ({ target }) => {
// add_button을 클릭하면 Controller의 handleAddTodo 메서드 실행
if (target.classList.contains('add_button')) {
const $lastElement = this.$newEl.querySelector('li:last-child');
const nextId = Number($lastElement.dataset.id) + 1;
const todo = {
id: nextId,
text: `todo`,
};
handleAddTodo(todo);
}
// reverse_button을 클릭하면 Controller의 handleReverseTodo 메서드 실행
if (target.classList.contains('reverse_button')) {
handleReverseTodo();
}
};
}
getTemplate() {
return `
<ul></ul>
<button class="add_button">Add</button>
<button class="reverse_button">Reverse</button>
`;
}
(위와 동일)
}
GUI를 가진 소프트웨어를 객체 지향적으로 잘 구조화하기 위해 생겨남!
MVC 패턴의 본질적인 목표는 관심사를 분리하는 것(Model과 View를 분리하는 것)
1979년 제록스 팔로알토 연구소에서 최초의 태블릿 PC인 ‘다이나북’을 개발하는 과정에서 전문 지식 여부나 남녀노소 상관없이 모두가 직관적으로 사용할 수 있는 GUI 개발
이 과정에서 Trygve Reenskaug
는 ‘사용자가 세상을 인식하는 방법(멘탈 모델)과 컴퓨터가 정보를 인식하고 처리하는 방법(컴퓨터 모델)이 다르다’라는 점을 인식하게 됨.
GUI를 사용하는 애플리케이션은 UI 관련된 코드와 데이터 저장/처리 코드는 특성과 역할이 뚜렷하기 다르다고 판단함
비즈니스 로직과 시각적인 UI, 둘 사이를 연결해주는 부분을 코드 안에서 분리하고 역할 부여해주어야 한다고 생각함
명확한 역할 분리로 인해 서로간 결합도 낮출 수 있음
Model, View, Controller 3가지 컴포넌트로 명확하기 구분됨
코드의 재사용성 및 확장성
Model과 Controller는 여러 View에서 재사용 가능하며, View도 다른 Model과 함께 재사용 가능
서비스 유지보수 및 테스트 용이
변경이 필요한 부분을 쉽게 파악하고, 수정이나 확장할 경우 해당 부분에만 집중하여 개발 가능
개발자 간의 커뮤니케이션 효율성 높임
Model과 View의 의존성을 완전히 분리시킬 수 없음
복잡한 구조의 애플리케이션의 경우, 하나의 Controller에 다수의 View와 Model이 복잡하게 연결되어 서로간 의존성이 커지는 상활 발생할 수 있음
Controller 역할이 과도하게 커지면 Massive-View-Controller 현상 피할 수 없음
하나의 Controller에 수많은 View와 Model이 연결되어 있어 Controller의 부하가 커지게됨
Model은 Controller와 View에 의존하지 않아야 한다.
⇒ Model 내부에 Controller와 View에 관련된 코드가 있으면 X
View는 Model에만 의존해야 하고, Controller에는 의존하면 안됨
View가 Model로부터 데이터를 받을 때는 사용자마다 다르게 보여주어야 하는 데이터만 받아야함
Controller는 Model과 View에 의존해도 됨
View가 Model로부터 데이터를 받을때, 반드시 Controller에서 받아야함
[참고 사이트]
여기도 MVC, 저기도 MVC! MVC 패턴이 뭐야?