2주차 회고

정태수·2025년 4월 7일

과제 셀프회고

기술적 성장

VDOM 구현을 해봄으로써 작동원리에 대해 깊은 생각을 해볼수 있었다.
createVNode - 가상 DOM 생성
normalizeVNode - 컴포넌트나 자식들을 정규화
createElement - 실제 DOM으로 변환.
eventManger - 이벤트 위임 등록/삭제 등 관리
renderElement - vNode를 받아서 실제 DOM에 렌더링 / 이벤트 연결 / 초기 렌더링 담당
updateElement - 기존 DOM과 새로운 노드를 diff해서 변경된 부분을 반영

createVNode

createVNode 함수는 type, props, children 객체를 반환한다 하지만 실제로 문자열, 숫자, 함수형 컴포넌트, null, undefined, boolean, 배열 등 다양한 형태의 children이 들어온다. 다양한 형태로 들어오는 children을 재귀형태로 평탄화하지 않으면 제대로 렌더링이 될수 없다.

특히 JSX에서는 여러 계층의 children이 중첩 배열로 들어오기 때문에, 이를 재귀적으로 평탄화하기 위한 flattenChildren 함수가 필요했다. 이 함수는 단순히 children을 1차원 배열로 만드는 것뿐 아니라, null, undefined, boolean 같은 falsy적인 값처럼 렌더링할 수 없는 값도 필터링 해준다.

normalizeVNode

normalizeVNode는 JSX에서 컴포넌트가 함수로 들어올 경우, 이를 실행해 실제 VNode로 변환해줘야 하는데, 이 정규화 단계를 거치지 않으면 정상적으로 렌더링되지 않는다. typeof 를 돌려서 function으로 나오게 된다면 해당 function에 대해서 어떻게 normalize 해야하는지에 대한 고민을 했다. 정규화는 단순히 값들을 일관된 형태로 변환해주는 것이 아니라, 다양한 타입을 처리하고 재귀적으로 구조를 단순화하여 vNode로 표현되게끔 만들어주는 매우 중요한 작업이다.

eventManger

이번 과제에서는 이를 Virtual DOM 시스템과 어떻게 연결할 것인지, 즉 VNode 단에서 정의된 이벤트 핸들러가 실제 DOM에 어떻게 반영되고, 이벤트 위임 시스템과 어떤 방식으로 연동되는지를 설계하는 것이 핵심이었다.

가장 먼저 고민한 부분은 이벤트 핸들러가 props를 통해 전달된다는 점이었다. JSX에서

<div onClick={...}>

처럼 작성된 이벤트는 결국 createVNode를 거치며 VNode 객체의 props로 전달되며, 실제 DOM 요소로 변환되는 시점인 createElement 내부에서 처리해야 했다. 이때 "on"으로 시작하는 속성들을 감지해, 일반 속성이 아닌 이벤트로 분기하고, addEvent()를 호출해 이벤트를 위임 형태로 등록했다.

이 위임 구조는 EventManager 클래스 내부에서 구현되었고, 여기서 두 가지 주요 자료구조를 활용했다. 첫 번째는 Map<HTMLElement, Function> 형태의 맵으로, 어떤 타겟 요소가 어떤 이벤트 핸들러를 갖고 있는지를 저장하는 역할을 했다. 두 번째는 WeakMap<HTMLElement, Set>으로, 루트 엘리먼트 단위로 이미 어떤 이벤트 타입이 등록되었는지를 기억하기 위한 용도로 사용했다. 이렇게 하면 동일한 루트에 동일한 이벤트가 중복 등록되는 걸 방지할 수 있고, 루트 DOM이 제거되면 자동으로 참조도 사라져 메모리 누수를 막을 수 있었다.

VNode → createElement → EventManager(addEvent)로 이어지는 이 흐름은 Virtual DOM에서 선언한 이벤트를 실제 DOM에 안전하고 일관되게 반영할 수 있도록 도와줬다. 단순히 DOM에 직접 addEventListener를 붙이는 방식이 아니라, 추상화된 이벤트 관리 시스템과 연계하는 구조를 만들어냄으로써, 이후 상태 변경이나 리렌더링 시에도 복잡도를 낮출 수 있었다.

updateElement

Virtual DOM의 핵심은 실제 DOM과 VNode의 차이를 최소한의 변경으로 동기화하는 것이다. 이 역할을 수행하는 중심 함수가 바로 updateElement였다. 이 함수는 이전에 렌더링된 VNode와 새롭게 렌더링될 VNode를 비교한 뒤, 바뀐 부분만 실제 DOM에 반영한다. 이 과정이 잘 작동해야 렌더링 최적화가 이루어지고, 불필요한 DOM 변경이나 이벤트 재등록 같은 부작용도 막을 수 있다.

처음에는 단순히 타입(VNode.type)만 비교해서 다르면 교체하고, 같으면 재귀적으로 자식까지 내려가면서 비교하는 구조로 설계했다. 그리고 props는 updateAttributes를 통해, children은 updateElement를 다시 재귀 호출하는 방식으로 처리했다. 기본적인 비교 알고리즘은 다음과 같은 단계로 나눴다:

null 체크: newNode나 oldNode가 존재하지 않는 경우 노드를 추가하거나 제거
텍스트 노드 처리: string이나 number일 경우 nodeValue를 통해 값만 바꿈
타입 비교: 타입이 다르면 바로 교체
속성 비교: updateAttributes를 호출해서 변경된 props만 업데이트
자식 노드 재귀적 처리: 최대 자식 수만큼 반복하며 updateElement 재귀 호출
특별한 비교 함수(shallowEqual 같은)를 도입하지는 않았지만, 단순한 재귀 구조만으로도 의외로 많은 변경을 걸러낼 수 있었다. 전체 노드를 무작정 교체하기보단 필요한 부분만 업데이트하는 흐름을 직접 구현하면서, 자연스럽게 렌더링 최적화의 개념을 체감할 수 있었다.
다만 자식 노드의 순서 변경이나 중간 삽입 등 복잡한 구조에서는 한계가 명확히 드러났고, key 기반 diffing이나 상태 보존 같은 고급 전략이 왜 필요한지 분명히 느낄 수 있었다.
이전까지는 “렌더링이 된다 / 안 된다”의 결과만을 바라봤다면, 이번 경험을 통해 “렌더링이 어떻게 이루어지고, 어디서 낭비가 발생하는가”를 고민하는 시야를 갖게 되었다.

학습 효과 분석

VDOM 구현으로 리액트 원리에 대해 조금 더 다가간 느낌이다.
state 개념에 대해서도 조금 접근한 것 같은 느낌이고 동시에 리렌더링에 대해서 어떤 원리인지 다시한번 각인되는 과제였다.

profile
프론트엔드 개발자

0개의 댓글