치춘짱베리굿나이스

React를 클론코딩 #1 가상 돔 본문

ClientSide/React

React를 클론코딩 #1 가상 돔

치춘 2022. 10. 5. 02:44

React를 클론코딩 #1

React를 사용해서 클론코딩은 이제 흔하다

React를 클론코딩해보자

물론 정말 심도있는 .. 마치 리액트의 심연같은 .. 그런 기능까진 놓칠 수 있지만 대략적이고 큼직큼직한 내용을 한번 클론코딩해서 익혀보려 한다

https://github.com/chichoon/Raect

 

GitHub - chichoon/Raect: 리액트 및 자바스크립트 객체들 짝퉁 만들어보기

리액트 및 자바스크립트 객체들 짝퉁 만들어보기. Contribute to chichoon/Raect development by creating an account on GitHub.

github.com

참고로 레포지토리 이름은 리액트 짝퉁이라 Raect이다 (래익트 ㅋㅋ)

가상 DOM

https://blog.chichoon.com/746

 

React

React 서론 리액트 써도 좀 알고 쓰자!!! 의 일환이다 리액트로 토이 프로젝트 몇번 해 보면서 진지하게 리액트의 원리와 동작 과정에 관해 고찰해본 적이 몇 번이나 되는가? 나는… 없는 듯 하다

blog.chichoon.com

이전 포스팅에서 꽤나 열심히 다뤘으므로 짧게 요약하자면~~

가상 DOM이란 리액트에서 처음 도입한 개념으로, 매번 어떠한 값이나 노드에 변화가 있을 때마다 실제 DOM을 수정해서 렌더 트리를 자주 새로고침 하는 것보다, 변화를 가상 DOM에 기록한 뒤 변화 전과 후를 비교하여 변경된 부분만 렌더 트리에 적용하는 방식으로 레이아웃 계산 횟수를 줄이는 방법이다

원래대로라면 매번 DOM에서 변경할 노드를 찾고… 찾은 노드를 바꿔주고… 바꿔주면 그대로 렌더 트리에 적용하고… 를 반복하느라 코드도 무진장 길어지고 연산량도 많았으나, 이제는 가상 DOM에 먼저 변화를 적용해준 뒤 렌더링을 단 한 번만 진행하여 코드 길이도 짧고 연산 횟수도 줄어들었다

JQuery 시절에 그랬듯 $() 함수에 메서드 체이닝을 걸고… 걸고… 걸고… 할 필요가 없는 것이다

가상 DOM 만들어보기

1. HTML 태그 트리

<div class="title-wrapper">
    <h1>환영합니다!</h1>
    <p>이곳은 chichoon 블로그 입니다</p>
</div>

요런 태그를 이용해서 가상 DOM 시뮬레이션을 해 보자

2-1. HTML 태그 트리를 Javascript 객체 형태로 만들어보기

{
    type: "div",
    attributes: {
        class: "title-wrapper"
    },
    children: [
        {
            type: "h1",
            attributes: {},
            children: ["환영합니다!"]
        },
        {
            type: "p",
            attributes: {},
            children: ["이곳은 chichoon 블로그 입니다"]
        }
    ]
}

HTML 태그는 태그 종류, 속성, 자손 3가지 요소로 이루어져 있으므로, 자바스크립트 객체로 풀면 위와 같은 형태로 제작이 가능하다

자바스크립트 문법 형태로 트리를 만들어 본 것이다

자바스크립트에선 함수도, 클래스도 모두 객체의 일종이므로 결국 근-본 형태로 작성해본 것이라고 할 수 있겠다

 

interface AttributeObject {
  [key: string]: string;
}

interface DOMObjectType = {
    type: string,
    attributes: AttributeObject,
    children: Array<DOMObjectType | string>
}

타입스크립트 타입으로 치면 이런 형태가 되겠다

속성 (attribute) 에 어떤 값이 들어올 지 확실하게 모르기 때문에 (태그별로 가질 수 있는 속성 종류가 조금씩 다르므로) 객체의 key와 value가 문자열이라는 것만을 타입에 명시해주었다

 

실제 리액트 타입을 열어보면 아예 각 태그별로 속성 종류가 다 지정되어 있다

