setState 업데이터 함수에서 prev가 항상 최신값을 가리키는 이유

yugyeongKim·2025년 6월 8일
1

setState 업데이트

setNumber(n => n + 1);
setNumber(n => n + 1);
setNumber(n => n + 1);
  • 결과와 이유 이 결과는 3이 됩니다. React가 이벤트 핸들러를 수행하는 동안 여러 코드를 통해 작동하는 방식은 다음과 같습니다.
    1. setNumber(n => n + 1)n => n + 1 함수를 큐에 추가합니다.

    2. setNumber(n => n + 1)n => n + 1 함수를 큐에 추가합니다.

    3. setNumber(n => n + 1)n => n + 1 함수를 큐에 추가합니다.

      다음 렌더링 중에 useState 를 호출하면 React는 큐를 순회합니다. 이전 number state는 0이었으므로 React는 이를 첫 번째 업데이터 함수에 n 인수로 전달합니다. 그런 다음 React는 이전 업데이터 함수의 반환 값을 가져와서 다음 업데이터 함수에 n으로 전달하는 식으로 반복합니다.

      queued updatenreturns
      n => n + 100 + 1 = 1
      n => n + 111 + 1 = 2
      n => n + 122 + 1 = 3

      React는 3을 최종 결과로 저장하고 useState에서 반환합니다.

      이것이 위 예시 “+3”을 클릭하면 값이 3씩 올바르게 증가하는 이유입니다.

      <button onClick={() => {
        setNumber(number + 5);
        setNumber(n => n + 1);
      }}>
  • 결과와 이유 이 결과는 6이 됩니다.
    1. setNumber(number + 5) : number는 0이므로 setNumber(0 + 5)입니다. React는 큐에 5로 바꾸기” 를 추가합니다.

    2. setNumber(n => n + 1) : n => n + 1는 업데이터 함수입니다. React는 해당 함수를 큐에 추가합니다.

      다음 렌더링하는 동안 React는 state 큐를 순회합니다.

      queued updatenreturns
      ”replace with 50 (unused)5
      n => n + 155 + 1 = 6

      React는 6을 최종 결과로 저장하고 useState에서 반환합니다.

업데이터 함수(n => n + 1 ) 내부의 매개변수는 어떻게 항상 최신 state 값을 가리킬까??

useState 내부 동작 방식

1. Fiber와 훅 노드의 매핑

  • React는 각 함수형 컴포넌트를 렌더링할 때 Fiber 객체를 생성하고, 이를 currentlyRenderingFiber 전역 포인터로 가리킵니다.
function FiberNode(tag, pendingProps, key, mode) {
  // --- 컴포넌트 식별 ---
  this.tag         // 컴포넌트 유형 (예: FunctionComponent, HostComponent)
  this.key         // 리스트 렌더링 시 고유 식별자
  this.elementType // JSX 타입 (예: 'div' 또는 Component 함수)
  this.type        // 실제 컴포넌트 정의

  // --- 렌더링 대상 노드 ---
  this.stateNode   // 클래스 인스턴스나 DOM 노드 레퍼런스

  // --- 트리 구조 포인터 ---
  this.return      // 부모 FiberNode
  this.child       // 첫 번째 자식 FiberNode
  this.sibling     // 다음 형제 FiberNode
  this.index       // 자식 인덱스

  // --- 훅 연결 리스트의 시작점 ---
  this.memoizedState // ▶ HookNode 리스트의 머리(head)를 가리킴

  // --- 클래스형 setState 또는 이펙트 큐(옵션) ---
  this.updateQueue   // 클래스형 컴포넌트의 setState 호출 큐 또는 useEffect 이펙트 큐

  // (이외에도 pendingProps, memoizedProps, flags, effectTag, nextEffect 등 다수의 스케줄링/이펙트 메타데이터를 포함)
}
  • 각 Fiber 객체는 memoizedState라는 속성을 가지고 있는데, 이것은 해당 Fiber에 연결된 훅 노드(hook node)들의 연결 리스트의 첫 번째 요소를 가리킵니다.
  • useState 훅 노드는 현재 상태 값을 저장하는 memoizedState와 상태 업데이트를 관리하는 업데이트 큐(UpdateQueue)를 가집니다.

