그냥 공부 목적으로 번역을 하면서 읽은 것을 옮겨 적어보았습니다.
오역이 있다면 알려주세요 감사합니다 ~
Model View Controller 란 무엇인가 ?
MVC는 코드를 조직화를 위한 하나의 유명한 디자인 패턴입니다.
Model - App의 데이터를 관리합니다.
View - Model의 시각적인 표현을 담당합니다.
Controller - User와 System간을 연결합니다.
Model이 곧 데이터고 이 APP에서는 실질적인 투두리스트인것입니다. 그리고 메소드로 추가, 수정, 삭제를 가질 것입니다.
View는 데이터의 표현입니다. 이 어플리케이션에서는 HTML을 DOM과 CSS안에 렌더를 할 것입니다.
Controller는 Model과 View를 이어주며, 사용자의 입력과, 클릭, 타이핑 같은 유저 상호작용을 위한 콜백들을 다룹니다.
Model은 View를 간섭안하고 View 또한 Model을 간섭안합니다. Controller만이 그들을 연결해줍니다.
초기 세팅
이 앱은 완전히 JS로 이루어 질 예정이고, 모든 작업이 JS안에서 다뤄질것입니다. 그러므로 HTML에서는 Body에 오직 하나의 Root 엘리먼트만 가지게 됩니다.
/* HTML */
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Todo App</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div id="root"></div>
<script src="script.js"></script>
</body>
</html>
또한 약간의 CSS를 더했는데, 이 글의 핵심적 요소는 아니기때문에 넘어가겠습니다. 여기서 코드를 확인하실 수 있습니다.
Getting Started
무슨 클래스가 이 MVC의 어떤 부분에 존재하는지 쉽게 이해하기 위해 코드를 간단히 해보았습니다. 여기서 Model, View 클래스 그리고 두 가지를 가지고 있는 Controller 클래스를 만들었습니다. app은 Controller의 인스턴스가 될 것입니다. 굉장히 추상적이고 좋습니다.
class Model {
constructor() {}
}
class View {
constructor() {}
}
class Controller {
constructor(model, view) {
this.model = model
this.view = view
}
}
const app = new Controller(new Model(), new View())
Model
3 파트중에 제일 간단한 Model에 한번 보겠습니다. 모델은 DOM 조작과 관련된 어떤 이벤트에도 속해 있지 않습니다. 데이터를 저장하고, 수정만합니다.
// Model
class Model {
constructor() {
// The state of the model, an array of todo objects, prepopulated with some data
this.todos = [
{id: 1, text: 'Run a marathon', complete: false},
{id: 2, text: 'Plant a garden', complete: false},
]
}
addTodo(todoText) {
const todo = {
id: this.todos.length > 0 ? this.todos[this.todos.length - 1].id + 1 : 1,
text: todoText,
complete: false,
}
this.todos.push(todo)
}
// Map through all todos, and replace the text of the todo with the specified id
editTodo(id, updatedText) {
this.todos = this.todos.map((todo) =>
todo.id === id ? {id: todo.id, text: updatedText, complete: todo.complete} : todo,
)
}
// Filter a todo out of the array by id
deleteTodo(id) {
this.todos = this.todos.filter((todo) => todo.id !== id)
}
// Flip the complete boolean on the specified todo
toggleTodo(id) {
this.todos = this.todos.map((todo) =>
todo.id === id ? {id: todo.id, text: todo.text, complete: !todo.complete} : todo,
)
}
}
여기서 우리는 addTodo, editTodo, deleteTodo, toggleTodo를 가집니다. 이 메소드들은 굉장히 직관적입니다. 새로운 투두를 배열에 넣어주고, 투두리스트 중 아이디를 찾아 수정하고 대체하고, 배열을 필터를 통해 삭제해주고, 투두의 complete 불린 속성을 스위칭해줍니다.
app은 윈도창에서도 접근이 가능합니다. 간단히 테스트해보실 수 있습니다.
이걸로 당장은 Model로서는 충분합니다. 마지막에 우리는 투두리스트들 로컬 스토리지에 저장하여 반영구적으로 만들것입니다. 하지만 일단은 새로고침을 할때마다 데이터는 초기화가 될 것입니다.
여기서 볼 수 있듯이, 모델은 오로지 투두리스트 데이터 그자체를 다루고, 수정만합니다. Model은 입력(무엇이 그것을 수정하고 있는지), 출력(무엇이 출력되는지)를 이해하지 못하거나 모를것입니다.
만약 직접 모든 액션을 콘솔에 입력 및 뷰와 아웃풋을 콘솔을 통해 표현한다면, 현재까지는 완전한 CRUD APP 대한 완전한 기능적인건 다 가졌다고 볼 수 있습니다.
View
우리는 DOM조작을 통해 뷰를 생성할 것 입니다. 우리가 여기서 리액트 JSX 혹은 템플릿 언어를 사용하지 않는 순수한 자바스크립트만 사용하기 때문에 장황하고 보기 안좋을 수 있습니다. 직접적인 DOM 조작이란 그런것이죠 하하
Controller와 Model은 DOM, HTML 요소, CSS, 어떤 것도 알아서는 안되고, 그 모든 관련된 것들은 View안에 있어야합니다.
첫번째로 요소를 검색과 생성하기 위해 helper메소드를 만들어줄것입니다.
// View
class View {
constructor() {}
// Create an element with an optional CSS class
createElement(tag, className) {
const element = document.createElement(tag)
if (className) element.classList.add(className)
return element
}
// Retrieve an element from the DOM
getElement(selector) {
const element = document.querySelector(selector)
return element
}
}
Constructor에 View가 필요한 것들을 셋업하겠습니다.
The root element of the app - #root
The title heading - h1
A form, input and submit button for adding a todo - form, input, button
The todo list - ul
모든 변수들을 constructor에 담음으로써 쉽게 참조할 수 있도록합니다.
// View
class View {
constructor() {
// The root element
this.app = this.getElement('#root')
// The title of the app
this.title = this.createElement('h1')
this.title.textContent = 'Todos'
// The form, with a [type="text"] input, and a submit button
this.form = this.createElement('form')
this.input = this.createElement('input')
this.input.type = 'text'
this.input.placeholder = 'Add todo'
this.input.name = 'todo'
this.submitButton = this.createElement('button')
this.submitButton.textContent = 'Submit'
// The visual representation of the todo list
this.todoList = this.createElement('ul', 'todo-list')
// Append the input and submit button to the form
this.form.append(this.input, this.submitButton)
// Append the title, form, and todo list to the app
this.app.append(this.title, this.form, this.todoList)
}
// ...
}
이제는 view 부분은 우리가 셋업한 것에서 변동이 없을건데요.
사소한 두가지가 있는데, getter와 입력값(new Todo)의 resetter 입니다.
// View
get _todoText() {
return this.input.value
}
_resetInput() {
this.input.value = ''
}
모든 셋업은 끝났습니다. 투두들에 변화가 있을때마다 변하는 부분을 투두 리스트에 반영에 다시 보여주는 부분이 제일 복잡합니다.
// View
displayTodos(todos) {
// ...
}
displayTodos 메소드는 구성되어 있는 투두 리스트들을 ul, li 요소들을 생성해서 보여줍니다. 투두가 변경, 추가, 삭제 될때마다 displayTodos 메소드는 todos 변수를 모델로부터 다시 호출하여 투두 목록을 초기화하고 다시 보여줍니다. 이렇게 모델 상태와 뷰를 연동케합니다.
첫번째로 할 것은 todo 노드들을 호출 될때마다 모두 삭제할 것입니다. 그러면 우리는 어떤 투두가 있는지 확인 할 것입니다. 없다면 우리는 빈 목록 메세지를 보여주겠죠.
// View
// Delete all nodes
while (this.todoList.firstChild) {
this.todoList.removeChild(this.todoList.firstChild)
}
// Show default message
if (todos.length === 0) {
const p = this.createElement('p')
p.textContent = 'Nothing to do! Add a task?'
this.todoList.append(p)
} else {
// ...
}
이제 우리는 투두들을 루프를 통해 순회한뒤 존재하는 투두들에 해당되는 체크박스, Span, 버튼들을 출력합니다.
// View
else {
// Create todo item nodes for each todo in state
todos.forEach(todo => {
const li = this.createElement('li')
li.id = todo.id
// Each todo item will have a checkbox you can toggle
const checkbox = this.createElement('input')
checkbox.type = 'checkbox'
checkbox.checked = todo.complete
// The todo item text will be in a contenteditable span
const span = this.createElement('span')
span.contentEditable = true
span.classList.add('editable')
// If the todo is complete, it will have a strikethrough
if (todo.complete) {
const strike = this.createElement('s')
strike.textContent = todo.text
span.append(strike)
} else {
// Otherwise just display the text
span.textContent = todo.text
}
// The todos will also have a delete button
const deleteButton = this.createElement('button', 'delete')
deleteButton.textContent = 'Delete'
li.append(checkbox, span, deleteButton)
// Append nodes to the todo list
this.todoList.append(li)
})
}
이제 뷰와 모델은 셋업이 되었습니다. 단지 우리는 둘을 연결할 방법이 없습니다. 사용자의 입력을 바라보는 이벤트가 없고, 이벤트에 대한 출력을 다루는 핸들러또한 없습니다.
콘솔은 여저힌 임시적인 컨트롤러로써 역할을 합니다. 투두를 더하고 삭제가 가능합니다.
Controller
드디어 Controller를 통해 Model과 View를 이을 차례입니다. 지금까지 Controller에 뭐가 있는지 확인 해 볼까요?
// Controller
class Controller {
constructor(model, view) {
this.model = model
this.view = view
}
}
우리의 첫 View와 Model을 연결하는 것은 투두가 변경이 일어날때마다 displayTodos를 호출하는 메소드를 만드는 것입니다. 또한 constructor안에서 한번 호출함으로써 초기 투두가 있는지 확인 후 출력합니다.
// Controller
class Controller {
constructor(model, view) {
this.model = model
this.view = view
// Display initial todos
this.onTodoListChanged(this.model.todos)
}
onTodoListChanged = (todos) => {
this.view.displayTodos(todos)
}
}
Controller는 이벤트들이 일어난 이후에 핸들링할 것입니다. 새로운 투두를 제출하고, 삭제 버튼을 클릭하고, 투두의 체크박스를 클릭하면 이벤트가 실행될 것입니다. View는 반드시 저 이벤트들을 기다리고 있어야합니다. 왜나하면 저 이벤트들을 뷰의 사용자 입력이기 때문입니다. 그러나 이 이벤트에서 발생하는 응답에 대한 책임을 controller한테 보냅니다.
우리는 이벤트에 대한 핸들러를 Controller에 만들것입니다.
// Controller
handleAddTodo = (todoText) => {
this.model.addTodo(todoText)
}
handleEditTodo = (id, todoText) => {
this.model.editTodo(id, todoText)
}
handleDeleteTodo = (id) => {
this.model.deleteTodo(id)
}
handleToggleTodo = (id) => {
this.model.toggleTodo(id)
}
Setting up event listeners
이제 우리는 핸들러가 있지만, Controller는 여전히 언제 핸들러들을 호출해야할 지 모릅니다. 우리는 뷰에 있는 DOM 요소에 이벤트 리스너를 넣어야합니다. submit에 대한 응답은 form에서, 그리고 Click 과 change 이벤트는 투두 리스트에서 응답할 것입니다. ( 여기서 '수정'에 관한건 스킵하겠습니다. 이 것보다 조금 더 복잡하기때문에 )
// VIEW
bindAddTodo(handler) {
this.form.addEventListener('submit', event => {
event.preventDefault()
if (this._todoText) {
handler(this._todoText)
this._resetInput()
}
})
}
bindDeleteTodo(handler) {
this.todoList.addEventListener('click', event => {
if (event.target.className === 'delete') {
const id = parseInt(event.target.parentElement.id)
handler(id)
}
})
}
bindToggleTodo(handler) {
this.todoList.addEventListener('change', event => {
if (event.target.type === 'checkbox') {
const id = parseInt(event.target.parentElement.id)
handler(id)
}
})
}
우리는 핸들러를 뷰로부터 호출해야합니다. 뷰로부터 이벤트를 듣기 위해서 우리는 메소드들을 bind할 것입니다.
여기서 모든 이벤트들을 다루기 위해서 에로우 함수를 사용했습니다. 이는 Controller의 this 컨텍스트를 사용해 뷰에서 이벤트들을 호출할 수 있도록 해줍니다. 만약 에로우 함수를 사용하지 않는다면, 우리는 수동으로 다 bind를 해줘야할 것입니다.
EX) this.view.bindAddTodo(this.handleAddTodo.bind(this))
// Controller
this.view.bindAddTodo(this.handleAddTodo)
this.view.bindDeleteTodo(this.handleDeleteTodo)
this.view.bindToggleTodo(this.handleToggleTodo)
이제 각 요소에서 submit, click, change 이벤트가 발생할때마다 그에 대응하는 핸들러들이 동작할 것입니다.
Respond to callbacks in the model
우리가 뭔가 생략한 것이 있습니다. 이벤트들을 듣고, 핸들러들이 동작하지만 아무것도 일어나지 않았습니다. 이유는 Model이 View를 업데이트 되야하는지, view 업데이트를 위해 뭘 해야하는지 모르기 때문입니다. 우리는 displayTodos 라는 메소드가 이를 해결하기 위해 View가 가지고 있습니다. 하지만 먼저 언급했듯이 Model과 View는 서로에 관해 알면 안됩니다.
모델은 이벤트들을 듣고있다가 컨트롤러에서 뭔가 일어났다는 것을 알게하기 위해 다시 돌려줍니다.
우리는 이미 onTodoListChanged 라는 메소드를 이를 다루기 위해 만들었습니다. 우리는 단지 Model이 이를 알게하게 하면됩니다. 우리는 이것을 핸들러와 View에게 했던것처럼 Model에게 bind를 시켜줍니다.
모델 에서는 onTodoListChanged 에 대한 bindToodoListChanged를 추가해줍니다.
// Model
bindTodoListChanged(callback) {
this.onTodoListChanged = callback
}
그리고 controller에 bind 시킬겁니다. 뷰에서 했던 것 처럼요.
// Controller
this.model.bindTodoListChanged(this.onTodoListChanged)
이제 모든 Model 메소드들 뒤에 onTodoListChanged콜백을 호출할 겁니다.
//Model
deleteTodo(id) {
this.todos = this.todos.filter(todo => todo.id !== id)
this.onTodoListChanged(this.todos)
}
Add Local Storage
여기까지, 앱의 대부분은 완성됬고, 모든 컨셉은 입증된 상태입니다. 우리는 여기서 브라우저의 로컬 스토리지안에 데이터를 유지함으로써 좀 더 영구적으로 만들 것 입니다. 그래야 새로고침 이후에도 유지가 되겠죠.
이제 로컬 스토리지에 빈 어레이를 넣어줄 초기 값을 세팅합니다.
// Model
class Model {
constructor() {
this.todos = JSON.parse(localStorage.getItem('todos')) || []
}
}
우리는 메소드를 Model 상태와 마찬가지로 로컬스토리지에 업데이트 할 수 있도록 commit이라는 private 메소드를 만들어 줍니다.
// Model
_commit(todos) {
this.onTodoListChanged(todos)
localStorage.setItem('todos', JSON.stringify(todos))
}
this.todos가 변경 된 후 우리는 이 메소드를 호출할 것입니다
// Model
deleteTodo(id) {
this.todos = this.todos.filter(todo => todo.id !== id)
this._commit(this.todos)
}
Add live editing functionality
퍼즐의 마지막 조각은 존재하고 있는 투두리스트를 수정하는 기능입니다. 수정은 항상 단순 추가, 삭제 보다 까다롭습니다. 간단하게 구현을 하기위해서, 수정 버튼이나 입력을 통한 span교체를 요구하지는 않을 것 입니다. 또한 editTodo를 매번 글자를 타이핑할때마다 호출하고 싶지않습니다. 왜냐하면 이 행위는 투두리스트 전체를 Re-render를 시키기 때문이죠.
저는 새로운 수정 값을 임시 상태변수에 업데이트할 메소드를 뷰에 만들 것입니다. 그리고 Model을 업데이트할 Controller안에 handleEditTodo 메소드를 호출합니다. 입력 이벤트는 내용 편집 가능 요소를 타이핑할 때 발생하고 내용 편집 가능 요소를 벗어날 때, 포커스아웃을 합니다.
// View
constructor() {
// ...
this._temporaryTodoText
this._initLocalListeners()
}
// Update temporary state
_initLocalListeners() {
this.todoList.addEventListener('input', event => {
if (event.target.className === 'editable') {
this._temporaryTodoText = event.target.innerText
}
})
}
// Send the completed value to the model
bindEditTodo(handler) {
this.todoList.addEventListener('focusout', event => {
if (this._temporaryTodoText) {
const id = parseInt(event.target.parentElement.id)
handler(id, this._temporaryTodoText)
this._temporaryTodoText = ''
}
})
}
이제 투두 아이템을 클릭하게되면, 임시 상태 변수를 업데이트 할 '수정' 모드로 들어갑니다. 그리고 탭(tab)키 혹은 다른 곳을 클릭한다면 임시 상태 변수를 Model에 저장하고, 임시 상태를 초기화합니다.
editTodo 핸들러를 꼭 bind하세요.
// Controller
this.view.bindEditTodo(this.handleEditTodo)
https://www.taniarascia.com/javascript-mvc-todo-app/
'Good to Know' 카테고리의 다른 글
스위치 구문에 Default 생략에 관해서 (0) | 2022.11.21 |
---|---|
VSCODE Syntax Highlighting이 안될 때 (0) | 2022.11.17 |
router.push vs Link vs a 태그 (0) | 2022.10.02 |
CSR(Client Side Rendering) vs SSR(Server Side Rendering) (0) | 2022.07.17 |
$(document).ready() 는 바닐라 자바스크립트로 ? (0) | 2022.07.10 |