diffTrees 함수의 알고리즘 생각하다보니… 일단 새롭게 리렌더링 될때 만들어지는 VDOM을 생성해 놓고, 그 다음에 비교를 해야 하는데…?
diffTrees를 통해 달라진 점만 찾아서 노드를 교체할 계획인데 그럴려면 실제 리렌더링시에 새롭게 만들어지는 가상 돔이 필요하다.
꼭 지켜야할 규칙! → 기존의 rootDOM을 기준으로 ‘하나의 노드’ 씩 그려나가야한다.

위가 rootDOM이라고 하면 새롭게 만들어지는 가상 DOM은 이 돔을 기반으로 그려져야 한다.
만약 여기서 3에서 리렌더링이 발생하면 1부터 다시 그려질게 아니라 3의 노드 부터 그려져야한다.
그러면 어떻게 해야할까?
우선은 rootDOM으로 부터 리렌더링이 발생한 VNode를 리렌더링 함수에 입력을 한다.
하지만 하위(=4,5 번 노드)의 노드들은 새롭게 노드가 그려지므로 기존의 상태(=hookMetaData필드)가 유지되지 않은채 완전히 새로운 노드가 만들어져 버린다.
최선의 방법은 reRender 가 실행되는 순간에 각각의 Node에 접근해서 생성을 하는데 각 Node에 진입하는 순간에!!! (type 이 함수일 경우 함수를 호출 하기 전에!!)

위의 함수를 만들고 흐름대로 구현을 완료했다면 이제는 올바르게 ‘가상 돔’을 만들 수 있다.
위의 작업의 전체 흐름은 다음과 같다.

우선은 첫번째 작업부터 진행 해보겠다.
특정 노드에서 setter 함수 호출시 발생하는 함수다. 각 노드를 DFS로 순환하면서 동기화 작업을 실행한다. 최종적으로 실제로 화면에 그려져야할 DOM을 반환한다.
매개변수 → setter 함수가 발생한 컴포넌트 함수를 재 호출된 결과값 : 최신 VNode
매개변수를 넣었으면 기존의 innternalRender 함수와 동일하게 DFS 순회를 진행한다.
이때 각 노드에 접근 할때마다 rootDOM트리도 동시에 순환한다. 같이 순환하면서 노드단위로 비교작업을 한다. → // TODO DFS 순회 함수 구현
각 노드 단위로 비교를 진행한다. 비교 기준은 다음과 같다.
비교는 DFS 의 후위 순회한다는 전제로 진행한다…
위의 작업을 각 Node에 집입할때 진행하고 다음 Node로 이동한다.
동기화 작업
이러면 동기화 작업은 완료된다.
위의 작업이 모두 완료되면 업데이트된 상태값을 갖고있는 VDOM이 만들어진다.
이 VDOM을 rootDOM으로 변경한다.
우선 기존의 rootDOM의 rootVnode에서 리렌더링이 발생한다.
이때는 기존의 hookMetaData필드가 VNode안에 있으므로 최신 상태를 갖고 리렌더링이 된다.
하지만 문제는 새롭게 리렌더링을 할때 생기는 하위 노드에서 발생한다. 하위 노드가 새롭게 생기면서 기존의 노드의 hookMetaData를 유지하지 않고 초깃값으로 생겨난다.

