티스토리 뷰

Rodrigo Pombo의 Build your own React 블로그 글을 참고해서 리액트의 기본 개념, 기능들을 직접 따라 만들어 보면서, 신경쓰지 못한 부분을 알고 가고자 한다.

알아가는 과정 속에서 원글을 응용하여 직접 돌려보며 확인해볼 수 있는 실습환경을 구성 및 번역 내용이 들어간 프로젝트를 만들었다. learn-react-by-building-ko

이전 글에서는 createElement, render 함수를 만들어 보며 원리와 동작을 이해해보고 성능 최적화를 위한 Concurrent Mode에 대해서 알아보았습니다.

오늘은 리액트의 Fiber 개념과 Render 함수 그리고 Commit 단계, Reconciliation 재조정에 대해서 알아보겠습니다.

똑같이 원글을 따라 가며, Step4~Step6 까지의 내용을 담고 있으며 각 스텝마다 실습을 구성했습니다.

Step IV: Fibers

작업 단위(unit of work)를 조직화하려면 자료 구조가 필요합니다: fiber tree 입니다.

각 엘리먼트마다 하나의 fiber를 갖게 되고, 각 fiber는 하나의 작업 단위가 됩니다. 예시로 보여드리겠습니다.

다음과 같은 엘리먼트 트리를 렌더링하고 싶다고 가정해봅시다:

jsxDidact.render(
    <div>
        <h1>
            <p />
            <a />
        </h1>
        <h2 />
    </div>,
    container,
);

render 함수에서 우리는 루트 fiber를 만들고 그것을 nextUnitOfWork로 설정할 겁니다. 나머지 작업은 performUnitOfWork 함수에서 일어나는데, 거기서 각 fiber마다 세 가지를 하게 됩니다:

  1. 엘리먼트를 DOM에 추가한다 (현재 fiber를 DOM에 추가 (윗부분))
  2. 엘리먼트의 자식들을 위한 fiber를 만든다. (현재 fiber의 자식들을 위한 새 fiber들을 만들어 트리에 연결 (가운데))
  3. 다음 작업 단위를 선택한다. (다음 작업할 fiber를 반환 (아랫부분))

이 자료 구조의 목표 중 하나는 다음 작업 단위를 쉽게 찾는 것입니다. 그래서 각 fiber는 자신의 첫 번째 자식(first child), 다음 형제(next sibling), 그리고 부모(parent)로의 링크를 가지고 있습니다.

어떤 fiber에 대한 작업을 끝냈을 때, 자식이 있다면 그 자식 fiber가 다음 작업 단위가 됩니다. 예시에서 div fiber에 대한 작업이 끝나면, 다음 작업 단위는 h1 fiber가 됩니다. fiber에 자식이 없다면, 형제(sibling)를 다음 작업 단위로 삼습니다.

예를 들어, p fiber는 자식이 없으므로 작업을 끝낸 뒤 a fiber로 이동합니다. 자식도 형제도 없다면 "삼촌(uncle)" — 즉 부모의 형제 — 로 갑니다. 예시에서의 a와 h2 fiber 관계처럼요. 또한 부모가 형제를 가지고 있지 않다면, 형제를 가진 부모를 만나거나 루트에 도달할 때까지 부모들을 따라 계속 올라갑니다. 루트에 도달했다면, 이번 렌더링의 모든 작업을 끝냈다는 의미입니다. 이제 코드로 옮겨봅시다.

먼저, render 함수에서 이 코드를 제거합시다.

function createDom(fiber) {
  const dom =
    fiber.type == "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(fiber.type)
​
  const isProperty = key => key !== "children"
  Object.keys(fiber.props)
    .filter(isProperty)
    .forEach(name => {
      dom[name] = fiber.props[name]
    })
​
  return dom
}
​
function render(element, container) {
  // TODO set next unit of work
}
​
let nextUnitOfWork = null

