본문 바로가기

Front/향해플러스

Virtual DOM 만들기 - #1. createVNode (향해 플러스 2주차)

2주차 과제

1주차에서 SPA를 직접 구현하긴 했지만, 리렌더링이 트리거될 떄 변경된 부분만 다시 그리는 것이 아니라 전체 DOM을 다시 렌더링하는 방식이었기 떄문에 진정한 의미의 SPA라고 보기는 어려웠다. 성능 면에서도 비효율적인 부분이 많았다. 그래서 이번 2주차 과제에서는 Virtual DOM을 직접 구현하고, diffing 알고리즘을 도입상태가 변경된 부분만 선택적으로 리렌더링되도록 개선하는 것이 목표였다. 평소 diffing 알고리즘이 어떻게 동작하고 구현하는지 궁금했었는데 흥미로운 도전이 될 것 같다.

이번 과제에서는 Vite 번들러 환경에서 JSX를 사용하기 위해서 Babel 관련 패키지를 설정하고, 프로덕션 코드로 사용 가능한 형태로 만들기 위해 ES5 코드로 변환하는 트랜스파일링 과정이 적용되었다.

JSX에서 실제 DOM으로 변환하기 까지의 과정은 아래와 같다.

---
config:
  layout: dagre
---
flowchart TD
    A["JSX"] --> B["createVNode"]
    B --> C["normalizeVNode"]
    C --> D{"container._vNode 존재 여부"}
    D -- "없음 - 최초 렌더링" --> E["createElement로 DOM 생성"]
    E --> F["container.appendChild"]
    D -- "있음 - 재렌더링" --> G["updateElement로 diff & patch 수행"]
    F --> H["container._vNode = normalizedVNode"]
    G --> H
    H --> I["setupEventListeners 등록"]
    I --> J["렌더링 완료 → container 반환"]

createVNode란?

JSX는 브라우저가 바로 이해할 수 없기 때문에 빌드 과정에서 JavaScript 객체로 변환된다. React에서는 이 객체를 가상 DOM(Virtual DOM)이라고 부르고, 우리는 그걸 흉내 내보는 것이 목표였다.

export function createVNode(type, props, ...children) {
  // children은 배열이므로 flat을 통해 하나의 배열로 만들어준다. (평탄화 작업)
  // filter를 통해 null, undefined, false를 제거한다.
  return {
    type,
    props,
    children: children.flat(Infinity).filter((child) => child !== null && child !== undefined && child !== false),
  };
}

JSX → CreateVNode로 변환되는 과정

JSX 코드는 빌드 과정에서 Vite, Babel 등의 JSX 트랜스파일러에 의해 자동으로 createVNode 형태로 변환된다.

예시

// JSX 코드
<button className="btn">Click</button>
// 트랜스 파일된 결과
createVNode("button", { className: "btn" }, "click")

왜 children을 평탄화(flat)하고 filter할까?

JSX에서 반복문을 돌리거나 조건부 렌더링을 하면, 다음과 같이 중첩 배열이 나올 수 있다.

const items = ["A", "B", "C"];
createVNode("ul", null, items.map(item => createVNode("li", null, item));
// 결과
[
    createVNode("li", null, "A"),
    createVNode("li", null, "B"),
    createVNode("li", null, "C")
]

이런식으로 children 에 들어가면

createVNode("ul", null, [[li, li, li]])

이렇게 children의 구조가 중첩 배열 형태로 나온다. 이러한 중첩 구조를 한 번에 평탄화 하기 위해 flat(Infinity) 가 필요하다. 또한 조건부 렌더링에서 `false`, `null`, `undefined`도 `children`으로 들어올 수 있는데, 이걸 그대로 두면 `createElement` 단계에서 문제가 생기므로 `filter`로 제거해준다.


마무리

`createVNode`는 React의 JSX 트랜스파일 결과와 거의 동일한 구조로 가상 DOM 객체를 만들어주는 역할을 한다. 이 단계는 나중에 `createElement`로 실제 DOM을 만들거나 `updateElement`로 diffing을 수행할 수 있게 해주는 핵심 기반이다.

 

다음 글에서는 `createVNode` 를 정제해서 일관된 포맷으로 바꿔주는 `normalizeVNode`를 만들어보자!