→ 그래서 본질적으로는 기존 rootNode와 새롭게 리렌더링되는 newNode와 비교해서 hookMetaData를 넣어줘야한다.
결국 객체로 이루어진 전체 트리구조가 필요하다. 그래야 빠르게 비교를 해 바뀐 부분을 찾을 수 있다. 해당 트리 생성은 다음과 같은 상황에서 필요하다.
이 트리 구조는 최종적으로 중첩 객체로 이루어져있다. 자식으로 컴포넌트가 있다면 자식 컴포넌트를 실행시켜서 새로운 객체로 풀어버린다. 이런 과정을 재귀적으로 실행시키면 하위의 모든 노드들이 객체로 풀어져 있다.
→ 그 이후에는 위의 로직 사용한다.
→ 일단 아래는 무시
기존의 internalRender 함수를 리팩토링하여 Node순환을 한번만 한다.
일단 계속해서 내가 헷갈려 하는 부분을 찾았다. 내가 생각하는 기본적인 트리 구조는 각 노드들이 동일한 데이터 구조(ex. 객체)를 갖고 있고, 만약 자식이 있다면 각 노드들이 자식으로 중첩된 객체 구조로 이루어져 있다고 생각했다. 그리고 지금 만드려는 트리 구조도 계속 이런 생각으로 설계를 시도했다. 하지만 여기서 이상한 부분이 있다.
✅ 함수형 컴포넌트 Vnode와 호스트 엘리먼트 VNode
이 두개는 internalRender 함수를 실행 하면 다른 결과가 나온다. 우선 이 두개를 다시 정의해 보자.
type: string, 예: <div>, <button> ): 실제 DOM 엘리먼트에 직접 대응 되는 노드. 이 노드는 children 속성을 통해 다른 vnode를 자식을 갖는다. → 내가 생각하는 일반적이 노드.type: function, 예: <App>, <Btn>): 이 노드는 그 자체가 DOM엘리먼트가 아니다. 즉 이 노드 단독으로는 DOM을 그릴수 없다. 해당 컴포넌트 함수를 호출해 해소해줘야 한다.(함수형 컴포넌트를 실행 했을때의 반환값은 하위 VNode를 반환하기에…) 보통 호스트 엘리먼트 VNode를 반환한다. → 자기 자신을 실행하면 다른 VNode를 ‘생산’한다.props.children 이 아니다. 이건 컴포넌트에 전달되는 prop일 뿐이다. 이 노드의 자식은 함수형 컴포넌트가 반환한 결과의 Vnode이다.✅ type이 함수형 컴포넌트라도 일단 type이 호스트 엘리먼트가 되도록 모두 풀어서 중첩 객체로 표현할까?
이 생각처럼 하려면 2단계가 필요하다.
RenderdeVNode 가, 원래의 상위 함수형 컴포넌트 RenderedVnode의 _renderedChildVNode속성으로 중첩되야한다. 그리고 해소된 Vnode의 internalRender 함수의 결과값인 RenderedVnode 는 그 자식들을 _renderedChildren 으로 중첩 시킨다.
기존의 internaRender 함수의 반환값은 없었지만 트리구조를 그리기 위해서는 RenderVNode를 반환 해야한다. RenderVNode의 인터페이스는 다음과 같다.
기존의 VNode를 확장
_renderedChildVNode 필드
_renderedChildren
domRef
const rootNode: HTMLElement = document.createElement(vnode.type);
위처럼 생성되는 노드를 참조한다. 이러면 최종적으로 만들어지는 트리만 갖고도 실제 DOM에 접근 할 수 있다.
이제 기존의 internalRender 함수를 수정해보자.
기존의 함수는 반환값이 없어서 아무것도 할 수 없다. 단지 사이드 이팩트로 DOM만 드리는 작업을 한다. 하지만 이제는 DOM 그리기 + 트리 생성 까지 할 수 있도록 수정해 보자.
interface RenderVNode extends VNode{
_renderedChildVNode?:VNode,
_renderedChildren?:VNode[],
domRef?:HTMLElement
}
const resolvedComponent: VNode = vnode.type(vnode.props); //컴포넌트 해소
internalRender(resolvedComponent, parent);
// 해소된 Vnode를 internalRender 함수로 재귀한 값인 RenderedVNode를 상위노드와 연결해야 트리 구조가 만들어 진다.
renderedVNode._renderedChildVNode = internalRender(
resolvedComponent,
parent
);
Object.entries(vnode.props).forEach(([prop, value]) => {
if (prop === "children" && value != null && Array.isArray(value)) {
childrenHandler(rootNode, value as ChildElementType[], renderedVNode);
//...
function childrenHandler(
rootNode: HTMLElement,
value: ChildElementType[],
renderedVNode: RenderedVNode
) {
value.forEach((child: ChildElementType) => {
// 1) 문자열 또는 숫자면 텍스트 노드
if (typeof child === "string" || typeof child === "number") {
const textNode = document.createTextNode(String(child));
rootNode.appendChild(textNode);
//🔎 리프노드일 경우 텍스트 노드전용 RenderedVNode를 만들어 tree구조에 연결해준다.
const textRenderedVNode: RenderedVNode = {
type: "#text",
props: { nodeValue: String(child) },
domRef: textNode,
key: null,
ref: null,
};
//🔎 상위 renderedVNode에 연결한다. -> tree의 연결고리 생성
renderedVNode._renderedChildren!.push(textRenderedVNode);
return;
} else if (child !== null && child !== undefined) {
// 🔎 호스트 엘리먼트일 경우 재귀로 internalRender를 실행하는데 이때 생기는 반환 노드를 상위의 노드(renderedVNode)에 연결한다.
renderedVNode._renderedChildren!.push(internalRender(child, rootNode));
return;
}
});
}

모든 VNode는 intnernalRender 함수를 통해 렌더링이 된다. 이때 이 함수는 어떤 VNode를 받던 최종적으로는 VNode의 렌더링된 결과 를 나타내는 RenderedVNode를 반환 해야한다. 그리고 이 renderedVNode객체들이 메모리 상의 트리를 구성하는 실제 ‘노드’ 들이다.
최종적으로 internalRender함수로 만즐어진 트리는 internalRender함수의 외부인 render 함수에서 변수로 받아야 할듯?
✅ 재조정 과정은 리렌더링 되는 새로운 노드 트리 → diff 비교 이렇게 순차적으로 이러나는게 아니라 동시에 발생한다. 리렌더링 되면서 새로운 노드가 만들어기 전에 diff 비교를 하고 결과에 따라 노드가 만들어 진다.
서로 다른 타입의 두 엘리먼트는 서로 다른 트리를 만들어낸다.
개발자는 key prop을 통해, 여러 렌더링 사이에서 어떤 자식 엘리먼트가 변경되지 않아야 할지 표시할 수 있다. → 최적화의 핵심!
하지만 key를 사용하지 않아도 버그는 발생하지 않는다. 하지만 개발자의 의도대로 동작하지 않는 위험이 존재한다.
만약 개발자가 key prop을 사용하지 않고 개발을 하는 상황을 전개 해본다. 이때 내가 만든 프레임워크는 어떻게 노드끼리 비교를 해 상태를 이전할 수 있을까?

위와 같은 노드 트리의 List노드에서 리렌더링이 발생한다.
그러면 자식의 Item컴포넌트 노드들이 생성된다. 이때 기존의 노드 인스턴스와 동일한 노드가 생성되지 않는다. List 컴포넌트가 싱행되면서 새로운 자식 노드 인스턴스들이 생성된다. 하지만 이대로 만들어진 노드들로 이루어진 tree를 이용해 DOM을 그리면 안된다. 이렇게 되면 기존의 상태들은 동기화 되지 못하고 초기화된 상태를 갖고 있기 때문에 업데이트된 최신 상태를 이용 할 수 없다.(위의 사진처럼 초기화된 state:0을 갖고 있다.)
✅ 동기화 작업 등장!
그래서 이때 동기화 작업을 생각하게 된다.
"각 노드들을 비교하면서 해당 노드 인스턴스가 리렌더링 전의 인스턴스와 동일한 노드일까?"
? "상태 필드 동기화 시작!"
: "새롭게 생성된 노드 인스턴스 그래도 사용."
각 노드 단계에서 리렌더링 전의 노드와 리렌더링 후의 노드를 비교해본다.
만약 동일하지 않으면 리렌더링으로 새롭게 생성된 노드를 교체한다.
만약 동일한 노드(type과 index가 동일)하면 리렌더링 전의 상태 필드를 새로운 노드에 주입한다. 그리고 리렌더링 된 노드를 기존 노드 트리에 교체한다. → 잘못된 생각!
만약 동일한 노드(type과 index가 동일)하면 리렌더링 전의 VNode를 재사용 한다. 그리고 새롭게 생기는 VNode의 Props필드는 최신값이므로 덮어 쓴다. → 이렇게 되면 업데이트된 상태를 계속해서 사용할 수 있다.
→ 위의 과정을 추상화 하면 다음 사진과 같다.


사실 개발자가 원하는 결과는 위와 같지 않다. 파란색Item노드와 빨강색 Item노드의 순서가 바뀌면서 상태 또한 빨강색 노드의 상태인 state:1 은 그대로 리렌더링 후 인덱스 1인 노드(빨강색 노드) 에 바인딩 되고, 파랑색 노드의 상태인 state:2 는 리렌더링 후 인덱스 0 인 노드(파랑색 노드) 에 바인딩이 된는게 개발자가 원하는 결과이다.
하지만 key가 없기 때문에 index를 기준으로 비교를 하는데 이때 type이 동일해 동일한 인스턴스 노드라고 판단하여 엉뚱한 VNode를 재사용 하게된다. 그래서 실제로 리액트 팀에서는 key의 사용을 강력하고 권하고 있다.
(사실 공식문서로 key 가 중요하다~ 이런 말 계속 봤는데 실제로 이렇게 뜯어서 확인하니까 엄청 중요한 수준이 아니라 무조건 써야한다…)
✅ 렌더 단계와 Diffing의 동시적 진행
setState 등으로 리렌더링이 트리거되면, 변경이 발생한 컴포넌트(예: List)의 렌더링 함수(vnode.type(vnode.props) 호출)가 다시 실행된다.currentVNode)가 반환된다. 이 VNode 객체는 이전 VNode 객체와는 다른 독립적인 JavaScript 객체임.internalRender 함수가 이 새로운 VNode를 받아서 처리할 때, 단순히 DOM을 만들거나 트리를 구축하는 것을 넘어, 이 새로운 VNode를 이전 렌더링의 해당 위치에 있던 RenderedVNode (oldVNode)와 즉각적으로 비교(diff)한다.✅ diff 비교 과정에서는root의 VNode가 비교 되는게 아니라 root intenralRender 함수의 반환값인
RenderedVNode를 비교한다는걸 계속 인지 해야 한다. → (RenderedVNode로 이루어진 트리를 순회하면서 비교 하는거니까…)
→ root의 RenderedVNode와 새로운 VNode 비교
type 비교: oldVNode와 currentVNode의 type이 같은지 확인한다.key 비교 (리스트인 경우): type이 같고 리스트 안에 있다면 key를 비교한다.type과 key (또는 인덱스)가 일치하여 동일한 논리적 컴포넌트라고 판단되면, 해당 컴포넌트의 기존 인스턴스(상태를 가진)를 재사용한다. 즉, 이전에 만들어져 상태를 가지고 있던 그 컴포넌트 인스턴스를 버리지 않고 그대로 둔다. internalRender.ts에서 pushCurrentVNode와 popCurrentVNode를 사용하는 hookManager 부분이 바로 이 컴포넌트 인스턴스와 훅 상태를 연결하는 로직과 매우 중요한 연관이 있다. → 위의 함수들이 useState가 지금 어떤 노드에서 실행되는지 알 수 있게 해준다.props를 전달하여 컴포넌트 함수를 다시 실행한다. 이때 useState는 이 인스턴스에 연결된 기존 상태 값을 반환하므로, 상태가 유지된다. → 기존 상태 값을 반환 할 수 있는 이유는 전적으로 hookManager 덕분…나는 2번 과정에서 계속 기존의 상태를 주입 한다고 생각했는데 그게 아니다! 일단 사진을 전체 재조정 과정은 아래의 사진과 같다.

diff 를 렌더 단계와 분리해서 생각하면 안된다. 렌더 단계에서 새로운 VNode 트리를 구축하는 순간에 노드 단위에서 각 VNode를 이전 트리의 해당 노드와 diff (즉시 비교) 한다.RenderedVNode 트리 내부적으로 domRef를 통한 연결 상태를 업데이트할 뿐입니다. → 실제 DOM을 조작하는 행위는 위의 모든 작업이 끝난 뒤에, 변경사항이 발생한 표식을 추적해서 변경된 부분만 DOM을 수정해 최소한으로 DOM에 접근해야 한다.