DOM 노드를 생성하는 부분은 별도의 함수로 따로 떼어둡니다. 나중에 다시 사용할 거예요. render 함수에서는 nextUnitOfWork를 fiber 트리의 루트로 설정합니다.

function render(element, container) {
  nextUnitOfWork = {
    dom: container,
    props: {
      children: [element],
    },
  }
}
​
let nextUnitOfWork = null

그러면 브라우저가 준비됐을 때 우리의 workLoop를 호출하게 되고, 우리는 루트부터 작업을 시작하게 됩니다.

function workLoop(deadline) { // 재귀적으로 브라우저가 준비되면, 실행되는 구조
  let shouldYield = false
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork( // 작업을 반복적으로 진행하면서, 다음 작업을 할당
      nextUnitOfWork
    )
    shouldYield = deadline.timeRemaining() < 1 // 작업 반복 중에 브라우저에게 양보해야할 타이밍이 발생하면 반복을 중단 ()
  }
  requestIdleCallback(workLoop) // 브라우저가 작업을 진행하고, 진행할 작업을 다시 예약
}
​
requestIdleCallback(workLoop)

먼저, 새 노드를 만들고 DOM에 붙입니다. DOM 노드는 fiber.dom 속성에 추적해 둡니다. 그다음 각 자식마다 새로운 fiber를 만듭니다.

그리고 그것이 첫 번째 자식인지 아닌지에 따라 자식(child) 으로 설정하거나 형제(sibling) 로 설정해서 fiber 트리에 추가합니다. 마지막으로 다음 작업 단위를 찾습니다. 먼저 자식을, 그다음 형제를, 그다음 삼촌을, 이런 식으로 시도합니다. 이것이 우리의 performUnitOfWork입니다.

function performUnitOfWork(fiber) {
  if (!fiber.dom) {
    // 본인의 DOM을 만든다.
    fiber.dom = createDom(fiber)
  }
​
  if (fiber.parent) {
    // 부모 DOM에 생성한 본인의 DOM을 추가한다.
    fiber.parent.dom.appendChild(fiber.dom)
  }
​
  // 자식 및 형제를 구조화 한다.
  const elements = fiber.props.children
  let index = 0
  let prevSibling = null
​
  // 자식을 순회
  while (index < elements.length) {
    const element = elements[index]
​
    // 자식 Fiber를 생성
    const newFiber = {
      type: element.type,
      props: element.props,
      parent: fiber,
      dom: null,
    }

    if (index === 0) {
      // 본인 Fiber에 자식 Fiber를 연결한다,
      fiber.child = newFiber
    } else {
      // 자식이 없다면, 본인을 이전 형제 Fiber로 연결한다.
      prevSibling.sibling = newFiber
    }
​
    // 다음 Fiber와 이전 Fiber와의 형제 관계를 규정하기 위해서 일단 본인을 할당해 둔다.
    prevSibling = newFiber
    index++
  }

  // 현재 Depth의 자식과 형제를 모두 fiber로 만들고 관계를 만들었으면

  if (fiber.child) {
    // 자식이 있다면 다음 작업으로는 자식 fiber를 반환
    return fiber.child
  }

  let nextFiber = fiber

  while (nextFiber) {
    if (nextFiber.sibling) {
      // 형제가 있다면 다음 작업으로는 형제 fiber를 반환
      return nextFiber.sibling
    }

    // 형제가 없다면 부모 Fiber를 다음 작업으로 한다.

    nextFiber = nextFiber.parent
  }
}

핵심 동작

"fiber 하나를 DOM에 붙이고, 자식 fiber들을 만들어 child/sibling 링크로 트리에 엮은 다음, 자식 → 형제 → 삼촌 순서로 다음에 작업할 fiber를 찾아 반환한다."

