
useState 원리 살피기
useState는 어떻게 상태를 업데이트할까
• Tech
들어가며
- 이 글은
useState가 내부에서 어떻게 동작하는지 코드 레벨에서 학습하였습니다.if (__DEV__)와 같이 개발환경에서만 동작하는 코드는 제외하였습니다.- 학습한 내용을 기반으로 도식화하여 이해하기 쉽게 정리하였습니다.
- 언제 무엇이 어디에 저장되고 어떤 순간에 리렌더 되는지 이해하는 것을 목표로 합니다.
useState 기본 사용법 복습
useState의 기본적인 사용법입니다.
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount((v) => v + 1)}>{count}</button>;
}import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount((v) => v + 1)}>{count}</button>;
}useState에 기본값 0 을 전달하면, 현재 값(count)과 업데이터(setCount)를 반환합니다.- 이 글에서는 useState의 세 가지 부분에 대해서 다룹니다.
- 마운트시 useState
- setState 로 상태를 업데이트
- 리렌더링시 useState
Fiber와 Hook
useState에 대해 알아보기 전에, Hook의 구조와 이를 포함하는 Fiber에 대해서 알아볼 필요가 있습니다.
React에서 Hook에 대한 정보는 컴포넌트가 정보가 저장되어있는 Fiber라는 객체안에 저장되어있습니다.
그럼 Fiber란 무엇일까요? 간단히 알아봅시다.
Fiber

- 리액트는 함수 컴포넌트 하나당 하나의 Fiber 객체를 갖습니다.
- 이러한 Fiber는 리액트가 렌더링을 효율적으로 관리하기 위한 아키텍처입니다.
- 이번 렌더에서 계산할 입력/상태/우선순위 등의 정보를 담고, 트리 탐색을 가능하게 합니다.
type Fiber = {
tag: WorkTag,
key: null | string,
elementType: any,
type: any,
stateNode: any,
return: Fiber | null,
dependencies: Dependencies | null,
child: Fiber | null,
sibling: Fiber | null,
index: number,
// 이번 글에서 중요하게 다룰 부분
memoizedProps: any,
pendingProps: any,
memoizedState: any, // Hook이 저장된 부분
lanes: Lanes, // 업데이트 플래그
childLanes: Lanes, // 하위 Fiber의 업데이트 플래그
...
};type Fiber = {
tag: WorkTag,
key: null | string,
elementType: any,
type: any,
stateNode: any,
return: Fiber | null,
dependencies: Dependencies | null,
child: Fiber | null,
sibling: Fiber | null,
index: number,
// 이번 글에서 중요하게 다룰 부분
memoizedProps: any,
pendingProps: any,
memoizedState: any, // Hook이 저장된 부분
lanes: Lanes, // 업데이트 플래그
childLanes: Lanes, // 하위 Fiber의 업데이트 플래그
...
};- Fiber는 컴포넌트의 type, key, props 등 렌더링에 필요한 다양한 정보를 담고 있습니다.
- Fiber 구조에서, 컴포넌트 내에 선언된 Hook들은
memoizedState안에 연결리스트로 저장되어있습니다.
Fiber 속 Hook 객체

