2024년 1월 정보들

Table of Contents

1 [250107] build-your-own-react

링크 : https://pomb.us/build-your-own-react/

예전에 보았던 블로그를 다시한번 읽어보게되었다. 리액트를 실무에서 사용한 적 없지만, 아주 좋은 글임을 확실하다. 프론트엔드 시장에서 한 획을 그은 리액트의 코어로직에 대해서 많이 알게 되었다.

1.1 JSX -> createElement

JSX는 Babel 을 이용하여 Javascript로 변환된다. 이것으로 가상DOM 객체를 생성할 수 있다.

// JSX 코드
/** @jsx Didact.createElement */
<div className="container">
  <h1>Hello</h1>
</div>

// Babel이 변환한 JavaScript 코드
Didact.createElement(
  "div",
  { className: "container" },
  Didact.createElement("h1", null, "Hello")
)

createElement 는 이런 형태다

function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.map(child =>
	typeof child === "object"
	  ? child
	  : createTextElement(child)
      ),
    },
  }
}

function createTextElement(text) {
  return {
    type: "TEXT_ELEMENT",
    props: {
      nodeValue: text,
      children: [],
    },
  }
}

1.2 fiber 재조정

리액트는 Fiber라는 자료구조를 사용하여 가상DOM을 관리한다.

{
  type: string | function,  // DOM 엘리먼트 타입 또는 함수형 컴포넌트
  dom: DOM Node,           // 실제 DOM 노드
  props: Object,          // 속성들
  parent: Fiber,         // 부모 Fiber
  child: Fiber,         // 첫번째 자식 Fiber
  sibling: Fiber,      // 형제 Fiber
  alternate: Fiber,   // 이전 트리의 Fiber
  effectTag: string, // "PLACEMENT", "UPDATE", "DELETION" 중 하나
}

작업단위로 나누어서 처리하며, 이를 통해 브라우저가 렌더링을 중단하지 않고도 작업을 수행할 수 있다.

function workLoop(deadline) {
  let shouldYield = false
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
    shouldYield = deadline.timeRemaining() < 1
  }
  if (!nextUnitOfWork && wipRoot) {
    commitRoot()
  }
  requestIdleCallback(workLoop)
}

작업 처리는

  1. Render Phase
    • Fiber 트리를 생성한다.
    • DOM 엘리먼트를 생성하고, 이전 트리와 비교하여 변경사항을 찾는다.
  2. Commit Phase
    • 변경사항을 한번에 반영한다.

Render Phase 는 performUnitOfWork 함수에서 처리된다.

Commit Phase는 performUnitOfWork 에서 변경한 Fiber 트리를 반영한다.

function commitRoot() {
  deletions.forEach(commitWork)  // 삭제된 Fiber를 커밋한다.
  commitWork(wipRoot.child)  // 첫번째 자식 Fiber를 커밋한다.
  currentRoot = wipRoot
  wipRoot = null
}

function commitWork(fiber) {
  if (!fiber) {
    return
  }

  let domParentFiber = fiber.parent
  while (!domParentFiber.dom) {
    domParentFiber = domParentFiber.parent
  }
  const domParent = domParentFiber.dom

  if (
    fiber.effectTag === "PLACEMENT" &&
    fiber.dom != null
  ) {
    domParent.appendChild(fiber.dom)
  } else if (
    fiber.effectTag === "UPDATE" &&
    fiber.dom != null
  ) {
    updateDom(
      fiber.dom,
      fiber.alternate.props,
      fiber.props
    )
  } else if (fiber.effectTag === "DELETION") {
    commitDeletion(fiber, domParent)
  }

  commitWork(fiber.child)
  commitWork(fiber.sibling)
}

1.3 useState Hook

React의 핵심 기능 중 하나인 상태관리를 위한 Hook입니다. useState의 내부동작은 다음과 같다:

1.3.1 초기화와 상태 관리

  1. 전역 변수로 현재 작업중인 Fiber(wipFiber)와 Hook의 인덱스(hookIndex)를 관리
  2. Hook은 {state, queue} 형태의 객체로 관리되며 Fiber의 hooks 배열에 순서대로 저장됨
  3. 컴포넌트가 다시 렌더링될 때 이전 트리(alternate)에서 동일 인덱스의 Hook을 찾아 상태를 복원