단계 현재 fiber 무슨 일이 일어나나 다음 fiber
1 root div fiber 생성, root.child = div div (자식)
2 div DOM 생성/append, h1·h2 fiber 생성 h1 (자식)
3 h1 DOM 생성/append, p·a fiber 생성 p (자식)
4 p DOM 생성/append, 자식 없음 a (p의 sibling)
5 a DOM 생성/append, 자식 없음, sibling 없음 → 부모(h1)로 → h1.sibling = h2 h2 (h1의 sibling)
6 h2 DOM 생성/append, 자식 없음, sibling 없음 → 부모(div)로 → div.sibling 없음 → 부모(root)로 → root.sibling 없음 → undefined

위 형태로 진행되는 것은 DFS(깊이 우선 탐색)인데, 재귀 대신 fiber의 child/sibling/parent 링크를 이용해 반복적으로 구현한 것이다. 재귀의 경우, 콜 스택이 있고 중간에 브라우저 제어권을 넘길 수 없기 때문이다.

브라우저에 제어권을 넘기는 이유는, 브라우저의 메인 스레드는 싱글 스레드이기 떄문에 한번에 한가지 일만 가능하다. 따라서 메인 스레드가 어떠한 작업으로 막혀버리면, 브라우저가 진행하는 작업인 사용자 입력 처리, 레이아웃 계산, 페인팅, 애니메이션 프레임, 가비지 컬렉터 등을 진행할 수 없어 멈추게 된다.

Fiber 구조를 통해서 우리는 작업 단위를 분할 할 수 있게 되었고 한번에 진행하는 방식이 아닌 동시성 모드 형식으로 일정 부분을 진행하고 제어권을 넘겨 줄 수 있게 되었다.

VirtualDOM이 단순히 처음 부터 끝까지 한번에 표현하는 설계도라면, Fiber는 VirtualDOM과 같이 설계도 이자 실제 DOM을 어떤 일정(스케줄링)으로 만들지에 관한 내용도 담겨있다고 이해하면 될 것 같다.

현대 React를 생각해보면 이러한 구조가 기여한 것이 있을 것이라고 생각된다.

VirtualDOM 구조로 인해서 브라우저의 DOM API가 없어도, 그것을 위한 요소를 추상화하여 만들어 낼 수 있어 SSR로 이끄는데 기여했을 것이라고 본다.
또한 Fiber 덕분에 서버에서도 일부 작업에 대한 기준, 스케줄링이 가능해졌다고 본다 따라서 SSR Streaming 그리고 RSC (React Server Component) 기술이 탄생할 수 있었다고 생각한다.

실습

step4-1을 실행해서, index.js의 내용을 트랜스파일 후 step4-2를 실행해서 브라우저 내용을 확인해보세요.


Step V: Render와 Commit 단계

여기서 또 다른 문제가 있습니다. 우리는 엘리먼트 하나를 작업할 때마다 새 노드를 DOM에 추가하고 있습니다. 그리고 기억해두세요, 브라우저는 우리가 트리 전체를 다 렌더링하기 전에 우리 작업을 중단시킬 수 있습니다. (deadline 때문에) 그런 일이 일어나면, 사용자는 미완성된 UI를 보게 됩니다. 우리는 그걸 원하지 않아요.

그래서 DOM을 변경(mutate)하는 부분을 여기서 제거해야 합니다. dom을 생성한다고 해서, 화면에 보이지는 않고 부모와 연결되어야 보이기 시작하기 때문에 DOM의 관계를 연결하는 부분을 제거하여 Fiber가 만들어지기 전까지는 사용자에게 들어나는 것이 없음

function performUnitOfWork(fiber) {
  // 유지 — DOM 노드는 만들고 fiber.dom에 저장
  if (!fiber.dom) {
    fiber.dom = createDom(fiber)
  }

  // 제거 — DOM 트리에 붙이는 것은 commit 단계로 미룸
  // if (fiber.parent) {
  //   fiber.parent.dom.appendChild(fiber.dom)
  // }

  // 이하 children fiber 생성 + 다음 작업 단위 찾기는 그대로
  ...
}