fiber.memoizedState는 컴포넌트내에 선언된 첫번째 훅을 가리킵니다.- 각 훅은 이어져있고 훅 호출 순서가 리스트의 순서입니다.
훅의 정보는 호출 순서로 Fiber 내에 저장되어있습니다.
이때 조건문에서 선언하게 되면
function Bad() {
const [a] = useState(0); // Hook#1
if (something) {
const [b] = useState(0); // 때로는 호출, 때로는 건너뜀 → 순서 깨짐
}
const [c] = useState(0); // 어떤 렌더에서는 Hook#2, 다른 렌더에서는 Hook#3가 되어 버림
}function Bad() {
const [a] = useState(0); // Hook#1
if (something) {
const [b] = useState(0); // 때로는 호출, 때로는 건너뜀 → 순서 깨짐
}
const [c] = useState(0); // 어떤 렌더에서는 Hook#2, 다른 렌더에서는 Hook#3가 되어 버림
}- 한 렌더에서는 훅 호출이 2번, 다른 렌더에서는 3번이 되어 호출 순서가 달라집니다.
- 호출 횟수가 달라지면 직전과 훅 포인터 매칭이 달라져 상태/이펙트가 엉뚱한 훅에 연결됩니다.
- 그렇게 되면,
useState가useEffect의 노드를 참조하려 들거나 엉뚱한 훅의queue를 가져와 다른 상태가 갱신되게 됩니다. - 따라서 훅은 조건문 내에서 사용할 수 없고 반드시 최상단에서 호출해야 합니다.
type Fiber = {
memoizedState: Hook | null, // [연결리스트 head] Hook이 저장된 부분
...
};
// fiber.memoizedState
type Hook = {
memoizedState: any; // 훅의 현재 값/데이터
baseState: any;
baseQueue: Update | null; // [원형 연결리스트 tail] 남은 업데이트
queue: UpdateQueue | null; // setState/dispatch 업데이트 큐
next: Hook | null; // 다음 훅
};
// hook.baseQueue
type Update<S, A = any> = {
lane: Lane;
revertLane: Lane;
action: A; // setState()에 넘긴 값/업데이터
hasEagerState: boolean;
eagerState: S | null;
next: Update<S, A> | null; // 다음 업데이트 노드
};
// hook.queue
type UpdateQueue<S, A = any> = {
pending: Update<S, A> | null; // [원형 연결리스트 tail] 새로 들어온 업데이트
lanes: Lanes; // 업데이트 우선순위
lastRenderedReducer: (S, A) => S;
lastRenderedState: S;
dispatch: Dispatch<A>; // setState 함수
};type Fiber = {
memoizedState: Hook | null, // [연결리스트 head] Hook이 저장된 부분
...
};
// fiber.memoizedState
type Hook = {
memoizedState: any; // 훅의 현재 값/데이터
baseState: any;
baseQueue: Update | null; // [원형 연결리스트 tail] 남은 업데이트
queue: UpdateQueue | null; // setState/dispatch 업데이트 큐
next: Hook | null; // 다음 훅
};
// hook.baseQueue
type Update<S, A = any> = {
lane: Lane;
revertLane: Lane;
action: A; // setState()에 넘긴 값/업데이터
hasEagerState: boolean;
eagerState: S | null;
next: Update<S, A> | null; // 다음 업데이트 노드
};
// hook.queue
type UpdateQueue<S, A = any> = {
pending: Update<S, A> | null; // [원형 연결리스트 tail] 새로 들어온 업데이트
lanes: Lanes; // 업데이트 우선순위
lastRenderedReducer: (S, A) => S;
lastRenderedState: S;
dispatch: Dispatch<A>; // setState 함수
};memoizedState라는 같은 이름을 가지고 있지만,fiber.memoizedState에는 컴포넌트 내에 선언된 훅들이 연결리스트로 저장되어있고hook.memoizedState에는 훅의 값이 저장되어있습니다.
- 업데이트 정보는
hook.baseQueue(이전 렌더에서 남은 업데이트)와hook.queue.pending(새로 추가된 업데이트)에 저장되어있습니다.- 업데이트 객체속
action에 실제setState를 통해 넘겨받은 값이나 식이 저장되어있습니다.
- 업데이트 객체속
- 업데이트가 발생하면, 훅 리스트는 처음부터 순회하여 어떤 훅인지 검사하기 때문에 head에 연결되어있고, 업데이트 객체는 마지막에 추가하기 때문에 tail에 연결되어있습니다.
fiber.memoizedState는 첫번째 훅(head)을 가리킵니다.hook.baseQueue와hook.queue.pending는 마지막 업데이트 객체(tail)을 가리킵니다.
1. 마운트시 mountState 의 동작
컴포넌트가 마운트시 useState 내부에서는 mountState 라는 함수가 동작합니다.
<Title /> 라는 컴포넌트를 예시로 단계별로 알아보겠습니다.

- 기본 훅 객체를 Fiber에 연결합니다.

- 초기값이 함수인 경우는 실행하고 값인경우는 그대로
memoizedState에 저장합니다.- Fiber에도
memoizedState(Hook들의 정보) 가 있고, Hook에도memoizedState(해당 Hook의 계산된 값)가 있습니다.
- Fiber에도

- 업데이트 정보가 저장될
queue를 만들어 Hook 객체에 연결합니다.

- Fiber와 Hook의 정보를 가진
dispatch(setState)를 만듭니다. setState함수를 실행하기만 해도, 어떤 컴포넌트의 어떤 훅에 대한 업데이트인지 바로 판단할 수 있습니다.

memoizedState와dispatch를 배열로 리턴합니다.
function mountState<S>(
initialState: (() => S) | S
): [S, Dispatch<BasicStateAction<S>>] {
// 1. 기본 훅 객체 생성 및 Fiber에 연결합니다.
const hook = mountWorkInProgressHook();
// 2. useState에 전달된 값이 함수면 실행하고 아니면 그냥 memoizedState에 저장합니다.
if (typeof initialState === "function") {
const initialStateInitializer = initialState;
initialState = initialStateInitializer();
}
hook.memoizedState = hook.baseState = initialState;
// 3. 업데이트 큐를 만들어서 훅 객체에 연결합니다.
const queue: UpdateQueue<S, BasicStateAction<S>> = {
pending: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
};
hook.queue = queue;
const queue = hook.queue;
// 4. setState 함수를 만듭니다.
const dispatch: Dispatch<BasicStateAction<S>> = (dispatchSetState.bind(
null,
currentlyRenderingFiber,
queue
): any);
queue.dispatch = dispatch;
// 5. state와 setState 리턴
return [hook.memoizedState, dispatch];
}function mountState<S>(
initialState: (() => S) | S
): [S, Dispatch<BasicStateAction<S>>] {
// 1. 기본 훅 객체 생성 및 Fiber에 연결합니다.
const hook = mountWorkInProgressHook();
// 2. useState에 전달된 값이 함수면 실행하고 아니면 그냥 memoizedState에 저장합니다.
if (typeof initialState === "function") {
const initialStateInitializer = initialState;
initialState = initialStateInitializer();
}
hook.memoizedState = hook.baseState = initialState;
// 3. 업데이트 큐를 만들어서 훅 객체에 연결합니다.
const queue: UpdateQueue<S, BasicStateAction<S>> = {
pending: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
};
hook.queue = queue;
const queue = hook.queue;
// 4. setState 함수를 만듭니다.
const dispatch: Dispatch<BasicStateAction<S>> = (dispatchSetState.bind(
null,
currentlyRenderingFiber,
queue
): any);
queue.dispatch = dispatch;
// 5. state와 setState 리턴
return [hook.memoizedState, dispatch];
}- 기본 훅 객체를 생성하고 Fiber에 연결합니다.
useState에 전달된 기본 값을 처리하고memoizedState에 저장합니다.- 업데이트 큐의 기본 값을 만들어서 훅 객체에 연결합니다.
- fiber랑 훅의 업데이트 큐의 정보를 가진
setState함수를 만듭니다.- 따라서 이후에
setState함수만 실행하면 되게끔, 어떤 fiber의 어떤 훅 큐에 업데이트 정보를 추가할지 미리 정하는 과정입니다.
- 따라서 이후에
state와setState를 리턴합니다.
2. setState 호출 시 일어나는 일