컴포넌트 코드 예시

import React, { useState } from 'react';

function MyComponent() {
  const [count, setCount] = useState(0); // 1️⃣
  const [name, setName]   = useState('Alice'); // 2️⃣
  
	...
	
  return (
    <div>
      <p>Count: {count}</p>
      <p>Name: {name}</p>
      <button onClick={() => setCount(prev => prev + 1)}>Increase</button>
    </div>
  );
}

HookNode

{
  "memoizedState": 0,           // count의 초기값 0
  "baseState": 0,               // internal 초기값 저장소
  "baseQueue": null,            // 아직 업데이트 없음
  "queue": {
    "pending": null,
    "lanes": 0,
    "dispatch": function,       // setCount 호출 시 사용하는 함수
    "lastRenderedState": 0
  },
  "next": // 2️⃣의 HookNode
}
  • Fiber 객체와 HookNode의 차이
    • Fiber: 컴포넌트 인스턴스 하나에 대응하며, 렌더링 메타데이터를 보관합니다.
    • HookNode: useState, useEffect 등 각 훅별로 상태·큐·다음 훅 링크를 저장하는 객체입니다.

2. 업데이트 큐에 작업 추가 (setState 호출)

컴포넌트 코드 예시

import React, { useState } from 'react';

function MyComponent() {
  const [count, setCount] = useState(0); // 1️⃣
  const [name, setName]   = useState('Alice'); // 2️⃣
  
	...
	
  return (
    <div>
      <p>Count: {count}</p>
      <p>Name: {name}</p>
      <button onClick={() => {
	      setCount(count + 1);
	      setCount(prev => prev + 1);
      }}>Increase</button>
    </div>
  );
}
  • setState(action)를 호출하면 내부적으로 dispatchSetState 함수가 실행됩니다.
// 첫 번째 클릭: dispatchAction 호출
dispatchAction(fiber, hook.queue, count + 1);
// queue.pending에는 update1을 가리키게 된다.
update1 = {
  action: count + 1,         // user-provided 콜백
  next: update1                      // 아직 하나뿐이므로 self-loop
}
hookNode1.queue.pending = update1

// 두 번째 클릭: dispatchAction 호출
dispatchAction(fiber, hookNode1.queue, prev => prev + 1)

// queue.pending에는 update2가 가리키게 된다.
update1 = {
  action: (prev) => prev + 1,         // user-provided 콜백
  next: update1                      // 아직 하나뿐이므로 self-loop
}
hookNode1.queue.pending = update2

// 원형 연결 리스트 구조
update2.next = update1
  • 위코드를 실행하면 내부적으로 다음이 호출됩니다:
  • 이 함수는 상태 변경 요청을 나타내는 새로운 업데이트 노드를 생성합다. 업데이트 노드는 action (setState에 전달된 값 또는 함수)과 같은 정보를 담습니다.
  • 생성된 업데이트 노드는 해당 훅 노드의 업데이트 큐즉시 추가됩니다. 이 업데이트 큐의 queue.pending 속성이 바로 이러한 새로운 업데이트 노드들의 연결 리스트를 가리킵니다.
  • 상태 값은 setState 호출 시 즉시 업데이트되지 않습니다. 업데이트는 우선순위(Lanes)에 따라 나중에 처리될 수 있기 때문에 큐에 저장됩니다.

HookNode

// 시각적 연결 구조
hookNode1.queue.pending → update2 ──┐
                                    ↓
                                 update1
                                    ↓
                                    └── back to update2

3. 재렌더링 예약 및 업데이트 처리

  • dispatchSetState 함수는 업데이트 노드를 큐에 추가한 후, 해당 Fiber의 컴포넌트를 다시 렌더링하도록 스케줄링합니다. 실제 재렌더링은 즉시 일어나지 않고 스케줄러에 따라 결정됩니다.
  • 재렌더링 과정(updateState / updateReducer)에서 React는 해당 훅 노드를 가져와 업데이트 큐(queue)에 쌓인 업데이트 노드들을 순차적으로 처리합니다. 이 과정은 훅 노드의 baseQueue에 있는 업데이트들을 순회하며 진행됩니다.