(HTMLAttributes가 모든 태그들이 갖고 있는 공통 속성들 인터페이스, ButtonHTMLAttributes가 이를 상속받아 버튼의 속성을 추가한 인터페이스)

@types/react 모듈의 index.d.ts를 열어보면 광기의 타입정의 현장을 볼 수 있다 (…) 나름 네이밍은 잘 되어 있어서 무엇을 위한 인터페이스인진 나름? 읽기 쉽다

<T> 는 C++ 템플릿과 비슷한 쓰임새의 제네릭 타입이다 (런타임 때 타입이 결정된다)

 

function createDOMElement(domObject: DOMObjectType) {
    const element = document.createElement(domObject.type);

    const attributeKeys = Object.keys(domObject.attributes);
    attributeKeys.forEach(
        (key) => element.setAttribuet(key, domObject.attributes[key])
    ); // 속성 추가

    element.children.forEach((child) => 
        typeof child === 'string'
            ? element.appendChild(document.createTextNode(child))
            : element.appendChild(createDOMElement(child))
    ); // 자식 노드 추가
    return element;
}

객체 형태의 가상 DOM 노드를 이용하여 실제 DOM 트리를 만드는 함수는 위와 같다

실제 DOM 노드를 document.createElement릁 통해 생성하고, 속성을 적용해준 후 자식 노드를 재귀로 추가해주는 방식이다

만약 자식 노드가 단순 문자열이라면, createTextNode를 통해 텍스트 노드를 생성한다

2-2. HTML 태그 트리를 Javascript 클래스 형태로 만들어 보기

객체 형태로 가상 DOM을 작성하게 되면 실제 DOM 노드를 생성하는 과정을 별도의 함수로 구현해서 매번 따로 호출해 주어야 했다

프로토타입을 이용하여 부모가 되는 객체를 생성해 놓고 이를 상속받는 식으로 구현하면 함수를 메서드로 추가하여 호출하기 간편하고, 클래스를 사용하면 객체 상속을 더 쉽게 할 수 있다 (사실 내가 클래스가 프로토타입보다 더 익숙한 것 뿐이다)

🤔 사실 자바스크립트에선 함수도 객체라서 어떤 방법이든 결국 원점은 객체이긴 한데..

 

interface AttributeObject {
  [key: string]: string;
}

export class VirtualDOMNode {
  #type;
  #attributes;
  #children;
  constructor(type: string, attributes: AttributeObject, children: Array<VirtualDOMNode | string>) {
    this.#type = type;
    this.#attributes = attributes;
    this.#children = children;
  }

