프레임워크(myReact)재조정 설계

MIlo·2025년 6월 27일

diffTrees 함수의 알고리즘 생각하다보니… 일단 새롭게 리렌더링 될때 만들어지는 VDOM을 생성해 놓고, 그 다음에 비교를 해야 하는데…?

전체 흐름 구상

  • diffTrees를 통해 달라진 점만 찾아서 노드를 교체할 계획인데 그럴려면 실제 리렌더링시에 새롭게 만들어지는 가상 돔이 필요하다.

  • 꼭 지켜야할 규칙! → 기존의 rootDOM을 기준으로 ‘하나의 노드’ 씩 그려나가야한다.

  • 위가 rootDOM이라고 하면 새롭게 만들어지는 가상 DOM은 이 돔을 기반으로 그려져야 한다.

  • 만약 여기서 3에서 리렌더링이 발생하면 1부터 다시 그려질게 아니라 3의 노드 부터 그려져야한다.

  • 그러면 어떻게 해야할까?

  • 우선은 rootDOM으로 부터 리렌더링이 발생한 VNode를 리렌더링 함수에 입력을 한다.

    • 이렇게 되면 업데이트된 최신의 상태값은 유지가 된다.
  • 하지만 하위(=4,5 번 노드)의 노드들은 새롭게 노드가 그려지므로 기존의 상태(=hookMetaData필드)가 유지되지 않은채 완전히 새로운 노드가 만들어져 버린다.

최선의 방법은 reRender 가 실행되는 순간에 각각의 Node에 접근해서 생성을 하는데 각 Node에 진입하는 순간에!!! (type 이 함수일 경우 함수를 호출 하기 전에!!)

  • 기존의 rootVNode와 비교를 해주는 함수를 사용해 비교를 한뒤
  • 동기화 작업이 필요하면 동기화를 해주는 synchronizeNode함수를 사용해 동기화를 시켜준다.
  • 그러면 유지된 최신의 상태 필드를 얻는다.
  • 업데이트된 Node를 바탕으로 함수 컴포넌트를 호출한다.
  • 위의 과정을 재귀로 반복한다.

  • 위의 흐름을 잘 살펴보면 지금 상황에서 필요한 함수가 두개 보인다.
    1. OldVNode와 NewVNode를 비교하는함수 (비교만 한다!!_역할 분리 집중)
    2. 데이터의 동기화가 필요한 상황에서(즉 1번 함수에서 동일하다고 판단이 된 상황) 노드의 데이터를 동기화 해주는 함수.

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

  • 다음 단계로는 만들어진 VDOM과 실제 rootDOM을 비교해 실제로 바뀐 부분의 노드만 변경합니다.(DOM변경 최소화)

위의 작업의 전체 흐름은 다음과 같다.

우선은 첫번째 작업부터 진행 해보겠다.

디테일 설계 아이디어

synchronizeTree 함수

특정 노드에서 setter 함수 호출시 발생하는 함수다. 각 노드를 DFS로 순환하면서 동기화 작업을 실행한다. 최종적으로 실제로 화면에 그려져야할 DOM을 반환한다.

  • 매개변수 → setter 함수가 발생한 컴포넌트 함수를 재 호출된 결과값 : 최신 VNode

    • type의 typeof === function 일 경우해당 type 재 호출(매개변수는 최신화된 VNode의 prop)
    • → 기존의 VNode를 넣으면 안된다.. 왜? → 상태 관련 필드는 최신화 상태지만, 만약 해당 컴포넌트가 조건부 렌더링 로직이 있다면? 기존의 VNode의 children필드에는 해당 노드가 포함되어 있지 않다. 그래서 컴포넌트를 일단 실행 시켜야 한다…
  • 매개변수를 넣었으면 기존의 innternalRender 함수와 동일하게 DFS 순회를 진행한다.

  • 이때 각 노드에 접근 할때마다 rootDOM트리도 동시에 순환한다. 같이 순환하면서 노드단위로 비교작업을 한다. → // TODO DFS 순회 함수 구현

  • 각 노드 단위로 비교를 진행한다. 비교 기준은 다음과 같다.

