9. CBD

로직

  1. 처음 DOMContentLoaded 이벤트가 발생하였을 때, 초기 state 값 설정 및 render 함수를 실행한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
window.addEventListener("DOMContentLoaded", () => {
createState({
todos: [
{ id: 3, content: "Javascript", completed: false },
{ id: 2, content: "CSS", completed: true },
{ id: 1, content: "HTML", completed: false },
],
todoFilter: ["All", "Completed", "Active"],
currentTodoFilterId: 0,
});

render();
});
  1. render가 실행되면 App 클래스의 인스턴스를 생성하고 생성된 인스턴스를 domStr 프로토타입 메서드를 사용하여 문자열로 반환하고 이 문자열을 node(virtual DOM)로 반환해주었다. 생성된 Virtual DOM과 기존 index.html에 있는 root가 생성한 Real DOM을 비교하는 diff 함수를 실행한다.
1
2
3
4
5
const render = () => {
const app = new App();
console.log(app);
diff(app.newDOM(), $root);
};
  1. App 클래스에서는 todoInput, todoList, todoFilter 라는 인스턴스 프로퍼티에 각각 클래스형 컴포넌트의 인스턴스를 할당해주었다. 이 프로퍼티(컴포넌트)를 components 배열에서 관리를 해준다. 또한, 각 컴포넌트에서 데이터가 변경되어 재렌더링이 발생하면 2번 과정이 발생한다.
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
let components = []; // App 인스턴스 여러개 만들면 구분해주기 위해 전역에서 선언
class App {
constructor() {
components.forEach((c) => c.unmount());

this.todoInput = new TodoInput();
this.todoList = new TodoList();
this.todoFilter = new TodoFilter();

components = [this.todoInput, this.todoList, this.todoFilter];

components.forEach((c) => c.mount());
}

domStr() {
return `
${this.todoInput.domStr()}
${this.todoList.domStr()}
${this.todoFilter.domStr()}
`;
}

newDOM() {
return domStrToNode(this.domStr());
}
}
  • 이렇게 생성된 컴포넌트에 이벤트 핸들러를 등록시켜주기 위해서는 App 클래스의 인스턴스가 생성될 때, 즉 데이터가 변경되어 렌더링이 될 때 마다 기존 컴포넌트 목록들에게 unmount(이벤트 제거)를 실행하고 다시 컴포넌트를 등록하고 mount(이벤트 등록)을 실행한다.
  • 이벤트를 등록하고 제거해주어 컴포넌트의 생명주기를 관리해주는 이유는 이벤트 위임으로 window에게 이벤트를 등록만 하고 제거해주지 않는다면 재렌더링 발생하여 컴포넌트가 새로 생성되면 그 때 다시 또 이벤트를 등록해주므로 불필요한 이벤트 등록과 성능상 문제가 발생할 수 있기 때문에 재렌더링이 발생할 때 꼭 이전 컴포넌트의 이벤트를 제거해줘야한다.
  1. 각 컴포넌트에서는 이벤트 핸들러를 작성해주고 mount 함수 안에 이벤트 리스너를 등록하고 unmount 함수안에 이벤트를 제거해준다. domStr 함수는 해당 컴포넌트를 HTML 구조를 문자열로 반환해준다.
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
class TodoList extends Component {
constructor() {
super();

let todos = [...state.todos];
if (state.currentTodoFilterId === 1)
todos = state.todos.filter((todo) => todo.completed);
if (state.currentTodoFilterId === 2)
todos = state.todos.filter((todo) => !todo.completed);

this.items = todos.map(({ id }) => new TodoItem(id));
}

mount() {
this.items.forEach((item) => item.mount());
}

unmount() {
this.items.forEach((item) => item.unmount());
}

domStr() {
return `
<ul class="todo-list">
${this.items.map((item) => item.domStr()).join("")}
</ul>
`;
}
}
  • todoList 경우는 App 에서 todoList, todoInput, todoFilter 컴포넌트를 관리해주는 것처럼 todoItem 컴포넌트를 관리해준다. App 에서 mount 함수를 실행하여 각 컴포넌트의 mount, unmount를 통해 생명주기를 관리해준 것 처럼 todoList의 mount 에서는 todoItem 컴포넌트의 mount, unmount를 관리해준다.
  • this.items 에는 각 id에 해당하는 TodoItem의 컴포넌트들로 구성된 배열이 할당된다. 그리고 domStr 함수에서 TodoItem 컴포넌트들을 map 고차함수로 돌면서 각 컴포넌트들의 domStr 함수로 호출하여 문자열로 반환한다.
  • TodoFilter에서 domStr 부분에서 id가 꼭 필요한가?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    domStr() {
    return `
    <ul class='todo-filter'>
    ${state.todoFilter
    .map(
    (filter, i) =>
    `<li class=" filter ${i === state.currentTodoFilterId ? 'active' : ''}" id="${i}">${filter}</li>`
    )
    .join('')}
    </ul>
    `;
    }
    • all, completed, active 에 id가 굳이 필요한가??
  1. 각 컴포넌트에서 state로 관리하는 부분이 변경될 때, 상태가 바뀌는 부분만 재렌더링이 일어나도록 구현하였으므로 todoInput에서는 재렌더링이 일어날 부분이 없으므로 autofocus 기능이 사라지지 않는다.
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
class TodoInput extends Component {
constructor() {
super();

this.addTodo = (e) => {
if (e.key !== "Enter" || e.target.value.trim() === "") return;

state.todos = [
...state.todos,
{ id: getNextId(), content: e.target.value, completed: false },
];
e.target.value = "";
};
}

mount() {
window.addEventListener("keyup", this.addTodo);
}

unmount() {
window.removeEventListener("keyup", this.addTodo);
}

domStr() {
return `<input class='todo-input' autofocus/>`;
}
}

문제

  • checkbox 클릭하면 checked가 생성되고 사라지는 렌더링은 잘 되지만, 화면에서는 바뀌지 않는 문제가 발생하였다.
    • attribute와 property의 차이때문에 발생하는 문제점이라고 생각되었다.
    • setAttribute대신에 요소를 replaceChild()로 자식노드를 교체해주었다.

소감

  • 혼자서 공식문서들여다 보는 것 보다 확실히 옆에서 같이 이야기 하고 생각을 공유하면서 코드를 작성해나가다 보니 diff 알고리즘도 완성할 수 있어서 기뻤다.
  • node.children 은 텍스트 노드를 제외한 자식 노드들을 반환
  • node.childNodes는 모든 자식 노드를 반환한다.
  • attribute와 property의 차이점에 대해 한번 더 생각해볼 수 있는 시간이 있어서 좋았다. attribute는 HTML에 있는 속성으로 rendering이 되면 attribute는 DOM property로 변환된다. 하지만 1:1 매칭이 되지는 않는다는 점을 주의하자.
    • attribute : 변하지 않는 초기 default 값 전달
    • property : 사용자의 행동으로 변할 수 있다.
    • diff 구현할 때, removeAttribute는 제대로 동작했는데, setAttribute할 때는 checkbox값이 삭제했을 때, 다음 todo에 이전되는 문제가 발생하였는데 이는 따로 정리해둬야겠다.