1.3.2 상태 업데이트 알고리즘

  1. setState 호출 시점:
    • 업데이트 함수를 Hook의 queue에 추가
    • 새로운 Fiber 트리 생성을 트리거
    • 다음 작업 단위 설정
  2. 다음 렌더링 시점:
    • 이전 Hook의 queue에 있는 모든 업데이트를 순차적으로 적용
    • 새로운 상태 계산 후 queue 초기화
    • 새로운 상태와 setState 함수 반환
function useState(initial) {
  // 이전 Hook의 상태를 가져온다
  const oldHook =
    wipFiber.alternate &&
    wipFiber.alternate.hooks &&
    wipFiber.alternate.hooks[hookIndex]

  // 새로운 Hook을 생성한다
  const hook = {
    state: oldHook ? oldHook.state : initial,  // 상태값
    queue: [],                                 // 업데이트 큐
  }

  // 3. 이전 큐의 모든 업데이트 적용
  const actions = oldHook ? oldHook.queue : []
  actions.forEach(action => {
  // 함수형 업데이트 지원
  hook.state = typeof action === 'function'
  ? action(hook.state)
  : action
  })

  // setState 함수를 생성한다
  const setState = action => {
    hook.queue.push(action)
    // 새로운 렌더링을 트리거한다 
    wipRoot = {
      dom: currentRoot.dom,
      props: currentRoot.props,
      alternate: currentRoot,
    }
    // 새로운 wipRoot 를 만들고, nextUnitOfWork에 설정하면 렌더링이 시작된다.
    // 알겠지만 nextUnitOfWork은 덮어씌워질 수 있음.
    // 해결방법은 nextUnitOfWorks 로 배열(큐)을 만들어서 순차적으로 처리하면 됨.
    nextUnitOfWork = wipRoot
    deletions = []
  }

  // Hook을 저장하고 반환한다
  wipFiber.hooks.push(hook)
  hookIndex++
  return [hook.state, setState]
}

useState의 주요 특징:

  1. Hook의 상태는 Fiber 트리에 저장됩니다
  2. 업데이트는 큐에 저장되어 배치로 처리됩니다
  3. 상태 변경 시 새로운 렌더링이 트리거됩니다

1.3.3 시간복잡도 분석

  • Hook 찾기: O(1) - 인덱스로 직접 접근
  • 상태 복원: O(1) - 이전 트리에서 찾아서 복사
  • 업데이트 적용: O(n) - 큐에 저장된 모든 업데이트를 순차적으로 적용

1.4 이벤트 처리

React는 이벤트를 Props로 전달받아 처리합니다.

const isEvent = key => key.startsWith("on")
const isProperty = key => key !== "children" && !isEvent(key)

function updateDom(dom, prevProps, nextProps) {
  // 이전 이벤트 리스너 제거
  Object.keys(prevProps)
    .filter(isEvent)
    .filter(key =>
      !(key in nextProps) ||
      isNew(prevProps, nextProps)(key)
    )
    .forEach(name => {
      const eventType = name.toLowerCase().substring(2)
      dom.removeEventListener(eventType, prevProps[name])
    })

  // 새로운 이벤트 리스너 추가
  Object.keys(nextProps)
    .filter(isEvent)
    .filter(isNew(prevProps, nextProps))
    .forEach(name => {
      const eventType = name.toLowerCase().substring(2)
      dom.addEventListener(eventType, nextProps[name])
    })
}

이벤트 처리의 특징:

  1. on으로 시작하는 prop을 이벤트로 인식합니다
  2. 이벤트 리스너를 직접 DOM에 추가/제거합니다
  3. 이벤트 이름은 소문자로 변환됩니다 (onClick -> click)

이렇게 구현된 미니 React는 실제 React의 핵심 개념들을 잘 보여줍니다. Fiber 아키텍처를 통한 점진적인 렌더링, Hook을 이용한 상태관리, 그리고 선언적인 이벤트 처리 등 React의 주요 특징들을 이해하는데 도움이 됩니다.

Author: Younghwan Nam

Created: 2025-01-07 Tue 04:25

Emacs 27.2 (Org mode 9.4.4)

Validate