비교는 DFS 의 후위 순회한다는 전제로 진행한다…

  • 비교조건
    1. 호스트 엘리먼트일 경우 비교 중단. 새로 만들어진 Node를 사용한다.
    2. 함수형 컴포넌트일 경우 두개의 Node(root트리의 Node 와 새롭게 만들어지는 Node)를 비교한다.
      1. 컴포넌트 이름을 기준으로 비교합니다.
      • 이름이 같으면 동기화 작업을 진행합니다.
      • 이름이 다르면 새로운 컴포넌트로 인식하고 동기화 작업을 진행하지 않고 건너 뛴다.

위의 작업을 각 Node에 집입할때 진행하고 다음 Node로 이동한다.

  • 동기화 작업

    • rootDOM과 새롭게 만들어지는 VDOM을 Node단위로 비교한다.
    • 위의 비교 조건에서 동기화 작업이 필요하다고 판단되면 동기화를 한다.
    • 동기화 작업은 목적은 해당 컴포넌트가 최신 hookMetaData필드를 갖고 호출되는 것.
      • 이래야 해당 컴포넌트 안의 useState함수가 다시 초깃값을 갖고 호출되지 않고, 기존의 업데이트된 상태값을 기준으로 호출된다.
    1. 기존의 rootDOM의 노드에서 hookMetaData를 복사해 새롭게 만들어질 Node에 주입한다.
  • 이러면 동기화 작업은 완료된다.

  • 위의 작업이 모두 완료되면 업데이트된 상태값을 갖고있는 VDOM이 만들어진다.

  • 이 VDOM을 rootDOM으로 변경한다.

수정사항

  • 근데 이렇게 트리를 순회하려면 트리가 있어야 하는데 지금 root에는 트리가 들어있는 상태가 아님…

설계 진입

우선 기존의 rootDOM의 rootVnode에서 리렌더링이 발생한다.

이때는 기존의 hookMetaData필드가 VNode안에 있으므로 최신 상태를 갖고 리렌더링이 된다.

하지만 문제는 새롭게 리렌더링을 할때 생기는 하위 노드에서 발생한다. 하위 노드가 새롭게 생기면서 기존의 노드의 hookMetaData를 유지하지 않고 초깃값으로 생겨난다.

→ 그래서 본질적으로는 기존 rootNode와 새롭게 리렌더링되는 newNode와 비교해서 hookMetaData를 넣어줘야한다.

본질적 문제 발견

  • 하지만 위의 설계 처럼 트리 순환을 하려면 Node객체로 이루어진 tree구조가 있어야한다.(기준점 필요) 하지만 지금은 노드를 순환하면서 바로 DOM으로 변환해 버려서 남아있지를 않다. 그렇게되면..? → 비교를 할 수 있는 대상이 없다. 즉 위 사진의 빨간 tree구조가 없다.

Node객체 tree 생성.__renderComponentTree

결국 객체로 이루어진 전체 트리구조가 필요하다. 그래야 빠르게 비교를 해 바뀐 부분을 찾을 수 있다. 해당 트리 생성은 다음과 같은 상황에서 필요하다.

  1. 초기 렌더링:
    1. 초기에 전체 트리구조를 만들어야 이후에 다시 렌더링이 될때 어떤 기준으로 변경해야할 기준점이 필요하다. 그래서 초기에 전체 트리를 만들어야 한다.
  2. 상태 또는 속성 변경 시:
    1. 상태 변경시 새롭게 트리를 그려야 변한 상태를 기준으로 그려진 최신 노드 정보로 이루어진 트리를 얻을 수 있다. 그리고 이 트리를 기준으로 바뀐 부분만 DOM에 적용해야한다.