대신, fiber 트리의 루트를 따로 추적할 겁니다. 이것을 작업 진행 중인 루트(work in progress root), 줄여서 wipRoot라고 부르겠습니다. 그리고 모든 작업이 끝나면 (작업할 다음 단위가 없다는 것으로 알 수 있죠), fiber 트리 전체를 한 번에 DOM에 커밋합니다. 이 일은 commitRoot 함수에서 수행합니다. 거기서 모든 노드를 재귀적으로 dom에 append합니다.

function render(element, container) {
  wipRoot = {
    dom: container,
    props: {
      children: [element],
    },
  }
  nextUnitOfWork = wipRoot
}
​
let nextUnitOfWork = null
let wipRoot = null // 작업이 완료된 fiber 트리 전체를 담고 있음
function workLoop(deadline) {
  let shouldYield = false
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(
      nextUnitOfWork
    )
    shouldYield = deadline.timeRemaining() < 1
  }
​
  if (!nextUnitOfWork && wipRoot) {
    commitRoot() // commitRoot 과정을 통해서 실제 이전에 만들어둔 메모리 DOM의 관계를 직접 연결하여 사용자에게 보여주는 단계가 진행된다.
    // 이때 브라우저에 의해서 끊어지지 않도록 해당 작업은 양보 없이 원자적으로 만든다.
  }
​
  requestIdleCallback(workLoop)
}

function commitRoot() {
  commitWork(wipRoot.child)
  wipRoot = null
}
​
function commitWork(fiber) {
  if (!fiber) {
    return
  }
  const domParent = fiber.parent.dom
  domParent.appendChild(fiber.dom)
  commitWork(fiber.child)
  commitWork(fiber.sibling)
}

Step VI: Reconciliation (재조정)

지금까지는 DOM에 요소를 추가하기만 했는데, 노드를 업데이트하거나 삭제하는 건 어떻게 할까요?

이제 그걸 해볼 차례입니다. render 함수에서 받은 요소들을 마지막으로 DOM에 커밋한 fiber 트리와 비교해야 합니다.

  1. 이전 트리 보관하기 따라서 커밋이 끝난 후 "마지막으로 DOM에 커밋한 fiber 트리"에 대한 참조를 저장해야 합니다. 이를 currentRoot라고 부릅니다. 또한 모든 fiber에 alternate 속성을 추가합니다. 이 속성은 이전 fiber, 즉 이전 커밋 단계에서 DOM에 커밋했던 fiber로 연결되는 링크입니다.
function commitRoot() {
  commitWork(wipRoot.child)
  currentRoot = wipRoot // 파이버 트리 저장
  wipRoot = null
}
​
function commitWork(fiber) {
  if (!fiber) {
    return
  }
  const domParent = fiber.parent.dom
  domParent.appendChild(fiber.dom)
  commitWork(fiber.child)
  commitWork(fiber.sibling)
}
​
function render(element, container) {
  wipRoot = {
    dom: container,
    props: {
      children: [element],
    },
    alternate: currentRoot, // 이전 파이버 트리 저장
  }
  nextUnitOfWork = wipRoot
}
​
let nextUnitOfWork = null
let currentRoot = null
let wipRoot = null
  1. reconcileChildren 함수 추출 이제 performUnitOfWork에서 새로운 fiber를 만드는 코드를 추출해서, 새로운 reconcileChildren 함수로 옮겨봅시다. 여기서 우리는 이전 fiber들과 새로운 요소들을 재조정(reconcile)할 것입니다.
function performUnitOfWork(fiber) {
  if (!fiber.dom) {
    fiber.dom = createDom(fiber)
  }

  // fiber 생성하던 부분을 reconcileChildren으로
  const elements = fiber.props.children
  reconcileChildren(fiber, elements)
​
  if (fiber.child) {
    return fiber.child
  }
  let nextFiber = fiber
  while (nextFiber) {
    if (nextFiber.sibling) {
      return nextFiber.sibling
    }
    nextFiber = nextFiber.parent
  }
}

