MVC 아키텍처 기반 TodoList V3를 만들어 보면서 느낀점을 기록해두려한다.

1. 동적 HTML vs 정적 HTML

동적 HTML은 js를 이용하여 동적으로 HTML을 render 해줘서 화면에 나타나게 하는 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$todoList.innerHTML = filteredTodos
.map(
({ id, content, completed }) => `
<li data-id="${id}">
<div class="view">
<input type="checkbox" class="toggle" ${completed ? "checked" : ""} />
<label>${content}</label>
<button class="destroy"></button>
</div>
<input class="edit" value="${content}" />
</li>
`
)
.join("");

const activeTodoCount = state.todos.filter((todo) => !todo.completed).length;

if (state.todos.length === activeTodoCount) {
document.querySelector(".clear-completed").style.display = "none";
} else {
document.querySelector(".clear-completed").style.display = "block";
}

if (!activeTodoCount) {
document.querySelector(".toggle-all").checked = true;
}

위 예제를 보면서 설명해보면, 동적 HTML로 생성한 HTML의 DOM을 조작하기 위해서는 innerHTML에 할당을 해준 뒤 render 함수를 사용하여 화면에 재렌더링 하는 방식을 사용하고 있다.

반면에 정적 HTML의 DOM을 조작하기 위해서는 document.querySelector를 꼭 써줘야 하고 자바스크립트 코드를 짜는데 있어서 통일성과 일관성을 유지하기가 어렵다.

또한, HTML이 바뀌었을 때 그에 따른 요소 노드도 변경되기 때문에 유지보수가 어려워지는 단점이 있다.

이를 js가 HTML에 종속되었다고 표현하며 js가 HTML에 종속되면 될수록 일관성이 결여된 코드를 짜게되며 가독성을 떨어뜨리고 복잡성을 증가시키는 요인이 되므로, 앞으로는 js를 사용하여 동적 HTML을 생성할 것이다.

동적 HTML의 단점도 분명히 있다. 예를 들면 빈 HTML 파일에다가 동적으로 HTML을 그려넣는 것이므로 SEO(검색엔진최적화)가 떨어질 수 있다. 하지만 HTML 요소를 조작하는 작업이 많다면 동적으로 HTML을 생성해주는 것이 편리하다.

2. 동적 HTML로 MVC 아키텍처 설계

처음에 Js 파일이 읽히고 난 다음에 render 함수로 HTML이 그려지기 때문에 다음과 같이 요소노드를 취득하면 null값이 나온다.

1
document.querySelector(".todo-list"); // null

그러면 어떻게 동적인 HTML요소를 조작할 수 있을까?

=> 이벤트 위임을 통해서 이벤트를 등록할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Todos v3</title>
<link rel="stylesheet" href="css/style.css" />
<script defer src="js/app.js"></script>
</head>
<body>
<div id="root"></div>
</body>
</html>

HTML 파일은 다음과 같이 #root div태그만 가지고 있고 div 태그 안에 동적으로 HTML을 생성해줄 것이다. 즉, 동적으로 생성된 HTML의 요소 조작을 위해서 부모 요소 노드인 document.querySelector('#root')를 통해 동적 HTML을 조작할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
window.addEventListener("DOMContentLoaded", fetchState);

$root.addEventListener("keyup", (e) => {
if (!e.target.matches(".new-todo")) return;

const content = e.target.value.trim();
if (e.key !== "Enter" || content === "") return;

addTodo(content);
e.target.value = "";
});

일단 window가 로드되면서 서버로 부터 초기 데이터를 받아 화면에 렌더링 해주는 것부터 시작한다.

이후, 아키텍처를 고려하면서 각자 역할에 맞는 함수를 분류하여 코딩을 한다.

3. 아키텍처가 무너지면 망한다

Model에는 state를 변경해주는 함수들이 모여있고, view에는 화면에 렌더링해주는 함수들이 모여있고, controller에는 화면에서 발생한 이벤트를 이벤트 핸들러로 이벤트 객체를 생성하여 Model의 함수의 인자로 넣어 호출한다.

여기서 중요한 점은 각자의 역할이 정해져있는데 이를 절대로 무시하거나 어기는 행위를 금지한다는 것이다.

1
2
3
4
5
6
// 함수로 통일시켜주기.
$root.addEventListener("click", (e) => {
if (!e.target.matches(".filter")) return;

setState({ filterId: e.target.id });
});

controller는 이벤트 객체를 Model의 함수의 인자로 전달하여 호출하는 역할을 해야한다. 하지만 위 예제에서는 controller가 Model의 함수를 호출하는 것이 아닌 직접 setState 함수를 호출하는 모습을 볼 수 있다.

이렇게 통일성이 지켜지지 않는다면 앞으로의 코딩 협업은 무쓸모가 되므로 꼭 아키텍처를 지키면서 생각하며 코딩하는 습관을 길러야 한다.

추가로 함수와 변수의 이름은 첫 아이 이름 짓듯이 신중하게 지어줘야 한다.

4. 이벤트 핸들러 내부에 중첩된 이벤트 핸들러

1
2
3
4
5
6
7
8
9
10
11
12
// 더블클릭했을 때, editId에 상태를 관리해줘야한다.
// addEventListener 안에 또 있는것은 나눠줘야한다.
$root.addEventListener("dblclick", (e) => {
e.path[2].classList.add("editing");

e.path[2].addEventListener("keyup", (e) => {
const content = e.target.value.trim();
if (e.key !== "Enter" || content === "") return;

editTodo(content, e.path[1].dataset.id);
});
});

더블클릭하였을 때, 생겨난 input 창에 글을 수정하는 로직을 구현하기 위해 사용한 코드이다.

위와 같이 이벤트 핸들러가 중첩되어 코드를 작성하게되면 앞서 설명한 일관성에 어긋나 유지보수를 어렵게 하는 원인이 되므로 이 경우 다음과 같이 코드를 최소한의 단위로 분리해주어야 하며, 아키텍처를 고려하여 역할을 나눠서 코딩을 해줘야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// Model

// Model에 editId를 재할당해준다. => render 함수에서는 state.editId를 보고 'editing' 클래스 추가할지 말지 확인하여 렌더링한다.
const createEditId = (id) =>
setState({
editId: +id,
});

// 편집은 한번에 한개씩만 허용하기 때문에 editId를 배열로 받지 않는다.
// 편집이 완료되었으면 editId를 초기값으로 돌려놔야 한다. render 함수에서는 state.editId를 보고 'editing' 클래스 유지할지 제거할지 판단하여 렌더링한다.
const editTodo = (content) =>
setState({
todos: state.todos.map((todo) =>
todo.id === +state.editId ? { ...todo, content } : todo
),
editId: 0,
});

// Controller

// 더블클릭하면 EditId를 생성하여 Model에 반영하는 함수를 호출한다.
$root.addEventListener("dblclick", (e) => {
createEditId(e.path[2].dataset.id);
});

// state.editId 값과 해당 요소가 같은지 비교하여 같으면 content를 매개변수로 받은 함수를 호출한다.
$root.addEventListener("keyup", (e) => {
if (!e.target.matches(".edit")) return;

const content = e.target.value.trim();
if (e.key !== "Enter" || content === "") return;

editTodo(content);
});

결론

그동안은 코드가 돌아가기만 하면 되는 것인줄 알았지만, 앞으로는 돌아가는 코드를 만들기 보다는 돌아가는 코드를 어떻게 하면 아키텍처를 잘 지키면서 각자 역할에 맞는 최소한의 단위로 구분하여 코드를 구현할지 생각하며 코딩을 하도록 해야한다는 것을 깊이 깨달은 날이었다.