  createDOMElement() {
    const element = document.createElement(this.#type);

    const attributeKeys = Object.keys(this.#attributes);
    attributeKeys.forEach((key) => element.setAttribute(key, this.#attributes[key]));

    this.#children.forEach((child) =>
      typeof child === 'string'
        ? element.appendChild(document.createTextNode(child))
        : element.appendChild(child.createDOMElement())
    );
    return element;
  }
}

앞서 만든 객체형 노드를 클래스 형태로 바꿔 보자

이번엔 아예 내부 메서드로 실제 DOM 노드를 만들어주는 함수를 추가했다 (createDOMElement)

각 속성값 (type, attributes, children) 은 외부에서 건드리지 못하도록 private로 숨겨 주었다

 

// Virtual DOM Tree (with class)
const virtualDOM = new VirtualDOMNode('div', { class: 'title-wrapper' }, [
    new VirtualDOMNode('h1', {}, ['환영합니다!']),
    new VirtualDOMNode('p', {}, ['이곳은 chichoon 블로그 입니다']),
]);

// create real DOM tree
document.querySelector('#root')?.appendChild(virtualDOM.createDOMElement());

우리가 만든 클래스형 가상 DOM을 실제 화면에 렌더링 해 보았다

훌륭하다! 가상 DOM으로 실제 렌더 트리를 만드는 것까지 성공하였다

3-1. 트리 렌더링 메서드 만들어보기

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App'; // 자식 요소

const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

원본 리액트에서는 이렇게 최상단 노드를 생성한 뒤, render 메서드를 이용하여 자식 노드들을 렌더링한다

최상단 노드를 추가하여 반환하는 createRoot와, 가상 돔 비교 없이 바로 렌더링하는 root.render 메서드를 만들어보자

 

export class RootNode {
  #rootDOMNode;
  constructor(rootNode: Element) {
    this.#rootDOMNode = rootNode;
  }

    render(childNode: VirtualDOMNode) {
        this.#rootDOMNode.replaceChildren(childNode.createDOMElement());
  }

  unmount() {
    this.#rootDOMNode.innerHTML = '';
  }
}

RootNode 클래스를 추가하고, 아무런 검증 절차도 없이 대뜸 렌더링만 해주는 메서드를 (ㅋㅋㅋ) 만들어 보았다

실제 리액트 원본에선 render 메서드 내부에 업데이트 큐에 최상위 노드를 등록하는 등 변경점이 발생하면 자동으로 리렌더링 (뷰 업데이트) 을 수행하는 로직이 들어있다

unmount도 만들어주…긴 했지만 일단은 최상위 노드를 비워주는 역할만 한다

 

export function createRoot(rootNode: Element | null) {
  if (!rootNode) throw new Error('No root node available');
  const root = new RootNode(rootNode);

  return root;
}

위의 클래스 인스턴스를 반환해주는 createRoot 함수를 만들자

마찬가지로 추가 설정은 전혀 없이, RootNode를 생성하여 반환한다

만약 rootNodenull일 경우 (대개 document.getElementByID가 실패했을 경우) 에러를 throw하여 렌더링이 멈추게끔 하였다

 

const root = createRoot(document.getElementById('root'));
root.render(
  new VirtualDOMNode('div', { class: 'title-wrapper' }, [
    new VirtualDOMNode('h1', {}, ['환영합니다!']),
    new VirtualDOMNode('p', {}, ['이곳은 chichoon 블로그 입니다']),
  ])
);

이제 최상단 index.ts에서 createRoot를 호출하여 root 요소를 만들고, root.render를 통해 화면에 한번 렌더링을 해 보자

 

굿

저 위의 이미지와 똑같아 보인다면 기분탓이다..

3-2. 함수를 도입해서 조금 더 간략하게 만들기

new VirtualDOMNode('div', { class: 'title-wrapper' }, [
    new VirtualDOMNode('h1', {}, ['환영합니다!']),
    new VirtualDOMNode('p', {}, ['이곳은 chichoon 블로그 입니다']),
]);

생성자를 매번 호출하자니 너무 지저분하다

이름이 짧은 함수로 한 번 감싸서 더 간략하게 만들어주자

리액트 공식 문서에서는 hyperscript라는 것을 알려주고 있는데, 사실상 React.createElement를 짧은 이름의 변수에 할당해서 간결하게 만든 것과 비슷하다

 

export function e(type: string, attributes: AttributeObject, children: Array<VirtualDOMNode | string>) {
  return new VirtualDOMNode(type, attributes, children);
}

e라는 함수를 만들었다

이름은 짧을 수록 간결하…지만, 등가교환으로 무슨 일을 하는 함수인지 알아보기 어려워진다

나는 Element의 첫 글자인 e로 이름을 지었다

내부에서 하는 일은 VirtualDOMNode 생성자를 호출해서 인스턴스 하나를 만드는 것밖에 없으므로, new VirtualDOMNode를 일일히 적을 때와 완벽하게 같은 일을 하는 대신 이름이 짧아졌을 뿐이다

이름을 줄이기 위해 alias 느낌으로 함수를 썼다고 생각하면 된다

 

e('div', { class: 'title-wrapper' }, [
    e('h1', {}, ['환영합니다!']),
    e('p', {}, ['이곳은 chichoon 블로그 입니다']),
]);

훨씬 짧아졌다

JSX는 e() 대신 <></> 구문을 선택했다고 보면 된다

4-1. 가상 DOM 비교 후 변경점만 렌더링하기 - 서론

대망의 ‘가상DOM을 사용하는 이유' the 변경점 비교하기 알고리즘이다

쫌 까다로운 것이, 아직 State를 구현하지 않았기 때문에 특정 값만 휙휙 바꿔보기 애매하다

따라서 setInterval을 이용하여 1초마다 가상 DOM을 바꿔주는 식으로 진행해 볼 것이다

 

let oldVirtualDOM = e('div', { class: 'title-wrapper' }, [
  e('h1', { class: 'title-header', id: 'header' }, [e('i', {}, ['환영합니다!'])]),
  e('p', { class: 'title-header', id: 'header-p' }, ['이곳은 chichoon 블로그 입니다']),
]);

let newVirtualDOM = e('div', { class: 'title-wrapper' }, [
  e('h1', { class: 'title-header', id: 'header' }, [e('i', {}, ['안녕히 가세요!'])]),
  e('p', { class: 'title-sub-header', id: 'header-p' }, ['다음에도 또 방문해 주세요']),
]);

두 개의 가상 DOM 트리를 생성해 두자

보다시피 i 태그와 p 태그 안의 내용물, 그리고 p 태그의 클래스만 서로 다를 뿐, 나머지는 다 동일하다

 

<div class='title-wrapper'>
    <h1 class='title-header' id='header'><i>환영합니다!</i></h1>
    <p class='title-header' id='header-p'>이곳은 chichoon 블로그 입니다</p>
</div>

<div class='title-wrapper'>
    <h1 class='title-header' id='header'><i>안녕히 가세요!</i></h1>
    <p class='title-sub-header' id='header-p'>다음에도 또 방문해 주세요</p>
</div>

위의 두 태그 트리를 가상 DOM 형태로 만들었을 뿐이다

 

const root = createRoot(document.getElementById('root'));
root.render(oldVirtualDOM);

setInterval(() => {
  const temp = oldVirtualDOM;
  oldVirtualDOM = newVirtualDOM;
  newVirtualDOM = temp;
  root.render(oldVirtualDOM);
}, 1000);

이번엔 setInterval을 이용해 두 트리를 번갈아가며 렌더링해 줄 것이다

oldVirtualDOM 내용물과 newVirtualDOM 내용물을 서로 교체한 뒤, oldVirtualDOM 내용물을 렌더링하는 방식으로 두 트리의 내용물을 번갈아 출력한다

 

1초마다 제대로 리렌더링은 되지만, 분명 우리가 바꾼 것은 내부 텍스트만인데 전체 div가 교체되는 것을 개발자 도구를 통해 알 수 있다

실제로 바뀌고 있는 값은 i 태그와 p 태그 내의 문자열 그리고 클래스 뿐인데, 이 때문에 전체 렌더 트리를 죄다 다시 그리는 것은 비효율의 극치이다

문자열과 몇몇 속성만 슬쩍 바꿔주면 되는데도 div 태그부터 다시 그리고 있기 때문이다…

 

아래의 내용부터는 상당히 복잡하고… 길다

노드 하나하나를 비교해야 하기 때문에 조건 분기가 매우 많은 편이다

가상 DOM 노드만 생성해놓고 헤헤 거의 다했다 하면서 실실 웃었는데 비교 로직 작성하면서 급정색했다

리팩토링할 미래의 나도 화이팅!

4-2. 가상 DOM 비교 후 변경점만 렌더링하기 - RootNode 클래스 개조

type VirtualDOMNodeType = VirtualDOMNode | null;

export class RootNode {
  #rootDOMNode: Element;
  #virtualDOMNode: VirtualDOMNodeType;
  constructor(rootNode: Element) {
    this.#rootDOMNode = rootNode;
    this.#virtualDOMNode = null;
  }

  render(childNode: VirtualDOMNode) {
    this.#rootDOMNode.replaceChildren(childNode.createDOMElement());
    this.#virtualDOMNode = childNode;
  }

  unmount() {
    this.#rootDOMNode.innerHTML = '';
    this.#virtualDOMNode = null;
  }

  update(newChildNode: VirtualDOMNodeType) {
    // 현재 Virtual DOM과 실제 DOM을 업데이트 하는 함수
        updateEachNode(this.#rootDOMNode, this.#virtualDOMNode, newChildNode, 0);
        this.#virtualDOMNode = newChildNode;
  }
}

위에서 만들었던 RootNode를 개조해 보자

  • 우선 내부 Private 프로퍼티로 virtualDOMNode라는 것을 갖게 된다
    • virtualDOMNode는 렌더링에 사용되는 (기준이 되는) 가상 DOM 트리이며, 첫 렌더링 때 RootNode에 저장된다
    • unmount는 가상 돔을 언마운트시키므로 virtualDOMNodenull로 초기화할 것이다
  • 그리고 update 메서드를 추가한다
    • update 메서드에 새로운 가상 DOM 트리를 인자로 넣어 주면, 기준 가상 DOM과 새로운 가상 DOM을 비교하여 변경점만 적용해 줄 것이다
    • 그리고, 기존의 가상 DOM 트리를 새로운 트리로 교체한다

그러면 아까는 setInterval 내에서 매번 다시 render시켜주는 대신 update 메서드를 호출하여 변경점만 바꿔줄 수 있을 것이다…!

4-3. 가상 DOM 비교 후 변경점만 렌더링하기 - 노드 비교 및 DOM 교체 함수

update 메서드가 가져야 할 기능은

  1. 기준 노드 (oldNode) 와 최신 노드 (newNode) 를 비교
  2. 두 노드가 같다면, 스킵
  3. 두 노드가 다르다면, 최신 노드 (newNode) 의 값을 기준 노드 (oldNode) 및 실제 DOM 노드 (parentNode) 와 동기화
    • 이때 parentNode는 실제 DOM 트리에서 두 노드의 부모격 노드로, 자식 노드를 삭제하거나 추가하는 것이 쉽기 때문에 부모 노드를 가지고 조작한다

비교 기능부터 만들어 보자

재귀를 사용해서 DFS로 노드를 탐색하므로 재귀를 모른다면… 조금 어렵다

재귀와 자식 노드 인덱스를 이용하는 방법은 이 링크에서 모티브를 얻었다

 

type VirtualDOMNodeType = VirtualDOMNode | null;

export function updateEachNode(
  parentNode: Element,
  oldNode: VirtualDOMNodeType,
  newNode: VirtualDOMNodeType,
  index: number
): void {
  if (!oldNode) { // oldNode에 값이 없을 경우 (노드 추가)
    if (newNode) 
            parentNode.appendChild((newNode as VirtualDOMNode).createDOMElement());
    return;
  }
  if (!newNode) { // newNode에 값이 없을 경우 (노드 삭제)
    parentNode.removeChild(parentNode.childNodes[index]);
    return;
  }
  if (oldNode.getType() !== newNode.getType()) { // old, new 태그명이 다를 경우
    parentNode.replaceChild(
            newNode.createDOMElement(), 
            parentNode.childNodes[index]
        );
  } else updateEachAttributes(parentNode, oldNode, newNode, index); // 속성 업데이트
  updateNextNode( // 재귀로 다음 노드 (자식 노드) 업데이트
        parentNode, 
        oldNode as VirtualDOMNode, 
        newNode as VirtualDOMNode, 
        index
    );
}

인자값

  • oldNodenewNode에 값이 아무 것도 안 들어올 가능성이 있다 (null)
    • 이전 가상DOM에는 존재하지 않는 노드였는데 새로운 DOM에서 요소가 추가됐다던가, 이전 가상DOM에는 존재했지만 새로운 DOM에서는 삭제되었을 경우
    • 따라서 oldNodenewNode의 타입을 VirtualDOMNode | null으로 재정의하여 null값이 올 수 있도록 한다
  • indexparentNode의 몇 번째 자식인지 알려주는 변수이다
    • 실제 DOM 노드인 parentNode와, 그 자식 노드의 인덱스를 이용하여 실제 DOM 트리에도 변경점을 반영해 볼 것이다

 

내부 로직

  • oldNodenull일 경우
    • 이전 DOM에는 존재하지 않는 노드였는데 새로운 DOM에서 추가되었을 경우이다
    • 만약 새로운 DOM에서도 아무런 노드도 추가되지 않았다면 (newNodenull이라면) 아무 동작도 하지 않고 반환한다
    • 그 외에는, newNode를 부모 노드 (parentNode) 의 밑에 추가한다 - 실제 DOM을 조작해야 하므로 아까 만든 createDOMElement 메서드를 이용한다
    • newNode를 그대로 복사했기 때문에 자식 노드가 일치한다고 판단하고, 자식 비교 (updateNextNode) 를 하지 않고 반환한다
  • newNodenull일 경우
    • 이전 DOM에는 존재했던 노드였는데 새로운 DOM에서 삭제된 경우이다
    • 부모 노드에서도 index를 이용하여 몇 번째 자식 노드가 삭제되었는지 파악하여 삭제한다
    • 노드가 삭제되어 자식이 존재하지 않으므로, 자식 비교 (updateNextNode) 를 하지 않고 반환한다
  • oldNodenewNode의 태그 (type) 가 다를 경우
    • 예시: oldNodediv인데 newNodep일 경우
    • 이럴 때는 oldNodenewNode로 교체해야 한다 (태그명만 바꿔주는 메서드가 없다)
    • replaceChild 메서드를 통해 실제 DOM의 내용물을 교체해주자
  • 그 외 케이스
    • 태그명이 일치하므로,
    • 내부 속성을 비교한 뒤 (updateEachAttributes)
    • 다음 노드를 비교한다 (updateNextNode)

4-4. 가상 DOM 비교 후 변경점만 렌더링하기 - 속성 (Attribute) 비교 함수

function updateEachAttributes(
        parentNode: Element, 
        oldNode: VirtualDOMNode, 
        newNode: VirtualDOMNode, 
        index: number
) {
  const oldAttributes = oldNode.getAttributes(); // 이전 가상 DOM 노드의 모든 Attribute
  const newAttributes = newNode.getAttributes(); // 새로운 가상 DOM 노드의 모든 Attribute

  const attrKeys = new Set(
        [...Object.keys(oldAttributes), ...Object.keys(newAttributes)]
    ); // 두 Attribute Object의 key를 모두 가져옴

  attrKeys.forEach((key) => {
    if (!newAttributes[key])
            (parentNode.childNodes[index] as Element).removeAttribute(key);
    else if (!oldAttributes[key] || oldAttributes[key] !== newAttributes[key])
      (parentNode.childNodes[index] as Element).setAttribute(key, newAttributes[key]);
  });
}

앞에서 노드가 추가됐을 때, 삭제됐을 때, 태그명을 모두 비교했으므로 이번엔 속성만 비교하여 달라진 값만 실제 DOM에 적용한다

우선 이전 노드 (oldNode) 와 새로운 노드 (newNode) 의 모든 속성 key를 가져와, Set() 컨테이너를 통해 중복을 제거한다

두 노드의 key를 모두 가지고 비교해야 어디서 속성이 삭제됐고 어디서 추가됐는지 알 수 있기 때문이다

 

  • 만약 newNode가 해당 key를 가지고 있지 않을 경우
    • attrKeys Set에는 해당 key가 저장되어 있으므로, oldNode는 해당 속성을 가지고 있다는 의미
    • 이전 (oldNode) 에는 해당 속성이 존재했으나, 새로운 DOM에는 속성이 삭제됨
    • removeAttribute를 통해 실제 DOM에서도 속성 삭제
  • 만약 oldNode가 해당 key를 가지고 있지 않거나, oldNode의 속성 값과 newNode의 속성 값이 다를 경우
    • 예를 들어, oldNode의 클래스는 title-header인데 newNode의 클래스는 title-sub-header일 경우 두 속성 값이 다른 것이다
    • oldNode가 key를 가지고 있지 않다면, attrKeys Set에는 해당 key가 저장되어 있으므로, newNode는 해당 속성을 가지고 있다는 의미
    • 이전 (oldNode) 에는 해당 속성이 없었으나, 새로운 DOM에서 속성이 추가됨
    • setAttribute를 통해 실제 DOM에서도 속성 삭제

4-5. 가상 DOM 비교 후 변경점만 렌더링하기 - 다음 노드로 이동하여 계속 비교

function updateNextNode(
    parentNode: Element, 
    oldNode: VirtualDOMNode, 
    newNode: VirtualDOMNode, 
    index: number
) {
  const oldNodeChild = oldNode.getChildren();
  const newNodeChild = newNode.getChildren();
  const parentNodeChild = parentNode.childNodes[index] as Element;

  for (let i = 0; i < newNodeChild.length || i < oldNodeChild.length; i += 1) {
    if (typeof oldNodeChild[i] === 'string') { // oldNode의 i번째 자식이 문자열일 경우
      parentNodeChild.replaceChild(
        typeof newNodeChild[i] === 'string'
          ? document.createTextNode(newNodeChild[i] as string)
          : (newNodeChild[i] as VirtualDOMNode).createDOMElement(),
        parentNodeChild.childNodes[i]
      );
    } else if (typeof newNodeChild[i] === 'string') // newNode의 i번째 자식이 문자열일 경우
      parentNodeChild.replaceChild(document.createTextNode(newNodeChild[i] as string), parentNodeChild.childNodes[i]);
    else
      updateEachNode(
        parentNode.childNodes[index] as Element,
        oldNodeChild[i] as VirtualDOMNode,
        newNodeChild[i] as VirtualDOMNode,
        i
      );
  }
}

마지막으로, 모든 값 (태그명, 노드 존재여부, 속성값) 을 비교한 뒤에는 다음 노드로 넘어가서 마저 비교할 차례이다

여기서는 DFS 방식으로 비교하므로, 먼저 자식 노드로 계속 타고 들어가 비교한 뒤 부모 노드로 빠져나오는 방식을 취한다

함수의 길이가 긴 이유는, 자식 노드가 단순 문자열일 경우 처리가 달라져야 하기 때문이다

 

  • oldNode의 자식 개수와 newNode의 자식 개수 중 더 많은 쪽만큼 순회한다
    • 한 쪽의 자식 개수가 많을 경우, 노드를 추가하거나 삭제해서 짝을 맞춰야 하기 때문이다
  • oldNodei번째 자식 (oldNodeChild[i]) 이 문자열일 경우
    • newNodei번째 자식 (newNodeChild[i]) 도 문자열일 경우, createTextNode를 이용하여 문자열 노드 생성 후 replaceChild를 이용해 i번째 자식을 교체한다
    • newNodei번째 자식이 문자열이 아닐 경우, 클래스에 만들었던 createDOMElement 메서드를 이용하여 DOM 노드로 변환 후 replaceChild를 이용해 i번째 자식을 교체한다
  • newNodei번째 자식이 문자열일 경우
    • oldNodei번째 자식도 문자열일 경우는 위에서 처리했다
    • createTextNode 메서드를 이용하여 문자열 노드 생성 후, 기존에 있던 i번째 자식을 replaceChild를 통해 단순 교체한다
  • 그 외의 경우, oldNodei번째 자식도, newNodei번째 자식도 문자열 노드가 아니므로 i번째 자식들에 대해 updateEachNode를 호출하여 노드 비교 과정을 반복하면 된다
    • updateNextNode 함수는 updateEachNode 함수 내부에서 호출되므로, 결국 updateEachNode 함수는 재귀적으로 호출된다고 할 수 있다

4-5. 가상 DOM 비교 후 변경점만 렌더링하기 - 결과

이제 실제로 변경된 값 (속성, 내부 문자열 등) 만 깜빡이는 것을 볼 수 있다

부모 노드까지 리렌더링되는 것이 아니기 때문에… 부모 div나 h1은 깜빡이지 않는 것을 볼 수 있다

길었다…

여담

다음에는 상태값을 저장하고 변경하는 로직 (useState()) 을 한번 구현해볼까 한다

근데 그 전에 로그인이 만들고 싶어서… 잠시 쿠키와 세션으로 튈 예정,, ㅎ


참고자료

https://stackoverflow.com/questions/49622045/in-typescript-what-does-t-mean

https://ko.reactjs.org/docs/react-without-jsx.html

https://wookgu.tistory.com/11

https://velog.io/@hanei100/React-React.createElement

https://dev.to/buttercubz/explained-and-created-a-simple-virtual-dom-from-scratch-5765

https://medium.com/@deathmood/how-to-write-your-own-virtual-dom-ee74acc13060

'ClientSide > React' 카테고리의 다른 글

React의 useContext  (0) 2023.05.06
Suspense와 Error Boundary를 이용한 로딩과 예외처리  (0) 2022.12.10
React  (0) 2022.10.01
useClickOutside 직접 구현하기  (0) 2022.07.01
react-portal 사용해보기  (0) 2022.05.15
Comments