function reconcileChildren(wipFiber, elements) {
  let index = 0
  let prevSibling = null
​
  while (index < elements.length) {
    const element = elements[index]
​
    const newFiber = {
      type: element.type,
      props: element.props,
      parent: wipFiber,
      dom: null,
    }
​
    if (index === 0) {
      wipFiber.child = newFiber
    } else {
      prevSibling.sibling = newFiber
    }
​
    prevSibling = newFiber
    index++
  }
}
​

이때 이전 파이버의 자식들(wipFiber.alternate)과 우리가 재조정하려는 요소 배열을 동시에 반복문(iterate)으로 돌립니다.

이전 fiber의 자식들(wipFiber.alternate)과 우리가 재조정하고 싶은 요소 배열을 동시에 순회합니다. 배열과 연결 리스트를 동시에 순회하는 데 필요한 모든 부수적인 코드를 무시하면, 이 while 루프 안에서 가장 중요한 것은 oldFiber와 element만 남습니다. element는 DOM에 렌더링하고 싶은 것이고, oldFiber는 지난번에 렌더링했던 것입니다.

function reconcileChildren(wipFiber, elements) {
 let index = 0
 let oldFiber =
   wipFiber.alternate && wipFiber.alternate.child
 let prevSibling = null
​
 while (index < elements.length || oldFiber != null) {
   const element = elements[index]
​
  let newFiber = null

   // TODO compare oldFiber to element

   if (oldFiber) {
      oldFiber = oldFiber.sibling
    }
​
   if (index === 0) {
     wipFiber.child = newFiber
   } else {
     prevSibling.sibling = newFiber
   }
​
   prevSibling = newFiber
   index++
 }
}
  1. 비교 로직 (Diffing Algorithm)

DOM에 어떤 변경 사항을 적용해야 하는지 확인하기 위해 둘을 비교해야 합니다. 비교에는 type을 사용합니다:

  • 이전 fiber와 새 element의 type이 같으면, DOM 노드를 그대로 유지하고 새 props로만 업데이트할 수 있습니다
  • type이 다르고 새 element가 있다면, 새 DOM 노드를 만들어야 한다는 뜻입니다
  • type이 다르고 이전 fiber가 있다면, 이전 노드를 제거해야 합니다

여기서 React는 키(keys)도 사용하는데, 이를 통해 더 나은 재조정이 가능합니다. 예를 들어, element 배열에서 자식들의 위치가 바뀌었을 때 이를 감지할 수 있습니다.

function reconcileChildren(wipFiber, elements) {
 let index = 0
 let oldFiber =
   wipFiber.alternate && wipFiber.alternate.child
 let prevSibling = null
​
 while (index < elements.length || oldFiber != null) {
  const element = elements[index]
​
  let newFiber = null

   // TODO compare oldFiber to element
  const sameType =
        oldFiber &&
        element &&
        element.type == oldFiber.type
​
  if (sameType) {
    // TODO update the node
  }
  if (element && !sameType) {
    // TODO add this node
  }
  if (oldFiber && !sameType) {
    // TODO delete the oldFiber's node
  }


  if (oldFiber) {
    oldFiber = oldFiber.sibling
  }
​
  if (index === 0) {
    wipFiber.child = newFiber
  } else {
    prevSibling.sibling = newFiber
  }
​
  prevSibling = newFiber
  index++
 }
}
  1. 이펙트 태그(effectTag) 활용

이전 fiber와 element의 type이 같을 때는, 이전 fiber의 DOM 노드와 element의 props를 유지한 채로 새 fiber를 만듭니다.

또한 fiber에 새로운 속성인 effectTag를 추가합니다. 이 속성은 나중에 커밋 단계에서 사용할 것입니다.