setState가 실행되면, 내부적으로 구현된 dispatch에는 Fiber와 Hook 정보가 담겨있어서 어디에 업데이트를 연결해야하는지 정보를 가지고 있습니다.setState가 실행되면, 현재 컨텍스트를 보고 업데이트 우선순위(lane)를 결정합니다.setState코드를 기반으로 업데이트 객체를 만듭니다.

- 업데이트 객체를 큐형태로 Hook 객체 안에
queue.pending에 추가합니다.

setState의 정보를 가지고 있는 Fiberlanes에 업데이트 플래그를 추가합니다.- 부모 Fiber로 올라가면서
childLanes에 업데이트 플래그를 추가합니다. - 그리고 루트 객체인 FiberRoot 의
pendingLanes에도 업데이트 플래그를 추가하고 스케줄러에 등록합니다.- FiberRoot는
ReactDOM.createRoot를 통해서 생성됩니다.
- FiberRoot는
function dispatchSetState<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
): void {
// 1) 이번 업데이트의 lane(우선순위 비트) 선택
const lane = requestUpdateLane(fiber);
// 2) 업데이트 큐에 들어갈 update 객체 생성
const update: Update<S, A> = {
lane,
revertLane: NoLane,
gesture: null,
action,
hasEagerState: false,
eagerState: null,
next: null as any,
};
// 3-1) 렌더 중, setState가 render phase에서 호출된 경우 특수하게 업데이트하고 종료합니다.
if (isRenderPhaseUpdate(fiber)) {
enqueueRenderPhaseUpdate(queue, update);
}
// 3-2) 렌더 밖, 이벤트 핸들러, useEffect/useLayoutEffect, 타이머 등에서 호출된 경우
else {
// 4) Eager 최적화 시도 - 조건에 맞을 경우 다음 상태를 계산하여 예약을 생략합니다.
const alternate = fiber.alternate;
let bailedOutEagerly = false;
if (
fiber.lanes === NoLanes &&
(alternate === null || alternate.lanes === NoLanes)
) {
const lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
try {
const currentState: S = queue.lastRenderedState as any;
const eagerState = lastRenderedReducer(currentState, action);
update.hasEagerState = true;
update.eagerState = eagerState;
// 4-1) 계산한 값이 동일하면 큐에는 넣되, 렌더 예약은 스킵합니다.
if (is(eagerState, currentState)) {
enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
bailedOutEagerly = true;
}
} catch (error) {}
}
}
// 5) Eager 스킵이 아니면 훅 큐(queue.pending 원형 리스트)에 삽입하고 렌더를 예약합니다.
if (!bailedOutEagerly) {
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
// lanes/childLanes 표시 + 스케줄러에 루트 작업 등록
scheduleUpdateOnFiber(root, fiber, lane);
// 전이(transition) 얽힘 메타: 같은 큐에 속한 관련 업데이트들의 lane을 묶는다.
entangleTransitionUpdate(root, queue, lane);
}
}
}
}function dispatchSetState<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
): void {
// 1) 이번 업데이트의 lane(우선순위 비트) 선택
const lane = requestUpdateLane(fiber);
// 2) 업데이트 큐에 들어갈 update 객체 생성
const update: Update<S, A> = {
lane,
revertLane: NoLane,
gesture: null,
action,
hasEagerState: false,
eagerState: null,
next: null as any,
};
// 3-1) 렌더 중, setState가 render phase에서 호출된 경우 특수하게 업데이트하고 종료합니다.
if (isRenderPhaseUpdate(fiber)) {
enqueueRenderPhaseUpdate(queue, update);
}
// 3-2) 렌더 밖, 이벤트 핸들러, useEffect/useLayoutEffect, 타이머 등에서 호출된 경우
else {
// 4) Eager 최적화 시도 - 조건에 맞을 경우 다음 상태를 계산하여 예약을 생략합니다.
const alternate = fiber.alternate;
let bailedOutEagerly = false;
if (
fiber.lanes === NoLanes &&
(alternate === null || alternate.lanes === NoLanes)
) {
const lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
try {
const currentState: S = queue.lastRenderedState as any;
const eagerState = lastRenderedReducer(currentState, action);
update.hasEagerState = true;
update.eagerState = eagerState;
// 4-1) 계산한 값이 동일하면 큐에는 넣되, 렌더 예약은 스킵합니다.
if (is(eagerState, currentState)) {
enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
bailedOutEagerly = true;
}
} catch (error) {}
}
}
// 5) Eager 스킵이 아니면 훅 큐(queue.pending 원형 리스트)에 삽입하고 렌더를 예약합니다.
if (!bailedOutEagerly) {
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
// lanes/childLanes 표시 + 스케줄러에 루트 작업 등록
scheduleUpdateOnFiber(root, fiber, lane);
// 전이(transition) 얽힘 메타: 같은 큐에 속한 관련 업데이트들의 lane을 묶는다.
entangleTransitionUpdate(root, queue, lane);
}
}
}
}- 이번 업데이트의 lane(우선순위 비트)를 선택합니다. (아직 표시/전파는 일어나지 않습니다.)
- 업데이트 큐에 들어갈 update 객체를 생성합니다.
- 호출된 경우에 따라 두 가지로 나누어 동작합니다.
- 렌더 중 - setState가 render phase에서 호출된 경우
- 렌더중 발생하는 업데이트만 다루는 특수한 큐에 업데이트 내용을 넣습니다.
- renderWithHooks 내부가 동일 컴포넌트를 즉시 리렌더해서 방금 넣은 업데이트를 소비합니다.
- 즉, 루트 수준 스케줄이 아니라 현재 렌더 콜스택 안에서 재시도(re-render loop) 하는 특수 경로입니다.
- 렌더 밖 - 이벤트 핸들러, useEffect/useLayoutEffect, 타이머 등에서 호출된 경우
- Eager 최적화를 시도합니다.
- 이 훅에 걸린 업데이트가 없다면 action으로 즉시 다음상태를 미리 게산합니다.
- 다음 상태가 이전 상태와 같다면 렌더예약을 생략하고 종료합니다.
- 렌더 중 - setState가 render phase에서 호출된 경우
- 렌더 밖에서 호출되었고 Eager 최적화 조건에 해당되지 않거나, 시도했지만 값이 다른경우
- 업데이트 객체를 훅 큐
queue.pending에 삽입합니다. - 해당 훅이 속한 Fiber가 속한, 루트를 찾습니다.
- 현재 fiber의 lanes 표시
- 루트까지 이동하면서 childLanes에 표시
- 루트 pendingLanes에 표시
- 해당 루트에 대한 렌더 작업을 스케줄러 큐에 넣습니다.
- 업데이트 객체를 훅 큐
eagerState 이란
eagerState는setState호출 순간, 렌더에 들어가기 전에 다음 state를 미리 계산해 둔 값입니다.- 이전 렌더의 계산결과와 이전 렌더의 reducer를 이용해서 계산합니다.
queue.lastRenderedReducer(queue.lastRenderedState, action)
- 목적은 불필요한 렌더와 계산을 줄여 성능을 높이는 것입니다.
시도 조건
- 이 훅에 걸린 업데이트 예정이 없고 & alternate 에도 일이 없어야합니다.
fiber.lanes === NoLanes && fiber.alternate?.lanes === NoLanes- 즉, 이 컴포넌트의 첫
setState이어야 합니다.
- 직전에 쓰던 리듀서와 동일할때 시도합니다.
- useState의 경우는 내부 구현이
useReducer와 거의 비슷합니다. - useState는 리듀서로
basicStateReducer를 사용하기 때문에 항상 동일합니다.
- useState의 경우는 내부 구현이
발생 가능한 상황
eager와 관련해서 세 가지 상황이 발생할 수 있습니다.
// 1) 한 틱에 여러 업데이트
const [n, setN] = useState(0);
setN(p => p + 1); // 첫 호출만 eager 시도 가능(대개 스케줄 O)
setN(p => p + 1); // 이후 호출들은 일반 경로로 큐에 쌓임
// 렌더 시 큐를 순서대로 소비해 최종 값 계산
// 2 동일 → 렌더 예약 스킵
const [n, setN] = useState(0);
setN(0); // eager=0, lastRenderedState=0 → 예약 생략(렌더 안 됨)
// 3) 상이 → 렌더 필요하지만 재계산 생략
const [n, setN] = useState(0);
setN(p => p + 1); // eager=1 → 스케줄 등록
// 다음 렌더에서 reducer 재호출 없이 eager(=1) 그대로 반영// 1) 한 틱에 여러 업데이트
const [n, setN] = useState(0);
setN(p => p + 1); // 첫 호출만 eager 시도 가능(대개 스케줄 O)
setN(p => p + 1); // 이후 호출들은 일반 경로로 큐에 쌓임
// 렌더 시 큐를 순서대로 소비해 최종 값 계산
// 2 동일 → 렌더 예약 스킵
const [n, setN] = useState(0);
setN(0); // eager=0, lastRenderedState=0 → 예약 생략(렌더 안 됨)
// 3) 상이 → 렌더 필요하지만 재계산 생략
const [n, setN] = useState(0);
setN(p => p + 1); // eager=1 → 스케줄 등록
// 다음 렌더에서 reducer 재호출 없이 eager(=1) 그대로 반영- 이미 lane가 있어서 eager을 시도할 수 없는 경우 일반 경로로 reducer를 호출하여 직접 계산합니다.
- eager 시도 조건을 만족해서 시도했는데, 이전 계산 값과 같은 경우 렌더 예약을 스킵합니다.
- eager 시도했는데 이전 계산 값과 다른 경우 렌더를 예약합니다. 그리고
다음 렌더에서
hasEagerState값을 체크하여 리듀서를 다시 부르지 않고eagerState를 그대로 사용합니다.
즉, eager의 시도는 setState가 호출된 시점에 바로 체크해서 계산하고, 그렇게 계산된 결과를 사용할지 말지 결정하는 것은 다음 렌더과정 중에서 결정하게 됩니다.
3. 업데이트시 updateState 의 동작
setState가 아니라, 리렌더링이 발생하고 useState를 만났을때 동작하는 과정입니다.
리렌더링 중에 useState를 만나면 내부적으로는 updateState 로 동작합니다.

