UI를 차단하지 않고 상태를 업데이트 할 수 있는 React Hook이다.
(리액트 19버전에서 변동사항이 있기 때문 리액트 19 공식문서를 보는 것을 권장드립니다)
const [isPending, startTransition] = useTransition(); // 매개변수를 받지 않는다.
useTransition은 정확히 두 개의 항목이 있는 배열을 반환한다.
isPending
플래그: 대기 중인 Transition이 있는지 알려준다.startTransition
함수: 상태 업데이트를 Transition으로 표시할 수 있게 해주는 함수이다.💡 React에서 Transition이란?
긴급하지 않는 상태 업데이트를 의미한다. React가 특정 상태 업데이트를 즉시 처리하지 않고, 우선순위가 낮은 작업으로 간주하여 처리할 수 있게 해준다.
- UI 반응성 유지: 중요한 업데이트(ex. 사용자 입력)에 우선순위를 두어 UI가 항상 반응적으로 유지되도록 한다.
- 부드러운 사용자 경험: 무거운 렌더링 작업을 뒤로 미루어 더 나은 사용자 경험(UX)를 제공한다.
- 동시성 처리: 여러 상태 업데이트가 동시에 발생할 때, 중요한 업데이트를 먼저 처리할 수 있게 해준다.
Transition의 사용 예시
useTransition
이 반환하는 startTransition
함수를 사용하면 state 업데이트를 Transition(낮은 우선순위)으로 표시할 수 있다.
function TabContainer(){
const [isPending, startTransition] = useTransition();
const [tab, setTab] = useState('A');
const handleTabButtonClick = (tabName) => {
startTransition(() => {
setTab(tabName);
});
}
}
💡 startTransition 내에서 호출되는 함수를 액션(Action)이라고 한다.
관례상 startTransition 내부에서 호출되는 모든 콜백(ex. 콜백 prop)은action
이라는 이름을 사용하거나Action
접미사를 포함해야 한다.function SubmitButton({ submitAction }) { // Action 접미사 사용 const [isPending, startTransition] = useTransition(); const handleSubmitButtonClick = () => { startTransition(() => { submitAction(); // Action 접미사 사용 }); } }
매개변수
scope
: 하나 이상의 set 함수를 호출하여 일부 state을 업데이트하는 함수이다.scope
를 즉시 호출하고 scope
함수를 호출하는 동안 동기적으로 예약된 모든 state 업데이트를 Transition으로 표시한다.(non-blocking)startTransition(async () => {
await someAsyncFunction();
// ✅ Using startTransition *after* await
startTransition(() => {
setPage('/about');
});
});
useTransition
은 Hook이기 때문에 컴포넌트나 커스텀 Hook 내부에서만 호출할 수 있다.
만약 다른 곳에서 Transition을 시작해야 하는 경우, 독립형 startTransition 함수를 호출 해야한다.
startTransition에 전달되는 함수는 즉시 실행되며, 실행 중에 발생하는 모든 상태 업데이트를 Transition으로 표시한다.
Transition으로 표시된 state 업데이트는 다른 state 업데이트에 의해 중단된다.
Transition 업데이트는 텍스트 입력을 제어하는 데 사용할 수 없다.
내부 동작 원리를 파악하는 코드는 React v19.0.0 버전의 코드입니다.
function mountTransition(): [
boolean, // isPending 플래그
(callback: () => void, options?: StartTransitionOptions) => void // startTransition 함수
] {
const stateHook = mountStateImpl((false: Thenable<boolean> | boolean)); // 상태 초기화
// startTransition 함수 생성
const start = startTransition.bind(
null,
currentlyRenderingFiber, // 현재 렌더링 중인 Fiber 노드(fiber)
stateHook.queue, // 상태 업데이트 큐(queue)
true, // 트랜지션 중의 상태(pendingState)
false // 트랜지션 후의 상태(finishedState)
);
// Hook 저장
const hook = mountWorkInProgressHook();
hook.memoizedState = start;
return [false, start];
}
mountTransition
함수는 useTransition
Hook을 초기화한다.
트랜지션의 대기 상태를 나타내는 isPending
플래그와 트랜지션을 시작하는 startTransition
함수를 생성한다.(일부 매개변수는 바인딩되어 미리 설정된다)
생성된 startTransition
함수는 현재 Fiber 노드와 상태 큐에 바인딩되며 Hook의 memoizedState로 저장되어 재사용된다.
이 함수는 트랜지션을 시작할 때 사용되며 컴포넌트가 리렌더링되어도 동일한 함수 참조를 유지한다.
export const enableAsyncActions = true; // 비동기 액선 플래그
export const enableTransitionTracing = false; // 트랜지션 추적 플래그
function startTransition<S>(
fiber: Fiber, // 현재 작업중인 컴포넌트의 Fiber노드
queue: UpdateQueue<S | Thenable<S>, BasicStateAction<S | Thenable<S>>>, // 상태 업데이트 큐
pendingState: S, // 트랜지션 중의 상태
finishedState: S, // 트랜지션 후의 상태
callback: () => mixed, // 트랜지션 동안 실행될 콜백
options?: StartTransitionOptions // 추가 옵션(ex. 트랜지션 이름 등)
): void {
const previousPriority = getCurrentUpdatePriority(); // 현재 우선순위 저장
setCurrentUpdatePriority(
higherEventPriority(previousPriority, ContinuousEventPriority)
); // 현재 우선순위, ContinuousEventPriority 중 더 높은것 선택
// ContinuousEventPriority는 연속적인 이벤트(드래그, 스크롤 등)에 사용되는 우선순위
const prevTransition = ReactSharedInternals.T; // 현재 진행 중인 트랜지션 정보 저장
const currentTransition: BatchConfigTransition = {}; // 새로운 트래지션 객체 생성
if (enableAsyncActions) {
// 현재 트랜지션 설정, 낙관적 업데이트 수행
// 비동기 작업의 결과를 기다리지 않고 UI를 즉시 업데이트 하는 방식
ReactSharedInternals.T = currentTransition;
dispatchOptimisticSetState(fiber, false, queue, pendingState);
} else {
// 일반적인 상태 업데이트 수행
// 적절한 업데이트 우선순위 요청
ReactSharedInternals.T = null;
dispatchSetStateInternal(
fiber,
queue,
pendingState,
requestUpdateLane(fiber)
);
ReactSharedInternals.T = currentTransition;
}
if (enableTransitionTracing) {
// 트랜지션 추적(이름 할당, 시작 시간 기록)
if (options !== undefined && options.name !== undefined) {
currentTransition.name = options.name;
currentTransition.startTime = now();
}
}
try {
if (enableAsyncActions) {
const returnValue = callback(); // 콜백 실행, 결과 저장
const onStartTransitionFinish = ReactSharedInternals.S;
if (onStartTransitionFinish !== null) {
// 트랜지션 시작을 추적할 때 사용
onStartTransitionFinish(currentTransition, returnValue);
}
if (
returnValue !== null &&
typeof returnValue === "object" &&
typeof returnValue.then === "function"
) {
// 콜백의 결과가 Promise인 경우
const thenable = ((returnValue: any): Thenable<mixed>);
const thenableForFinishedState = chainThenableValue(
thenable,
finishedState
); // Promise 결과와 finishedState 연결
dispatchSetStateInternal(
fiber,
queue,
(thenableForFinishedState: any),
requestUpdateLane(fiber)
); // 상태 업데이트
} else {
// 콜백의 결과가 Promise가 아닌경우
// 바로 finishedState로 상태 업데이트
dispatchSetStateInternal(
fiber,
queue,
finishedState,
requestUpdateLane(fiber)
);
}
} else {
// 비동기 액션이 비활성화인 경우
// 먼저 finishedState로 상태 업데이트 후 콜백 실행
dispatchSetStateInternal(
fiber,
queue,
finishedState,
requestUpdateLane(fiber)
);
callback();
}
} catch (error) {
if (enableAsyncActions) {
// 비동기 액션이 활성화인 경우 에러를 rejected 상태의 Thenable 객체로 변환
const rejectedThenable: RejectedThenable<S> = {
then() {},
status: "rejected",
reason: error,
};
dispatchSetStateInternal(
fiber,
queue,
rejectedThenable,
requestUpdateLane(fiber)
); // 상태 업데이트
} else {
throw error; // 비동기 액션이 비활성인 경우 에러를 그대로 던짐
}
} finally {
setCurrentUpdatePriority(previousPriority); // 이전에 저장해둔 업데이트 우선순위 복원
ReactSharedInternals.T = prevTransition; // 전역 트랜지션 상태를 이전 상태로 변경
}
}
startTransition
함수는 React의 트랜지션의 시작과 종료를 관리한다.
현재 우선순위를 저장하고 새로운 우선순위를 설정한 후 콜백을 실행하며 콜백 실행 중 발생하는 상태 업데이트는 트랜지션으로 처리된다.
비동기 작업의 경우 Promise 처리를 통해 상태 업데이트를 관리한다. 에러 처리와 정리 작업도 수행하여 안정적인 트랜지션 처리를 보장한다.
function dispatchOptimisticSetState<S, A>(
fiber: Fiber, // 현재 작업중인 컴포넌트의 Fiber 노드
throwIfDuringRender: boolean, // 렌더링 중 업데이트 시 에러를 던질지 여부
queue: UpdateQueue<S, A>, // 상태 업데이트 큐
action: A // 실행할 상태 업데이트 액션
): void {
const transition = requestCurrentTransition(); // 현재 진행 중인 트랜지션 정보 가져오기
const update: Update<S, A> = {
lane: SyncLane, // 동기 레인으로 설정(높은 우선순위 부여)
revertLane: requestTransitionLane(transition), // 트랜지션을 위한 레인 요청
action, // 전달받은 상태 업데이트 액션
hasEagerState: false,
eagerState: null,
next: (null: any),
};
if (isRenderPhaseUpdate(fiber)) {
// 렌더링 중인지 확인(UI를 그리고 있는지)
if (throwIfDuringRender) {
// 렌더링 중 업데이트가 허용되지 않는다면 에러 처리
throw new Error("Cannot update optimistic state while rendering.");
}
} else {
// 업데이트를 동시성 Hook 업데이트 큐에 추가
const root = enqueueConcurrentHookUpdate(fiber, queue, update, SyncLane);
if (root !== null) {
// 루트가 반환되면(업데이트가 필요한 경우)
// 타이머를 시작하고 Fiber 업데이트를 스케줄링
startUpdateTimerByLane(SyncLane);
scheduleUpdateOnFiber(root, fiber, SyncLane);
}
}
markUpdateInDevTools(fiber, SyncLane, action);
}
dispatchOptimisticSetState
함수는 낙관적 업데이트를 처리한다.
높은 우선순위(SyncLane)로 업데이트를 생성하여 UI를 즉시 반영하지만, 동시에 트랜지션 레인을 요청하여 후속 업데이트를 위한 준비를 한다.
이를 통해 사용자에게 즉각적인 피드백을 제공하면서도 실제 데이터 처리는 백그라운드에서 진행할 수 있도록 한다.
export function claimNextTransitionLane(): Lane {
const lane = nextTransitionLane; // 현재 사용가능한 다음 트랜지션 레인을 변수에 저장
nextTransitionLane <<= 1; // 왼쪽으로 1비트 시프트(다음 트랜지션을 위해 레인을 준비하는 과정)
if ((nextTransitionLane & TransitionLanes) === NoLanes) {
// 만약 nextTransitionLane 모든 트랜지션 레인을 벗어났다면(NoLanes, 비트마스크가 모두 0)
// 다시 첫번째 트랜지션 레인으로 리셋
nextTransitionLane = TransitionLane1;
}
return lane; // 최종적으로 선택된 레인을 반환
}
export function requestTransitionLane(
// 매개변수가 실제로 사용되지 않지만 이 함수는 트랜지션 컨텍스트 내에서 사용되어야 한다는 것을 알림
transition: BatchConfigTransition | null
): Lane {
if (currentEventTransitionLane === NoLane) {
// 만약 현재 이벤트에 대한 트랜지션 레인이 설정되지 않았다면(NoLane)
// 새 레인을 할당받는다.(새로운 이벤트의 시작을 의미)
currentEventTransitionLane = claimNextTransitionLane();
}
// 현재 이벤트에 대한 트랜지션 레인 반환
// 동일한 이벤트 내의 모든 트랜지션에 대해 재사용된다.
return currentEventTransitionLane;
}
requestTransitionLane 함수는 현재 이벤트에 대한 트랜지션 레인을 관리한다.
만약 현재 이벤트에 할당된 레인이 없다면(새로운 이벤트인 경우, NoLane) 새로운 레인을 할당받는다.
그리고 이 레인을 반환하여 동일한 이벤트 내의 모든 트랜지션에 대해 일관된 우선순위를 제공한다.
이를 통해 React는 효율적으로 트랜지션을 관리하고 일관된 사용자 경험을 제공할 수 있다.
function dispatchSetStateInternal<S, A>(
fiber: Fiber, // 현재 작업 중인 컴포넌트의 Fiber 노드
queue: UpdateQueue<S, A>, // 상태 업데이트 큐
action: A, // 실행할 상태 업데이트 액션
lane: Lane // 업데이트의 우선순위를 나타내는 레인
): boolean {
// 업데이트 객체 생성
const update: Update<S, A> = {
lane,
revertLane: NoLane,
action,
hasEagerState: false,
eagerState: null,
next: (null: any),
};
if (isRenderPhaseUpdate(fiber)) {
// 렌더링 중인 경우(UI를 그리고 있는 경우) 업데이트를 렌더링 큐에 추가
enqueueRenderPhaseUpdate(queue, update);
} else {
// 렌더링 단계가 아닌 경우
const alternate = fiber.alternate;
if (
fiber.lanes === NoLanes &&
(alternate === null || alternate.lanes === NoLanes)
) {
// 현재 Fiber와 alternate Fiber에 진행 중인 업데이트가 없는 경우
// 즉시 새 상태 계산(불필요한 리렌더링 방지)
const lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
try {
// 마지막에 사용된 리듀서 함수를 사용하여 새 상태 계산 시도
const currentState: S = (queue.lastRenderedState: any);
const eagerState = lastRenderedReducer(currentState, action);
update.hasEagerState = true;
update.eagerState = eagerState;
if (is(eagerState, currentState)) {
// 계산된 상태가 현재 상태와 같다면 bail out 처리하여 최적화
enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
return false;
}
} catch (error) {}
}
}
// 업데이트를 동시성 Hook 업데이트 큐에 추가
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
// 루트가 반환되면(업데이트가 필요한 경우)
// Fiber 업데이트를 스케줄링, 트랜지션 업데이트를 얽힘 처리
scheduleUpdateOnFiber(root, fiber, lane);
entangleTransitionUpdate(root, queue, lane);
return true;
}
}
return false;
}
dispatchSetStateInternal
함수는 React의 상태 업데이트 프로세스를 관리한다.
렌더링 단계에 따라 다르게 동작하며, 특히 렌더링 단계가 아닐 때는 즉시 새 상태를 계산하여 불필요한 리렌더링을 방지한다.
또한 업데이트를 큐에 추가하고 필요한 경우 Fiber 업데이트를 스케줄링한다.
이러한 최적화 과정을 통해 React는 효율적인 상태 관리와 렌더링을 수행한다.
export function requestUpdateLane(fiber: Fiber): Lane {
const mode = fiber.mode;
if (!disableLegacyMode && (mode & ConcurrentMode) === NoMode) {
// Concurrent 모드가 아닌 레거시 모드라면 항상 동기 처리(SyncLane)
return (SyncLane: Lane);
} else if (
(executionContext & RenderContext) !== NoContext &&
workInProgressRootRenderLanes !== NoLanes
) {
// 현재 렌더링 중이고, 진행 중인 렌더링 작업이 있다면
// 현재 렌더링 중인 레인들 중 하나를 임의로 선택
return pickArbitraryLane(workInProgressRootRenderLanes);
}
const transition = requestCurrentTransition();
if (transition !== null) {
// 현재 트랜지션이 있다면
// 연관된 액션 스코프 레인(낙관적 UI 업데이트를 위해 사용되는 특별한 범위)이 있는지 확인
const actionScopeLane = peekEntangledActionLane();
// 액션 스코프 레인이 있다면 사용하고 없다면 새로운 트랜지션 레인 요청
return actionScopeLane !== NoLane
? actionScopeLane
: requestTransitionLane(transition);
}
// 위 모든 조건에 해당하지 않는 경우(일반적인 상태 업데이트 등) 현재 이벤트의 우선순위를 기반으로 레인 결정
return eventPriorityToLane(resolveUpdatePriority());
}
requestUpdateLane
함수는 React 컴포넌트의 업데이트에 대한 우선순위(Lane)을 결정한다.
레거시 모드, 렌더링 중 업데이트, 트랜지션, 그리고 일반적인 상태 업데이트 등 다양한 상황에 따라 최적의 레인을 반환하여 React의 효율적인 업데이트 처리를 지원한다.
function entangleTransitionUpdate<S, A>(
root: FiberRoot, // 현재 작업 중인 Fiber 트리의 루트
queue: UpdateQueue<S, A>, // 상태 업데이트 큐
lane: Lane // 현재 업데이트의 우선순위를 나타내는 레인
): void {
if (isTransitionLane(lane)) {
// 현재 레인이 트랜지션 레인인 경우
let queueLanes = queue.lanes;
// 현재 큐의 레인과 대기 중인 레인을 교차하여(두 레인의 공통 부분 찾기) 실제로 처리해야 할 레인 결정
queueLanes = intersectLanes(queueLanes, root.pendingLanes);
// 새로운 큐 레인 생성(기존 큐 레인, 현재 업데이트 레인 병합)
const newQueueLanes = mergeLanes(queueLanes, lane);
queue.lanes = newQueueLanes; // 새로운 큐 레인 설정
// 루트에 이 레인들이 얽혀 있음(여러 업데이트가 서로 관련되어 있음)을 표시
markRootEntangled(root, newQueueLanes);
}
}
entangleTransitionUpdate
함수는 트랜지션 업데이트의 레인을 관리한다.
현재 업데이트 레인과 기존 큐의 레인을 병합하고 이를 루트에 표시한다.
이러한 얽힘 처리를 통해 React는 여러 트랜지션 업데이트 간의 관계를 추적하고 효율적으로 처리할 수 있다.
function updateTransition(): [
boolean, // isPending 플래그
(callback: () => void, options?: StartTransitionOptions) => void // startTransition 함수
] {
const [booleanOrThenable] = updateState(false); // 트랜지션 상태 초기화
const hook = updateWorkInProgressHook(); // 훅 정보 가져오기
const start = hook.memoizedState; // 시작 함수 가져오기
// booleanOrThenable이 boolean이라면 그 값을 사용하고 아니라면 비동기 작업의 완료를 기다린다.
// 이는 트랜지션이 진행 중인지(Pending) 여부를 결정한다.
const isPending =
typeof booleanOrThenable === "boolean"
? booleanOrThenable
useThenable(booleanOrThenable);
return [isPending, start];
}
updateTransition
함수는 useTransition
Hook의 업데이트 로직을 처리한다.
현재 트랜지션을 확인하고, isPending
플래그를 업데이트한다. 비동기 작업의 경우 useThenable
을 사용하여 처리한다.
이를 통해 React 컴포넌트는 트랜지션의 진행 상태를 추적하고 적절히 대응할 수 있다.
useTransition은 React 애플리케이션에서 UI의 반응성을 유지하면서 복잡한 상태 업데이트를 관리하는 강력한 도구이다.
초기화: useTransition이 호출되면, isPending 플래그와 startTransition 함수를 생성한다.
우선순위 관리: startTransition 함수는 상태 업데이트를 낮은 우선순위로 표시합니다.
상태 업데이트 처리
동기 처리: 일반적인 상태 업데이트는 즉시 처리되며, 필요한 경우 UI를 즉시 렌더링한다.
비동기 처리: 트랜지션으로 표시된 업데이트는 다른 중요한 작업을 방해하지 않도록 지연시킨다. 비동기 작업이 완료되면 상태가 업데이트되고 UI를 리렌더링한다.
최적화: 불필요한 리렌더링을 방지하기 위해 현재 상태와 새로운 상태를 비교한다.(같다면 bail out 처리)
트랜지션 관리: 여러 트랜지션을 효율적으로 관리하기 위해 레인(Lane) 시스템을 사용한다.
동기 vs 비동기 처리
동기 처리: dispatchSetStateInternal
함수를 사용하여 즉시 상태를 업데이트 하고 필요한 경우 리렌더링을 트리거한다.
비동기 처리: dispatchOptimisticSetState
사용하여 낙관적 업데이트를 수행한다.
이는 UI를 즉시 업데이트하지만, 실제 상태 변경은 비동기 작업이 완료된 후에 이루어진다.
이러한 매커니즘을 통해 useTransition은 복잡한 상태 업데이트를 효율적으로 관리하며, 사용자 경험을 크게 향상시킨다.
const updateQuantity = async (newQuantity) => {
return new Promise((resolve) =>
setTimeout(() => {
resolve(newQuantity);
}, 2000)
);
};
export default function App() {
const [quantity, setQuantity] = useState(1);
const [isPending, setIsPending] = useState(false);
const updateQuantityAction = async (newQuantity) => {
setIsPending(true);
const savedQuantity = await updateQuantity(newQuantity);
setIsPending(false);
setQuantity(savedQuantity);
};
return (
<div>
<h1>Checkout</h1>
<Item action={updateQuantityAction} />
<hr />
<Total quantity={quantity} isPending={isPending} />
</div>
);
}
수량을 빠르게 업데이트하면 요청이 진행 중일 때 계산 중이라는 UI가 표시되고, 수량을 클릭한 횟수만큼 총액이 여러번 업데이트 되는 것을 볼 수 있다.
const updateQuantity = async (newQuantity) => {
return new Promise((resolve) =>
setTimeout(() => {
resolve(newQuantity);
}, 2000)
);
};
export default function App() {
const [quantity, setQuantity] = useState(1);
const [isPending, startTransition] = useTransition();
const updateQuantityAction = async (newQuantity) => {
startTransition(async () => {
const savedQuantity = await updateQuantity(newQuantity);
startTransition(() => {
setQuantity(savedQuantity);
});
});
};
return (
<div>
<h1>Checkout</h1>
<Item action={updateQuantityAction} />
<hr />
<Total quantity={quantity} isPending={isPending} />
</div>
);
}
수량을 빠르게 업데이트하면 요청이 진행 중일 때 계산 중이라는 UI가 표시되고, 최종 요청이 완료된 후에만 총액이 업데이트되는 것을 확인할 수 있다.
업데이트가 Action 내에서 이루어지기 때문에 요청이 진행 중일 때도 수량은 계속해서 업데이트할 수 있다.
startTransition - React 공식문서(v18.3.1)
useTransition - React 공식문서(v18.3.1)
useTransition - React 공식문서(v19)
How does useTransition() work internally in React?
리액트 19에만 있는줄 알앗는데 18에도 있군요