콜백형 업데이트와 prev 매개변수

  • 콜백형 setState(prev => …)을 사용하면, dispatchReducerAction이 큐에 쌓인 업데이트를 처리할 때 실제 hook.memoizedState 값을 prev로 전달합니다.
  • 이 때문에 렌더 사이클이 끝나기 전이라도 prev는 항상 가장 최신 상태 값을 가리킵니다.
// dispatchAction 내부 (의사 코드)
function dispatchReducerAction(fiber, queue, action) {
  // 1. 큐에 추가
  queue.pending = enqueue(queue.pending, { action });
  // 2. 재렌더 예약
  scheduleUpdateOnFiber(fiber);
}

// 재렌더 시 updateState 처리
let baseState = hook.memoizedState;
queue.pending.forEach(update => {
  baseState = update.action(baseState);
});
hook.memoizedState = baseState;

packages/react-reconciler/src/ReactFiberHooks.js

function dispatchReducerAction<A>(
  fiber: Fiber,
  queue: UpdateQueue<A>,
  action: A,
): void {
  const update = { action, next: null };
  // 1. 원형 연결 리스트에 추가
  const pending = queue.pending;
  if (pending === null) {
    update.next = update;
  } else {
    update.next = pending.next;
    pending.next = update;
  }
  queue.pending = update;
  // 2. 재렌더 예약
  scheduleUpdateOnFiber(fiber);
}
function updateReducer() {
  ...
  if (pendingQueue !== null) {
    ...
    do {
      const action = update.action;
      baseState = reducer(baseState, action);
      update = update.next;
    } while (update !== first);
    hook.memoizedState = baseState;
  }
  return [hook.memoizedState, queue.dispatch];
}
  • reducer: action이 함수면 action(baseState), 아니면 action을 반환한다.

  • 원본 코드

    function dispatchAction<A>(
      fiber: Fiber,
      queue: UpdateQueue<A>,
      action: A,
    ): void {
      const update = { action, next: null };
      // 원형 연결 리스트에 추가
      const pending = queue.pending;
      if (pending === null) {
        update.next = update;
      } else {
        update.next = pending.next;
        pending.next = update;
      }
      queue.pending = update;
      scheduleUpdateOnFiber(fiber);
    }
    
    ...
    
    function updateReducer<S, I, A>(
      reducer: (S, A) => S,
      initialArg: I,
      init?: I => S,
    ): [S, Dispatch<A>] {
      const hook = updateWorkInProgressHook();
      const queue = hook.queue;
      let baseState = hook.memoizedState;
      const pendingQueue = queue.pending;
      if (pendingQueue !== null) {
        queue.pending = null;
        // 원형 리스트의 첫 노드
        let first = pendingQueue.next;
        let update = first;
        do {
          const action = update.action;
          baseState = reducer(baseState, action);
          update = update.next;
        } while (update !== first);
        hook.memoizedState = baseState;
      }
      return [hook.memoizedState, queue.dispatch];
    }

여기서 action은 둘중 하나입니다.

  • 콜백형: prev => prev + 1 같은 함수
  • 값형: count + 1처럼 미리 평가된 원시값(숫자·문자열 등)

count + 1 vs (prev) => prev + 1의 차이

  • setCount(count + 1)
    • 렌더링 시점에 클로저가 캡처한 stale count 값을 사용합니다.
    • 같은 렌더 프레임 내에서 두 번 호출해도 값이 중복되어 반영되는 문제가 발생합니다.
  • setCount(prev => prev + 1)
    • dispatchAction이 큐를 처리하면서 실제 hook.memoizedStateprev로 전달하므로, 여러 번 호출해도 내부 상태가 누적되어 올바르게 반영됩니다.

참고:

Medium

https://javascript.plainenglish.io/understanding-react-fiber-in-depth-abe1be7a1797

https://velog.io/@alsgud8311/React-Fiber-아키텍처-딥다이브

0개의 댓글