이 트리 구조는 최종적으로 중첩 객체로 이루어져있다. 자식으로 컴포넌트가 있다면 자식 컴포넌트를 실행시켜서 새로운 객체로 풀어버린다. 이런 과정을 재귀적으로 실행시키면 하위의 모든 노드들이 객체로 풀어져 있다.

→ 그 이후에는 위의 로직 사용한다.


→ 일단 아래는 무시

문제 발견_새로운 트리를 만드는 함수를 따로 구현 하지 않는다.(중복 로직)

기존의 internalRender 함수를 리팩토링하여 Node순환을 한번만 한다.

트리 생성 설계

기존 로직의 오류_ 잘못된 생각 수정사항

일단 계속해서 내가 헷갈려 하는 부분을 찾았다. 내가 생각하는 기본적인 트리 구조는 각 노드들이 동일한 데이터 구조(ex. 객체)를 갖고 있고, 만약 자식이 있다면 각 노드들이 자식으로 중첩된 객체 구조로 이루어져 있다고 생각했다. 그리고 지금 만드려는 트리 구조도 계속 이런 생각으로 설계를 시도했다. 하지만 여기서 이상한 부분이 있다.

✅ 함수형 컴포넌트 Vnode와 호스트 엘리먼트 VNode

이 두개는 internalRender 함수를 실행 하면 다른 결과가 나온다. 우선 이 두개를 다시 정의해 보자.

  • 노드의 종류:
    • 호스트 엘리먼트 VNode(type: string, 예: <div>, <button> ): 실제 DOM 엘리먼트에 직접 대응 되는 노드. 이 노드는 children 속성을 통해 다른 vnode를 자식을 갖는다. → 내가 생각하는 일반적이 노드.
    • 함수형 컴포넌트 VNode (type: function, 예: <App>, <Btn>): 이 노드는 그 자체가 DOM엘리먼트가 아니다. 즉 이 노드 단독으로는 DOM을 그릴수 없다. 해당 컴포넌트 함수를 호출해 해소해줘야 한다.(함수형 컴포넌트를 실행 했을때의 반환값은 하위 VNode를 반환하기에…) 보통 호스트 엘리먼트 VNode를 반환한다. → 자기 자신을 실행하면 다른 VNode를 ‘생산’한다.
  • 부모 - 자식 관계:
    • 호스트 엘리먼트 VNode: children 속성을 통해 자식 VNode (호스트 엘리먼트 VNode 또는 함수형 컴포넌트 VNode)를 가진다.
    • 함수형 컴포넌트 VNode: 이 노드의 자식은 props.children 이 아니다. 이건 컴포넌트에 전달되는 prop일 뿐이다. 이 노드의 자식은 함수형 컴포넌트가 반환한 결과의 Vnode이다.
  • 진짜 과제: 함수형 컴포넌트는 실제 DOM 트리에 직접 나타나지 않는다. (반환된 VNode가 나타나는 것일 뿐) 어떻게 이 함수형 컴포넌트와 해소된 결과를 VNode트리에 연결할지를 해결 해야한다.

해결책(생각의 흐름)

✅ type이 함수형 컴포넌트라도 일단 type이 호스트 엘리먼트가 되도록 모두 풀어서 중첩 객체로 표현할까?

이 생각처럼 하려면 2단계가 필요하다.

  1. ✅ 함수 해소 : 함수형 컴포넌트는 함수를 호출해서 새로운 하위 VNode를 얻어야한다.(VNode로 해소해야한다)
  2. ✅ 트리 연결: 해소된 VNode도 그냥 두는게 아니라 다시 렌더링을 하게된다. 이때의 결과인 RenderdeVNode 가, 원래의 상위 함수형 컴포넌트 RenderedVnode의 _renderedChildVNode속성으로 중첩되야한다. 그리고 해소된 Vnode의 internalRender 함수의 결과값인 RenderedVnode 는 그 자식들을 _renderedChildren 으로 중첩 시킨다.

RenderVNode 구조 설계

