diffing 구현
React에서는 화면에서 바뀐 부분만 부분적으로 렌더링 해주기 위해 diffing 알고리즘을 구현하였다.
virtualDOM과 realDOM을 비교해주기 위해 리액트 홈페이지에서 설명한 방법으로 vanilla JS로 diffing 알고리즘을 구현해보자.
0. 두 요소의 길이부터 비교한다.
우리가 작업하고 있는 요소를 $old, 새롭게 생성된 요소를 $new 요소라고 하자. 우리는 $new를 기준으로 $old의 요소들을 일치 시켜주는 행위를 하면 된다.
$new의 노드길이가 $old의 노드 길이보다 길다면, $old에서 추가해주면 되지만, $new가 $old보다 길이가 짧다면, $old의 나머지 부분을 제거해줘야한다.
0.1. 길이가 짧은 경우
1 | const diff = ($new, $old) => { |
0.2. 길이가 긴 경우
1 | newNodes.forEach(($n, i) => { |
- 이전 요소에 새로운 요소가 가진 자식 노드가 없다면 이전 요소에 추가해준다.
cloneNode(true)는 해당 요소의 자식노드들 까지 모두 복제해주기 위한 옵션이다.
1. 두 요소의 루트 노드부터 비교한다.
먼저 두 루트 요소의 타입이 다르면 $old 요소에서 제거하고 새로운 트리를 구축한다.
ex) 에서 앞서 tagName으로 비교하여 같지 않을 때, 자식노드까지 비교해주었는데, textNode와 commentNode는 서로 tagName이 같으므로 이들의 textContent로 비교해주어야 한다. 2번까지의 과정으로 $old 요소의 자식노드와 $new 요소의 자식노드들의 비교가 끝났고 일치까지 시켜줬다. 그러므로 이제는 자식 노드가 가지고 있는 attribute(class, inline-style, checked, disabled…)를 비교해줘야 한다. 새로운 노드의 attritube 중 이전 노드의 attribute와 같은 attribute가 없다면, 이전 요소 노드의 attribute를 새로운 노드의 attribute로 추가해준다. 이전 노드의 attribute 값과 새로운 노드의 attribute 값이 다르면 이전 노드 attribute에 값을 할당해준다. 만약 이전 노드의 attribute 중 새로운 노드의 attribute가 없다면 이전 노드의 해당 attribute를 제거해준다. 그리고 마지막으로 자식 노드들을 재귀적으로 diff 함수를 호출해주면 자식 노드까지 바뀐 부분만 확인해줄 수 있다. diff 알고리즘을 경현님과 같이 이야기 하면서 작성하다보니 완성할 수 있어서 기뻤다. 이전 노드의 attribute와 새로운 노드의 attribute가 다를 때, attribute를 추가해주기 위해 setAttribute()를 사용하였는데, attribute는 HTML 구조의 초기값을 설정해주는 것이므로, 사용자의 화면에 변화되는 것이 반영되지 않고 todo를 삭제할 때, 삭제한 다음 요소에 삭제 요소의 attribute가 남아있는 것처럼 보이는 문제가 발생하여 attribute를 추가하는 경우에는 replaceChild() 메서드를 사용하여 해당 요소의 노드를 바꿔주는 방식을 사용하였다. 때문에 attribute를 추가하는 경우에는 li 요소가 전부 렌더링 되는 모습을 보이고 attribute를 제거할 때는 input에 있는 checked attribute만 제거되므로 li 요소가 아닌 li 자식 요소의 input과 input의 다음 형제 요소들만 렌더링 되는 모습을 볼 수 있었다.1
2
3
4
5
6
7
8newNodes.forEach(($n, i) => {
const $o = oldNodes[i];
if ($o.tagName !== $n.tagName) {
$old.replaceChild($n.cloneNode(true), $o);
return;
}
});
2. textNode, commentNode 들은 textContent를 비교한 뒤 교체해준다.
1
2
3
4
5
6
7
8
9newNodes.forEach(($n, i) => {
const $o = oldNodes[i];
// textNode:3, commentNode:8
if ($n.nodeType === 3 || $n.nodeType === 8) {
if ($n.textContent !== $o.textContent)
$old.replaceChild($n.cloneNode(true), $o);
return;
}
});3. 이제 각 노드의 attribute 노드를 비교한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24newNodes.forEach(($n, i) => {
const $o = oldNodes[i];
const newAttrs = [...$n.attributes];
const oldAttrs = [...$o.attributes];
for (const $nAttr of newAttrs) {
const $sameAttr = oldAttrs.find(($oAttr) => $oAttr.name === $nAttr.name);
if ($sameAttr === undefined) {
$old.replaceChild($n.cloneNode(true), $o);
return;
}
if ($sameAttr.value !== $nAttr.value) $sameAttr.value = $nAttr.value;
}
oldAttrs.forEach(($oAttr) => {
if (!newAttrs.find(($nAttr) => $nAttr.name === $oAttr.name))
$o.removeAttribute($oAttr.name);
});
diff($n, $o);
});
소감