setNumber(n => n + 1);
setNumber(n => n + 1);
setNumber(n => n + 1);
setNumber(n => n + 1)
: n => n + 1
함수를 큐에 추가합니다.
setNumber(n => n + 1)
: n => n + 1
함수를 큐에 추가합니다.
setNumber(n => n + 1)
: n => n + 1
함수를 큐에 추가합니다.
다음 렌더링 중에 useState
를 호출하면 React는 큐를 순회합니다. 이전 number
state는 0
이었으므로 React는 이를 첫 번째 업데이터 함수에 n
인수로 전달합니다. 그런 다음 React는 이전 업데이터 함수의 반환 값을 가져와서 다음 업데이터 함수에 n
으로 전달하는 식으로 반복합니다.
queued update | n | returns |
---|---|---|
n => n + 1 | 0 | 0 + 1 = 1 |
n => n + 1 | 1 | 1 + 1 = 2 |
n => n + 1 | 2 | 2 + 1 = 3 |
React는 3
을 최종 결과로 저장하고 useState
에서 반환합니다.
이것이 위 예시 “+3”을 클릭하면 값이 3씩 올바르게 증가하는 이유입니다.
<button onClick={() => {
setNumber(number + 5);
setNumber(n => n + 1);
}}>
setNumber(number + 5)
: number
는 0
이므로 setNumber(0 + 5)
입니다. React는 큐에 “5
로 바꾸기” 를 추가합니다.
setNumber(n => n + 1)
: n => n + 1
는 업데이터 함수입니다. React는 해당 함수를 큐에 추가합니다.
다음 렌더링하는 동안 React는 state 큐를 순회합니다.
queued update | n | returns |
---|---|---|
”replace with 5 ” | 0 (unused) | 5 |
n => n + 1 | 5 | 5 + 1 = 6 |
React는 6
을 최종 결과로 저장하고 useState
에서 반환합니다.
n => n + 1
) 내부의 매개변수는 어떻게 항상 최신 state 값을 가리킬까??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 등 다수의 스케줄링/이펙트 메타데이터를 포함)
}
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>
);
}
{
"memoizedState": 0, // count의 초기값 0
"baseState": 0, // internal 초기값 저장소
"baseQueue": null, // 아직 업데이트 없음
"queue": {
"pending": null,
"lanes": 0,
"dispatch": function, // setCount 호출 시 사용하는 함수
"lastRenderedState": 0
},
"next": // 2️⃣의 HookNode
}
useState
, useEffect
등 각 훅별로 상태·큐·다음 훅 링크를 저장하는 객체입니다.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>
);
}
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
// 시각적 연결 구조
hookNode1.queue.pending → update2 ──┐
↓
update1
↓
└── back to update2
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)
count
값을 사용합니다.setCount(prev => prev + 1)
dispatchAction
이 큐를 처리하면서 실제 hook.memoizedState
를 prev
로 전달하므로, 여러 번 호출해도 내부 상태가 누적되어 올바르게 반영됩니다.참고:
https://javascript.plainenglish.io/understanding-react-fiber-in-depth-abe1be7a1797