기존의 internaRender 함수의 반환값은 없었지만 트리구조를 그리기 위해서는 RenderVNode를 반환 해야한다. RenderVNode의 인터페이스는 다음과 같다.

  1. 기존의 VNode를 확장

  2. _renderedChildVNode 필드

    1. 입력받은 VNode가 컴포넌트 함수일 경우 해당 함수를 해소 할때의 값.
  3. _renderedChildren

    1. 호스트 엘리먼드가 internalRender실행시 반환하는 Vnode 자식들
  4. domRef

    1. 각각의 노드(RenderVNode)들이 생성될때 실제 DOM에 그려지는 노드를 참조한다.
    2. 해당 필드가 있어야 실제 변화가 발생했을 때 정확히 어디를 변경해야할 지 알 수 있다.
    3. 각 internalRender 마다
      const rootNode: HTMLElement = document.createElement(vnode.type);

    위처럼 생성되는 노드를 참조한다. 이러면 최종적으로 만들어지는 트리만 갖고도 실제 DOM에 접근 할 수 있다.

    internalRender 함수에 적용

    이제 기존의 internalRender 함수를 수정해보자.

    기존의 함수는 반환값이 없어서 아무것도 할 수 없다. 단지 사이드 이팩트로 DOM만 드리는 작업을 한다. 하지만 이제는 DOM 그리기 + 트리 생성 까지 할 수 있도록 수정해 보자.


    작업 로그

    • internalRender가 반환하는 RenderVNode의 인터페이스를 정의한다.
    interface RenderVNode extends VNode{
       _renderedChildVNode?:VNode,
       _renderedChildren?:VNode[],
       domRef?:HTMLElement
    }
    • 함수형 컴포넌트일 경우 해소하는 과정. → 해당 컴포넌트를 호출해 해소한다.
    const resolvedComponent: VNode = vnode.type(vnode.props); //컴포넌트 해소
        internalRender(resolvedComponent, parent);
  • 풀어진 VNode를 internalRender 함수에 넣는다. 이때 internalRender 함수가 반환하는 RenderedVNode를 상위노드(= RenderedVNode)의 _renderedChildVNode 필드가 참조하도록 한다.
    • 이렇게 해야 tree의 연결 구조를 만들 수 있다.
// 해소된 Vnode를 internalRender 함수로 재귀한 값인 RenderedVNode를 상위노드와 연결해야 트리 구조가 만들어 진다.
    renderedVNode._renderedChildVNode = internalRender(
      resolvedComponent,
      parent
    );
  • 함수형 컴포넌트가 아닐경우 해당 로직이 실행 된다.
  • 각 노드를 처리할때마다 상위 renderedVNode에 계속해서 연결을 해줘야 한다!
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 비교를 하고 결과에 따라 노드가 만들어 진다.

Diff 알고리즘을 위한 상태 비교, 이전 알고리즘

  • 리액트팀의 휴리스틱 알고리즘
    • 기존의 노드와 리렌더링으로 만들어지는 새로운 노드를 비교할때는 다음과 같은 기준으로 비교한다.
      1. 서로 다른 타입의 두 엘리먼트는 서로 다른 트리를 만들어낸다.

      2. 개발자는 key prop을 통해, 여러 렌더링 사이에서 어떤 자식 엘리먼트가 변경되지 않아야 할지 표시할 수 있다. → 최적화의 핵심!

        하지만 key를 사용하지 않아도 버그는 발생하지 않는다. 하지만 개발자의 의도대로 동작하지 않는 위험이 존재한다.

diff 과정

만약 개발자가 key prop을 사용하지 않고 개발을 하는 상황을 전개 해본다. 이때 내가 만든 프레임워크는 어떻게 노드끼리 비교를 해 상태를 이전할 수 있을까?

위와 같은 노드 트리의 List노드에서 리렌더링이 발생한다.

