본문 바로가기

Front/향해플러스

Virtual DOM 만들기 - #5: updateElement (향해 플러스 2주차)

updateElement

`updateElement`는 새로운 가상 DOM과 이전 가상 DOM을 비교하여 변경된 부분만 실제 DOM에 반영하는 역할을 한다. 이 과정이 바로 Virtual DOM의 핵심, diffing 알고리즘이다.


1. 새로운 노드와 이전 노드가 모두 없는 경우

if (!newNode && !oldNode) return;

아무 작업도 하지 않는다.

 

예시

{show && <div>hello</div>}
// show가 false 일 때
// oldNode: undefined
// newNode: undefined

2. 새로운 노드가 없으면 기존 노드를 제거

if (!newNode && oldNode) {
  const child = parentElement.childNodes[index];
  if (child) parentElement.removeChild(child);
  return;
}

가상 DOM에서 해당 요소가 사라지면 실제 DOM에서도 지워야 한다.

 

왜 index를 쓰나?

서로 비교할 노드를 선택하기 위해 사용한다.

 

예시

oldNode: [<div>1</div>, <div>2</div>]
newNode: [<div>1</div>]

// newNode에서 두 번쨰 div 사라짐
// index: 1 에 있는 DOM을 제거
// 실제 DOM
<div>1</div>
<div>2</div> // 제거 대상!

3. 기존 노드가 없으면 새로운 노드 추가

if (newNode && !oldNode) {
  parentElement.appendChild(createElement(newNode));
  return;
}

새로운 노드가 추가돼서 DOM에 새로 붙여야한다.

 

예시

oldNode: [<div>1</div>]
newNode: [<div>1</div>, <div>2</div>]

// oldNode에 두 번째 div가 없음
// newNode에는 존재함
// index: 1에 DOM 새로 추가
// 실제 DOM
<div>1</div>
<div>2</div> // 새로 추가!

4. 현재 위치의 DOM 에 노드가 없으면 새로 추가

// 기존 자식 노드 가져오기
const $currentNode = parentElement.childNodes[index];

// 현재 노드가 없는 경우 새로운 노드 추가
// index가 밀릴 상황을 대비하여 없으면 새로 추가한다.
if (!$currentNode) {
  parentElement.appendChild(createElement(newNode));
  return;
}

 

가상 DOM 상에는 oldNode 있다고 판단했지만, 실제 DOM에는 해당 index 위치에 노드가 없을 수 있다. index 기반 순회가 불안전하거나 이전 렌더에서 잘못 제거된 경우를 대비한 방어 코드이다.

 

예시

oldChildren: [A, B]
newChildren: [B, C]

// 순서 변경 중 모종의 이유로 index 어긋났다고 가정
// index: 1 위치에 노드가 있어야하지만 노드가 없음
// index: 1 이 없으니까 [C]를 추가

5. 두 노드가 모두 문자열/숫자일 경우 텍스트 비교

if ((typeof newNode === "string" || typeof newNode === "number") &&
    (typeof oldNode === "string" || typeof oldNode === "number")) {
  if (newNode !== oldNode) {
    parentElement.replaceChild(document.createTextNode(newNode), $currentNode);
  }
  return;
}

두 노드가 모두 텍스트/숫자일 경우 같으면 유지하고 다르면 새로운 텍스트 노드로 교체한다.

 

예시

oldNode: ["hello"]
newNode: ["Hi"]

// 실제 DOM
// "hello" -> "Hi" 로 교체

6. 타입이 다르면 새 노드로 교체

if (newNode.type !== oldNode.type) {
  parentElement.replaceChild(createElement(newNode), $currentNode);
  return;
}

`<div>` -> `<span>` 처럼 태그 타입이 다르면 교체한다.

 

예시

oldNode: <div>Text</div>
newNode: <span>Text</span>
// 실제 DOM
// <div>Text</div> 
<span>Text</span> // 새로 교체

7. 속성과 이벤트 갱신

  // 기존 자식 노드 가져오기
  const $currentNode = parentElement.childNodes[index];
  
  // 새로운 노드와 기존 노드의 props를 확인한다.
  const newProps = newNode.props || {};
  const oldProps = oldNode.props || {};

  updateAttributes($currentNode, newProps, oldProps);

 

 

`ClassName`, `checked`, `disabled`, `onClick` 등 속성 변경 사항을 반영한다.

 

예시

// className
oldNode: <button>Click</button>
newNode: <button className="btn secondary">Click</button>

// checked
oldNode: <input type="checkbox" checked />
newNode: <input type="checkbox" />

// disabled
oldNode: <button disabled></button>
newNode: <button>Click</button>
// 실제 DOM

// className
<button class="btn secondary">Click</button> // className -> class 로 변경

// checked
<input type="checkbox" /> // checked 속성 제거

// disabled
<button>Click</button> // disabled 속성 제거

8. 자식 노드 재귀 비교

const newChildren = newNode.children || [];
const oldChildren = oldNode.children || [];
const maxLength = Math.max(newChildren.length, oldChildren.length);
for (let i = 0; i < maxLength; i++) {
  updateElement($currentNode, newChildren[i], oldChildren[i], i);
}

현재 요소의 자식들을 한 개씩 순회하며 재귀적으로 `updateElement` 를 수행한다.


9. 필요 없는 노드 제거

while ($currentNode.childNodes.length > newChildren.length) {
  $currentNode.removeChild($currentNode.lastChild);
}

가상 DOM에는 더 이상 자식이 없지만 실제 DOM에는 자식이 남아있는 경우 제거한다.

 

예시

oldNode: [<div>1</div>, <div>2</div>, <div>3</div>]
newNode: [<div>1</div>]
// 실제 DOM
<div>1</div> // 유지
//<div>1</div> // 삭제
//<div>1</div> // 삭제
// 마지막 자식 2개 제거

마무리

`updateElement`를 구현하면 Virtual DOM이 변경된 부분만 DOM에 반영되도록 만들 수 있다. 이로 인해 불필요한 렌더링이 줄어들고, 성능이 크게 향상된다.

 

다음 글에서는 이벤트 위임과 SyntheticEvent 구현을 두뤄보자!