그리고 element가 새 DOM 노드를 필요로 하는 경우에는 새 fiber에 PLACEMENT effect 태그를 붙입니다.

노드를 삭제해야 하는 경우에는 새 fiber가 없으므로 이전 fiber에 effect 태그를 추가합니다. 하지만 fiber 트리를 DOM에 커밋할 때는 작업 중인 루트(work in progress root)에서부터 진행하는데, 이 루트에는 이전 fiber들이 없습니다.

그래서 제거하려는 노드들을 추적하기 위한 배열이 필요합니다. 그런 다음 변경 사항을 DOM에 커밋할 때 그 배열에 있는 fiber들도 함께 사용합니다.

function render(element, container) {
 wipRoot = {
    dom: container,
    props: {
      children: [element],
    },
    alternate: currentRoot,
  }
  deletions = []
  nextUnitOfWork = wipRoot
}
​

let deletions = null // 노드 제거용 배열

function reconcileChildren(wipFiber, elements) {
 let index = 0
 let oldFiber =
   wipFiber.alternate && wipFiber.alternate.child
 let prevSibling = null
​
 while (index < elements.length || oldFiber != null) {
  const element = elements[index]
​
  let newFiber = null

   // TODO compare oldFiber to element
  const sameType =
        oldFiber &&
        element &&
        element.type == oldFiber.type
​
  if (sameType) {
    // 이전 fiber와 element의 type이 같을 때는, 이전 fiber의 DOM 노드와 element의 props를 유지한 채로 새 fiber를 만듭니다.
    newFiber = {
      type: oldFiber.type,
      props: element.props,
      dom: oldFiber.dom,
      parent: wipFiber,
      alternate: oldFiber,
      effectTag: "UPDATE",
    }
  }

  if (element && !sameType) {
    // element가 새 DOM 노드를 필요로 하는 경우에는 새 fiber에 PLACEMENT effect 태그를 붙입니다.
    newFiber = {
      type: element.type,
      props: element.props,
      dom: null,
      parent: wipFiber,
      alternate: null,
      effectTag: "PLACEMENT",
    }
  }

  if (oldFiber && !sameType) {
    // 노드를 삭제해야 하는 경우에는 새 fiber가 없으므로 이전 fiber에 effect 태그를 추가합니다.
    oldFiber.effectTag = "DELETION"
    deletions.push(oldFiber)
  }


  if (oldFiber) {
    oldFiber = oldFiber.sibling
  }
​
  if (index === 0) {
    wipFiber.child = newFiber
  } else {
    prevSibling.sibling = newFiber
  }
​
  prevSibling = newFiber
  index++
 }
}

function commitRoot() {
  deletions.forEach(commitWork)
  commitWork(wipRoot.child)
  currentRoot = wipRoot
  wipRoot = null
}
  1. 커밋 단계의 변화

이제 commitWork 함수가 새로운 effectTag들을 처리하도록 수정해봅시다. fiber에 PLACEMENT effect 태그가 있으면 이전과 동일하게 부모 fiber의 노드에 DOM 노드를 추가합니다. DELETION이라면 반대로 자식을 제거합니다. 그리고 UPDATE라면 기존 DOM 노드를 변경된 props로 업데이트해야 합니다. 이 작업은 updateDom 함수에서 수행할 것입니다.

function commitWork(fiber) {
    if (!fiber) {
        return;
    }
    const domParent = fiber.parent.dom;

    if (fiber.effectTag === 'PLACEMENT' && fiber.dom != null) {
        // fiber에 `PLACEMENT` effect 태그가 있으면 이전과 동일하게 부모 fiber의 노드에 DOM 노드를 추가합니다.
        domParent.appendChild(fiber.dom);
    } else if (fiber.effectTag === 'UPDATE' && fiber.dom != null) {
        // `UPDATE`라면 기존 DOM 노드를 변경된 props로 업데이트해야 합니다.
        updateDom(fiber.dom, fiber.alternate.props, fiber.props);
    } else if (fiber.effectTag === 'DELETION') {
        // `DELETION`이라면 반대로 자식을 제거합니다.
        domParent.removeChild(fiber.dom);
        return;
    }

    commitWork(fiber.child);
    commitWork(fiber.sibling);
}
  1. updateDom 함수