그러면 자식의 Item컴포넌트 노드들이 생성된다. 이때 기존의 노드 인스턴스와 동일한 노드가 생성되지 않는다. List 컴포넌트가 싱행되면서 새로운 자식 노드 인스턴스들이 생성된다. 하지만 이대로 만들어진 노드들로 이루어진 tree를 이용해 DOM을 그리면 안된다. 이렇게 되면 기존의 상태들은 동기화 되지 못하고 초기화된 상태를 갖고 있기 때문에 업데이트된 최신 상태를 이용 할 수 없다.(위의 사진처럼 초기화된 state:0을 갖고 있다.)

✅ 동기화 작업 등장!

그래서 이때 동기화 작업을 생각하게 된다.

  "각 노드들을 비교하면서 해당 노드 인스턴스가 리렌더링 전의 인스턴스와 동일한 노드일까?"
    ? "상태 필드 동기화 시작!"
    : "새롭게 생성된 노드 인스턴스 그래도 사용."
  • 각 노드 단계에서 리렌더링 전의 노드와 리렌더링 후의 노드를 비교해본다.

    • 일단 key의 유무부터 확인한다. 있으면 동일한 key를 가진 노드 끼리 비교한다. (이때 휴리스틱 알고리즘사용)
    • key가 없으면 해당하는 index끼리 비교를 진행 한다.
  • 만약 동일하지 않으면 리렌더링으로 새롭게 생성된 노드를 교체한다.

    • 이때 교체가 진행된 노드는 표식을 남겨야 한다.
  • 만약 동일한 노드(type과 index가 동일)하면 리렌더링 전의 상태 필드를 새로운 노드에 주입한다. 그리고 리렌더링 된 노드를 기존 노드 트리에 교체한다. → 잘못된 생각!

  • 만약 동일한 노드(type과 index가 동일)하면 리렌더링 전의 VNode를 재사용 한다. 그리고 새롭게 생기는 VNode의 Props필드는 최신값이므로 덮어 쓴다. → 이렇게 되면 업데이트된 상태를 계속해서 사용할 수 있다.

    • 이때도 마찬가지로 교체가 완료됐다는 표식을 남긴다.

→ 위의 과정을 추상화 하면 다음 사진과 같다.

  • 하지만 이 과정에는 이상한 흐름이 있다. 일단 전체 흐름을 다시 보자.

사실 개발자가 원하는 결과는 위와 같지 않다. 파란색Item노드와 빨강색 Item노드의 순서가 바뀌면서 상태 또한 빨강색 노드의 상태인 state:1 은 그대로 리렌더링 후 인덱스 1인 노드(빨강색 노드) 에 바인딩 되고, 파랑색 노드의 상태인 state:2 는 리렌더링 후 인덱스 0 인 노드(파랑색 노드) 에 바인딩이 된는게 개발자가 원하는 결과이다.

하지만 key가 없기 때문에 index를 기준으로 비교를 하는데 이때 type이 동일해 동일한 인스턴스 노드라고 판단하여 엉뚱한 VNode를 재사용 하게된다. 그래서 실제로 리액트 팀에서는 key의 사용을 강력하고 권하고 있다.

(사실 공식문서로 key 가 중요하다~ 이런 말 계속 봤는데 실제로 이렇게 뜯어서 확인하니까 엄청 중요한 수준이 아니라 무조건 써야한다…)

재조정 과정 디테일 설계

✅ 렌더 단계와 Diffing의 동시적 진행

1. 리렌더링 시작 및 컴포넌트 함수 호출:

  • setState 등으로 리렌더링이 트리거되면, 변경이 발생한 컴포넌트(예: List)의 렌더링 함수(vnode.type(vnode.props) 호출)가 다시 실행된다.
  • 이 함수 호출 결과로 새로운 VNode 객체(currentVNode)가 반환된다. 이 VNode 객체는 이전 VNode 객체와는 다른 독립적인 JavaScript 객체임.