- 렌더 중
useState를 만나게 되면, Fiber에서 현재 훅을 찾습니다.

- 훅 객체에서 [이전 렌더에서 남은 업데이트] + [새로 쌓인 업데이트] 을 합쳐서 원형 큐로 만듭니다.

- 렌더는 업데이트 우선순위를 가지고, 각 업데이트 객체도 우선순위(
lane)를 가지고 있습니다. - 이번 렌더 우선순위에 해당하는 업데이트만 계산합니다.
function onClick() {
setA(1); // SyncLane
setTimeout(() => {
setB(2); // DefaultLane
}, 0);
}function onClick() {
setA(1); // SyncLane
setTimeout(() => {
setB(2); // DefaultLane
}, 0);
}- 자동 배칭은 "같은 우선순위 컨텍스트에서 생긴 업데이트들"을 한 번의 렌더로 묶습니다.
- 서로 다른 lane(Sync vs Default) 업데이트는 같은 렌더에서 함께 처리되지 않습니다.
- 따라서 위 코드에서 두 개의 업데이트는 다른 렌더에서 처리됩니다.

- 계산된 결과는 훅 객체내에
memoizedState에 저장합니다. - 계산 되지 않은 업데이트는
baseQueue에 저장하여 다음 렌더로 넘깁니다.
function updateReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: (I) => S
): [S, Dispatch<A>] {
// 1. 훅 자리 맞추기 & 준비
const hook = updateWorkInProgressHook();
const queue = hook.queue;
queue.lastRenderedReducer = reducer; // 이번 렌더에서 사용한 리듀서 기록
// 2. pending 병합 → baseQueue 구성 (원형 리스트)
let baseQueue = hook.baseQueue;
const pendingQueue = queue.pending;
if (pendingQueue !== null) {
if (baseQueue !== null) {
const baseFirst = baseQueue.next;
const pendingFirst = pendingQueue.next;
baseQueue.next = pendingFirst;
pendingQueue.next = baseFirst;
}
current.baseQueue = baseQueue = pendingQueue;
queue.pending = null; // 새 업데이트 수신 준비
}
// 3. 큐 없음 빠른 경로 (bail-out) 또는 순회 준비
const baseState = hook.baseState;
if (baseQueue === null) {
hook.memoizedState = baseState; // 바꿀 것 없음
} else {
// 4. 큐 순회/적용: lane 필터링 → 스킵은 rebase, 적용은 state 갱신
const first = baseQueue.next;
let newState = baseState;
let newBaseState = null;
let newBaseQueueFirst = null;
let newBaseQueueLast: Update<S, A> | null = null;
let update = first;
let didReadFromEntangledAsyncAction = false;
do {
const updateLane = removeLanes(update.lane, OffscreenLane);
const isHiddenUpdate = updateLane !== update.lane;
let shouldSkipUpdate = isHiddenUpdate
? !isSubsetOfLanes(getWorkInProgressRootRenderLanes(), updateLane)
: !isSubsetOfLanes(renderLanes, updateLane);
if (enableGestureTransition && updateLane === GestureLane) {
const scheduledGesture = update.gesture;
if (scheduledGesture !== null) {
if (scheduledGesture.count === 0) {
update = update.next;
continue;
} else if (!isGestureRender(renderLanes)) {
shouldSkipUpdate = true;
} else {
const root: FiberRoot | null = getWorkInProgressRoot();
if (root === null) {
throw new Error(
"Expected a work-in-progress root. This is a bug in React. Please file an issue."
);
}
shouldSkipUpdate = root.pendingGestures !== scheduledGesture;
}
}
}
if (shouldSkipUpdate) {
// 스킵: 클론을 새 baseQueue에 이월(rebase) + 스킵 lane 표식
const clone: Update<S, A> = {
lane: updateLane,
revertLane: update.revertLane,
gesture: update.gesture,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: null,
};
if (newBaseQueueLast === null) {
newBaseQueueFirst = newBaseQueueLast = clone;
newBaseState = newState;
} else {
newBaseQueueLast = newBaseQueueLast.next = clone;
}
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
updateLane
);
markSkippedUpdateLanes(updateLane);
} else {
// 적용 경로 (revertLane/optimistic 포함)
const revertLane = update.revertLane;
if (revertLane === NoLane) {
if (newBaseQueueLast !== null) {
const clone: Update<S, A> = {
lane: NoLane,
revertLane: NoLane,
gesture: null,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: null,
};
newBaseQueueLast = newBaseQueueLast.next = clone;
}
if (updateLane === peekEntangledActionLane()) {
didReadFromEntangledAsyncAction = true;
}
} else {
if (isSubsetOfLanes(renderLanes, revertLane)) {
update = update.next;
if (revertLane === peekEntangledActionLane()) {
didReadFromEntangledAsyncAction = true;
}
continue;
} else {
const clone: Update<S, A> = {
lane: NoLane,
revertLane: update.revertLane,
gesture: null,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: null,
};
if (newBaseQueueLast === null) {
newBaseQueueFirst = newBaseQueueLast = clone;
newBaseState = newState;
} else {
newBaseQueueLast = newBaseQueueLast.next = clone;
}
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
revertLane
);
markSkippedUpdateLanes(revertLane);
}
}
// 실제 state 계산: eagerState 우선, 아니면 reducer 호출
const action = update.action;
if (shouldDoubleInvokeUserFnsInHooksDEV) {
reducer(newState, action); // DEV 검증용(결과 미사용)
}
newState = update.hasEagerState
? ((update.eagerState: any): S)
: reducer(newState, action);
}
update = update.next;
} while (update !== null && update !== first);
// 5. 새 기준/결과 기록 (원형 닫기, 값 비교 표식, thenable 전파)
if (newBaseQueueLast === null) {
newBaseState = newState;
} else {
newBaseQueueLast.next = newBaseQueueFirst;
}
if (!is(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate();
if (didReadFromEntangledAsyncAction) {
const entangledActionThenable = peekEntangledActionThenable();
if (entangledActionThenable !== null) {
throw entangledActionThenable;
}
}
}
hook.memoizedState = newState; // 이번 렌더 결과
hook.baseState = newBaseState; // 다음 렌더 기준 상태
hook.baseQueue = newBaseQueueLast; // 다음 렌더 기준 큐(tail)
queue.lastRenderedState = newState; // 직전 렌더 상태 갱신
}
// 6. 큐 메타 정리 & 동일 dispatch 반환
if (baseQueue === null) {
queue.lanes = NoLanes; // entangle 메타 리셋
}
const dispatch: Dispatch<A> = (queue.dispatch: any);
return [hook.memoizedState, dispatch];
}function updateReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: (I) => S
): [S, Dispatch<A>] {
// 1. 훅 자리 맞추기 & 준비
const hook = updateWorkInProgressHook();
const queue = hook.queue;
queue.lastRenderedReducer = reducer; // 이번 렌더에서 사용한 리듀서 기록
// 2. pending 병합 → baseQueue 구성 (원형 리스트)
let baseQueue = hook.baseQueue;
const pendingQueue = queue.pending;
if (pendingQueue !== null) {
if (baseQueue !== null) {
const baseFirst = baseQueue.next;
const pendingFirst = pendingQueue.next;
baseQueue.next = pendingFirst;
pendingQueue.next = baseFirst;
}
current.baseQueue = baseQueue = pendingQueue;
queue.pending = null; // 새 업데이트 수신 준비
}
// 3. 큐 없음 빠른 경로 (bail-out) 또는 순회 준비
const baseState = hook.baseState;
if (baseQueue === null) {
hook.memoizedState = baseState; // 바꿀 것 없음
} else {
// 4. 큐 순회/적용: lane 필터링 → 스킵은 rebase, 적용은 state 갱신
const first = baseQueue.next;
let newState = baseState;
let newBaseState = null;
let newBaseQueueFirst = null;
let newBaseQueueLast: Update<S, A> | null = null;
let update = first;
let didReadFromEntangledAsyncAction = false;
do {
const updateLane = removeLanes(update.lane, OffscreenLane);
const isHiddenUpdate = updateLane !== update.lane;
let shouldSkipUpdate = isHiddenUpdate
? !isSubsetOfLanes(getWorkInProgressRootRenderLanes(), updateLane)
: !isSubsetOfLanes(renderLanes, updateLane);
if (enableGestureTransition && updateLane === GestureLane) {
const scheduledGesture = update.gesture;
if (scheduledGesture !== null) {
if (scheduledGesture.count === 0) {
update = update.next;
continue;
} else if (!isGestureRender(renderLanes)) {
shouldSkipUpdate = true;
} else {
const root: FiberRoot | null = getWorkInProgressRoot();
if (root === null) {
throw new Error(
"Expected a work-in-progress root. This is a bug in React. Please file an issue."
);
}
shouldSkipUpdate = root.pendingGestures !== scheduledGesture;
}
}
}
if (shouldSkipUpdate) {
// 스킵: 클론을 새 baseQueue에 이월(rebase) + 스킵 lane 표식
const clone: Update<S, A> = {
lane: updateLane,
revertLane: update.revertLane,
gesture: update.gesture,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: null,
};
if (newBaseQueueLast === null) {
newBaseQueueFirst = newBaseQueueLast = clone;
newBaseState = newState;
} else {
newBaseQueueLast = newBaseQueueLast.next = clone;
}
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
updateLane
);
markSkippedUpdateLanes(updateLane);
} else {
// 적용 경로 (revertLane/optimistic 포함)
const revertLane = update.revertLane;
if (revertLane === NoLane) {
if (newBaseQueueLast !== null) {
const clone: Update<S, A> = {
lane: NoLane,
revertLane: NoLane,
gesture: null,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: null,
};
newBaseQueueLast = newBaseQueueLast.next = clone;
}
if (updateLane === peekEntangledActionLane()) {
didReadFromEntangledAsyncAction = true;
}
} else {
if (isSubsetOfLanes(renderLanes, revertLane)) {
update = update.next;
if (revertLane === peekEntangledActionLane()) {
didReadFromEntangledAsyncAction = true;
}
continue;
} else {
const clone: Update<S, A> = {
lane: NoLane,
revertLane: update.revertLane,
gesture: null,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: null,
};
if (newBaseQueueLast === null) {
newBaseQueueFirst = newBaseQueueLast = clone;
newBaseState = newState;
} else {
newBaseQueueLast = newBaseQueueLast.next = clone;
}
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
revertLane
);
markSkippedUpdateLanes(revertLane);
}
}
// 실제 state 계산: eagerState 우선, 아니면 reducer 호출
const action = update.action;
if (shouldDoubleInvokeUserFnsInHooksDEV) {
reducer(newState, action); // DEV 검증용(결과 미사용)
}
newState = update.hasEagerState
? ((update.eagerState: any): S)
: reducer(newState, action);
}
update = update.next;
} while (update !== null && update !== first);
// 5. 새 기준/결과 기록 (원형 닫기, 값 비교 표식, thenable 전파)
if (newBaseQueueLast === null) {
newBaseState = newState;
} else {
newBaseQueueLast.next = newBaseQueueFirst;
}
if (!is(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate();
if (didReadFromEntangledAsyncAction) {
const entangledActionThenable = peekEntangledActionThenable();
if (entangledActionThenable !== null) {
throw entangledActionThenable;
}
}
}
hook.memoizedState = newState; // 이번 렌더 결과
hook.baseState = newBaseState; // 다음 렌더 기준 상태
hook.baseQueue = newBaseQueueLast; // 다음 렌더 기준 큐(tail)
queue.lastRenderedState = newState; // 직전 렌더 상태 갱신
}
// 6. 큐 메타 정리 & 동일 dispatch 반환
if (baseQueue === null) {
queue.lanes = NoLanes; // entangle 메타 리셋
}
const dispatch: Dispatch<A> = (queue.dispatch: any);
return [hook.memoizedState, dispatch];
}- 훅 자리 맞추기
- 현재 호출 순서의 훅 정보를
updateWorkInProgressHook에서 호출합니다.
- 현재 호출 순서의 훅 정보를
- baseQueue 와 pending 병합 → baseQueue 구성
- 이 틱에 들어온 새 업데이트들
queue.pending을 기존baseQueue와 포인터 스왑으로 병합해, 이번 렌더에서 소비할 스냅샷 큐를 만듭니다. - 그 뒤 pending은 null로 비워 다음 업데이트를 받을 준비를 합니다.
- 이 틱에 들어온 새 업데이트들
- 큐가 없으면 빠른 경로(bail-out)
- 병합 결과
baseQueue가 없으면 바꿀 것이 없으므로queue.lanes를NoLanes로 초기화하고 종료합니다.
- 병합 결과
- 큐 순회하면서 적용하거나 스킵
- 원형 큐를 한 바퀴 돌며 각 업데이트의 lane을 이번 렌더의
renderLanes과 비교합니다.- 스킵: 클론을 만들어 새
baseQueue에 이월(rebase) 하고, 현재 Fiber에 스킵 lane 표식을 남겨 다음 렌더로 넘깁니다. - 적용:
hasEagerState면 그 값을 그대로, 아니면reducer(newState, action)로 계산합니다.
- 스킵: 클론을 만들어 새
- 원형 큐를 한 바퀴 돌며 각 업데이트의 lane을 이번 렌더의
- 결과 기록
- 순회가 끝나면 새
baseQueue를 원형으로 닫고,- 이번 결과를
hook.memoizedState에, - 다음 렌더 기준을
hook.baseState/hook.baseQueue에 저장, queue.lastRenderedState를 업데이트합니다.
- 이번 결과를
- 값이 실제로 바뀌었으면 "업데이트 받음" 표식을 남기고, 얽힌 async 액션을 읽은 경우에는 thenable을 throw해 Suspense로 전파합니다.
- 순회가 끝나면 새
- 큐 메타 정리 및
dispatch반환- 처리할 큐가 비었으면
queue.lanes = NoLanes로 리셋합니다. - 마지막으로
queue.dispatch(동일 참조)를 반환합니다.
- 처리할 큐가 비었으면
추가 학습
Lane
- 앞서 fiber에는 lane이라는 개념이 등장했습니다.
- 단순히 업데이트 플래그라고 설명하고, 이 fiber에 업데이트가 예정되어있는지 체크하는 용도정도로만 설명했습니다.
- 하지만 단순히 업데이트 플래그처럼 변수로 교체되는 개념은 아닙니다.
Lane의 종류
// ReactFiberLane.js
export const NoLanes: Lanes = /* */ 0b0000000000000000000000000000000;
export const NoLane: Lane = /* */ 0b0000000000000000000000000000000;
export const SyncHydrationLane: Lane = /* */ 0b0000000000000000000000000000001;
export const SyncLane: Lane = /* */ 0b0000000000000000000000000000010;
export const SyncLaneIndex: number = 1;
export const InputContinuousHydrationLane: Lane = /* */ 0b0000000000000000000000000000100;
export const InputContinuousLane: Lane = /* */ 0b0000000000000000000000000001000;
export const DefaultHydrationLane: Lane = /* */ 0b0000000000000000000000000010000;
export const DefaultLane: Lane = /* */ 0b0000000000000000000000000100000;
export const SyncUpdateLanes: Lane = SyncLane | InputContinuousLane | DefaultLane;
...// ReactFiberLane.js
export const NoLanes: Lanes = /* */ 0b0000000000000000000000000000000;
export const NoLane: Lane = /* */ 0b0000000000000000000000000000000;
export const SyncHydrationLane: Lane = /* */ 0b0000000000000000000000000000001;
export const SyncLane: Lane = /* */ 0b0000000000000000000000000000010;
export const SyncLaneIndex: number = 1;
export const InputContinuousHydrationLane: Lane = /* */ 0b0000000000000000000000000000100;
export const InputContinuousLane: Lane = /* */ 0b0000000000000000000000000001000;
export const DefaultHydrationLane: Lane = /* */ 0b0000000000000000000000000010000;
export const DefaultLane: Lane = /* */ 0b0000000000000000000000000100000;
export const SyncUpdateLanes: Lane = SyncLane | InputContinuousLane | DefaultLane;
...- Sync / Discrete: 클릭, 키다운 같은 즉각 반영 이벤트용.
- InputContinuous: 마우스무브, 스크롤처럼 연속 입력.
- Default: 일반 업데이트(이펙트 내부 setState 등).
- Transition: startTransition/useTransition로 표시한 저우선순위 UI 변경(여러 개의 transition lane들이 존재).
- Retry / Suspense 관련: 서스펜스 해제·재시도 시 쓰는 내부용 lane들.
- Hydration(SelectiveHydration): SSR 하이드레이션에서 선택적으로 깨우는 작업.
- Idle: 아주 한가할 때 처리해도 되는 업데이트.
- Offscreen: 숨겨진 트리(Offscreen/Suspense fallback 뒤)에 걸린 업데이트를 구분.
- NoLane: 없음(0).
특징
- 업데이트의 lane
- 각 업데이트 객체가 각자의 lane 비트를 가지고 있습니다.
- 루트의 집계(root.pendingLanes)
- 루트 차원에서는 모든 미처리 lane을 비트 OR로 "합쳐서" 보관합니다.
- 예:
pendingLanes = T1 | T2 | Default | …
- 이번 렌더가 처리할 집합(renderLanes)
- 스케줄러(getNextLanes/ensureRootIsScheduled)가 지금 당장 처리할 lane들의 부분집합을 고릅니다.
- 보통 가장 높은 우선순위 버킷(Sync/Discrete/Default/Transition/Idle…)을 우선으로 뽑습니다.
앞서 setState의 과정에서 lane은 부모로 전파되었습니다.
그런데 어떻게 자식 컴포넌트가 감지해서 리렌더링되는 걸까요?
- 렌더패스에서 props는 두개의 값을 사용합니다.
fiber.memoizedProps: 직전 커밋 시점의 props (이미 화면에 반영된 값)fiber.pendingProps: 이번 렌더에서 부모가 만들어 넣은 새 props
- 따라서 부모컴포넌트의 리렌더링이 이루어지고 나서 새로운
pendingProps를 전달하면 그때 변경된것을 감지하고 리렌더링을 하게 됩니다.
그렇다면 props가 없다면 리렌더링이 안될까요?
export function createElement(type, config, children) {
const props = {};
...
}export function createElement(type, config, children) {
const props = {};
...
}- props가 없다면 null로 전달되지만, 내부로직이
{}를 생성합니다. - 따라서 내부적으로 props의 참조가 새로 생성되기 때문에 리렌더링 됩니다.
- Context API의 로직중에는 스케줄러에 렌더를 예약하는 로직이 없습니다.
- 대신, Context 값이 바뀌게 만든 원인(대개 Provider 쪽 setState/props 변경)이 이미
scheduleUpdateOnFiber를 호출해서 루트 작업을 예약한 상태입니다. - 즉, Context에서 value가 변경되었을때 리렌더링되는 것은, Context 때문이 아닌,
setState의 영향입니다.
정리하기
- 구조
fiber.memoizedState→ 훅 리스트의 헤드- 각
Hook→memoizedState(값),baseQueue(이전렌더 업데이트),queue(새 업데이트),next(다음 훅)
- 마운트(
mountState)- 훅 노드 생성
- 초기값 저장
- 훅 전용
queue생성 setState(dispatch)바인딩[state, setState]리턴
- 업데이트(
updateState)fiber.memoizedState에서 현재 훅 찾기pending(새 배치) +baseQueue(남은 것) 병합- 현재 lane에 해당하는건 적용, 그 외 스킵
- 결과를
memoizedState에 기록
setState호출- update 객체 생성(필요하면 eagerState 계산)
- 훅의
queue.pending에 삽입 scheduleUpdateOnFiber로lanes/childLanes전파- 루트 스케줄 등록
- 다음 렌더에서 큐 소비 후 커밋