이전 fiber의 props와 새 fiber의 props를 비교해서, 사라진 props는 제거하고 새로 생기거나 변경된 props는 설정합니다.

업데이트해야 할 특별한 종류의 prop 중 하나가 이벤트 리스너입니다. 그래서 prop 이름이 "on" 접두사로 시작하면 다르게 처리할 것입니다.

이벤트 핸들러가 변경되었다면 노드에서 제거합니다. 그런 다음 새 핸들러를 추가합니다.

const isEvent = key => key.startsWith("on")
const isProperty = key =>
  key !== "children" && !isEvent(key)
const isNew = (prev, next) => key =>
  prev[key] !== next[key]
const isGone = (prev, next) => key => !(key in next)

function updateDom(dom, prevProps, nextProps) {
   //Remove old or changed event listeners
  Object.keys(prevProps)
    .filter(isEvent) // 이벤트만 대상
    .filter(
      key =>
        !(key in nextProps) ||
        isNew(prevProps, nextProps)(key) // 다음 Props에 없거나, 새로운 값인 경우에만
    )
    .forEach(name => {
      const eventType = name
        .toLowerCase()
        .substring(2)
      dom.removeEventListener( // 이벤트 제거
        eventType,
        prevProps[name]
      )
    })

   // Remove old properties
  Object.keys(prevProps)
    .filter(isProperty) // children, 이벤트 제외
    .filter(isGone(prevProps, nextProps)) // 다음 Props에 없는 key만 대상
    .forEach(name => {
      dom[name] = "" // 제거
    })
​
  // Set new or changed properties
  Object.keys(nextProps)
    .filter(isProperty) // children, 이벤트 제외
    .filter(isNew(prevProps, nextProps)) // 이전과 다음키의 값이 다른 것
    .forEach(name => {
      dom[name] = nextProps[name] // 추가 또는 변경
    })

  // Add event listeners
  Object.keys(nextProps)
    .filter(isEvent) // 이벤트이고
    .filter(isNew(prevProps, nextProps)) // 새로운 값인 경우
    .forEach(name => {
      const eventType = name
        .toLowerCase()
        .substring(2)
      dom.addEventListener( // 이벤트 등록
        eventType,
        nextProps[name]
      )
    })
}
  1. 실습 pnpm step6-1 실행하여 트랜스파일 후 pnpm step6-2 로 예제를 확인해보세요. 예제는 직접 상태를 변경하며 업데이트되고 사라지는 것을 확인하기 위한 타이머, 카운터 예제를 만들어보았습니다.

마무리

이번 시간을 통해서 우리는 Fiber를 만들어보면서 우리가 진행하는 작업 단위를 규정하고 나누었으며. Fiber의 내용을 Render 함수와 commit 단계를 통해서 효과적으로 진행하고 중단하여 메인스레드의 제어권을 스케줄링 할 수 있게 되었습니다. 또한 노드의 업데이트와 삭제 등을 위해서 altenate로 저장한 이전 Fiber 스냅샷과 비교하여 Reconciliation (재조정) 단계를 만들어 변경 사항을 작업 효율적으로 만들어 보았습니다.

이전에는 devtools의 performance 로그에 onCommitFiberRoot 등의 내용이 표시되었던 것이 위 내용들을 통해서 어떤 의미를 가지고 있는지 알 수 있기도 했습니다.

확실히 기술의 원리와 개념 등을 이해하는 것이 실제로 성능을 개선하거나, 다음 기술 등에 어떻게 영향을 주는 지를 자연스럽게 익히고 기억하는데 좋다고 느낀 순간이였습니다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/06   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함