2. 노드 단위의 즉각적인 Diffing 및 인스턴스 관리 (렌더 단계 안에서):

  • internalRender 함수가 이 새로운 VNode를 받아서 처리할 때, 단순히 DOM을 만들거나 트리를 구축하는 것을 넘어, 이 새로운 VNode를 이전 렌더링의 해당 위치에 있던 RenderedVNode (oldVNode)와 즉각적으로 비교(diff)한다.

✅ diff 비교 과정에서는root의 VNode가 비교 되는게 아니라 root intenralRender 함수의 반환값인 RenderedVNode 를 비교한다는걸 계속 인지 해야 한다. → (RenderedVNode 로 이루어진 트리를 순회하면서 비교 하는거니까…)

→ root의 RenderedVNode와 새로운 VNode 비교

  • 이 비교 과정이 매우 중요하다.
    • type 비교: oldVNodecurrentVNodetype이 같은지 확인한다.
    • key 비교 (리스트인 경우): type이 같고 리스트 안에 있다면 key를 비교한다.
    • 인스턴스 재사용 또는 생성:
      • 일단 type 이 있는지 먼저 확인하고, 없으면 index를 기준으로 비교 대상을 정한다. 이때 typekey (또는 인덱스)가 일치하여 동일한 논리적 컴포넌트라고 판단되면, 해당 컴포넌트의 기존 인스턴스(상태를 가진)를 재사용한다. 즉, 이전에 만들어져 상태를 가지고 있던 그 컴포넌트 인스턴스를 버리지 않고 그대로 둔다. internalRender.ts에서 pushCurrentVNodepopCurrentVNode를 사용하는 hookManager 부분이 바로 이 컴포넌트 인스턴스와 훅 상태를 연결하는 로직과 매우 중요한 연관이 있다. → 위의 함수들이 useState가 지금 어떤 노드에서 실행되는지 알 수 있게 해준다.
      • 이 재사용된 인스턴스에 새로운 VNode의 props를 전달하여 컴포넌트 함수를 다시 실행한다. 이때 useState는 이 인스턴스에 연결된 기존 상태 값을 반환하므로, 상태가 유지된다. → 기존 상태 값을 반환 할 수 있는 이유는 전적으로 hookManager 덕분…

2-1. 중요한점!

나는 2번 과정에서 계속 기존의 상태를 주입 한다고 생각했는데 그게 아니다! 일단 사진을 전체 재조정 과정은 아래의 사진과 같다.

  • diff 비교를 해서 만약에 같으면 root의 Tree의 RenderedVNode(VNode)의 Props필드를 업데이트 한다.
  • 그리고 해당 함수(컴포넌트)를 업데이트된 최신 props을 넣어 호출한다.
  • 호출된 자식 노드들도 위의 과정을 반복해서 실행.

3. 자식 노드로 재귀적 진행:

  • 현재 노드에 대한 인스턴스 결정 및 상태 처리 후, 그 노드의 자식 VNode들에 대해서도 위 2번의 과정을 재귀적으로 반복한다.

핵심!

  • diff 를 렌더 단계와 분리해서 생각하면 안된다. 렌더 단계에서 새로운 VNode 트리를 구축하는 순간에 노드 단위에서 각 VNode를 이전 트리의 해당 노드와 diff (즉시 비교) 한다.
  • 결국 기존에 최초에 생성된 root트리를 계속해서 수정한다.
  • 이때 실제 DOM 조작은 발생하지 않는다: 해당 과정에서는 실제 브라우저 DOM 을 직접 조작 하면 안된다. 변경된 사항을 ‘기록’ 하거나, RenderedVNode 트리 내부적으로 domRef를 통한 연결 상태를 업데이트할 뿐입니다. → 실제 DOM을 조작하는 행위는 위의 모든 작업이 끝난 뒤에, 변경사항이 발생한 표식을 추적해서 변경된 부분만 DOM을 수정해 최소한으로 DOM에 접근해야 한다.
profile
앵맹!

0개의 댓글