구조
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.
구조는 다음과 같다. 중요한 것은 모델과 뷰는 서로를 모른다는 것이다. 모델과 뷰를 알고 있는 컨트롤러가 모델과 뷰의 메서드를 이용해 프로그램 로직을 정의한다.
데이터 흐름
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 };
}
...
}