[번역] React에서 useTransition()은 어떻게 동작하나요?
React Internals Deep Dive - EP8
![[번역] React에서 useTransition()은 어떻게 동작하나요?](https://cdn.hashnode.com/res/hashnode/image/upload/v1713789041776/b1384840-df7b-4a0a-ad16-118ed759a216.jpeg)
영문 블로그 글을 번역했습니다. 허가를 받으면 시리즈를 이어갈 예정입니다.
원문링크:https://jser.dev/2023-05-19-how-does-usetransition-work
ℹ️React Internals Deep Dive에피소드 8,유튜브에서 제가 설명하는 것을 시청해주세요.
⚠React@18.2.0기준, 최신 버전에서는 구현이 변경되었을 수 있습니다.
💬 역자 주석: Jser의 코멘트는 ❗❗로 표시 해뒀습니다.
그 외 주석은 리액트 소스 코드 자체의 주석입니다.
... 은 생략된 코드입니다.
1. useTransition()은 무엇을 하나요?
이 기능이 무엇인지 알고 싶으시다면 react.dev의 공식 문서에서 가장 잘 설명되어 있습니다.
사용 방법은 다음과 같습니다.
function TabContainer() {
const [isPending, startTransition] = useTransition();
const [tab, setTab] = useState('about');
function selectTab(nextTab) {
startTransition(() => {
setTab(nextTab);
});
}
}
startTransition() 안에 setState() 호출을 넣으면 업데이트가 전환(transition)으로 표시되므로 우선순위가 낮고 두 가지 주요 의미를 갖게 됩니다.
React 소스 코드를 살펴봄으로써 어떻게 작동하는지 알아봅시다.
2. useTransition()은 어떻게 동작하나요?
useTransition() 은 훅이며, 이 시리즈의 수많은 에피소드를 통해 어디서 찾을 수 있는지 잘 알고 있습니다. 네, ReactFiberHooks.js에 있습니다.
2.1 mountTransition()
mountTransition()은 최초 렌더링 때 사용됩니다.
function mountTransition() {
const stateHook = mountStateImpl((false: Thenable<boolean> | boolean));
// ❗❗ ↗ 이것은 mountState() 내부와 동일합니다,
// ❗❗ 즉 우리는 이것을 useState()의 내부 호출이 있는것과 동일하게 여길 수 있습니다.
// The `start` method never changes.
const start = startTransition.bind(
null,
currentlyRenderingFiber,
stateHook.queue,
true,
false,
);
const hook = mountWorkInProgressHook();
// ❗❗ ↗ startTransition()을 보유하는 기본 훅
hook.memoizedState = start;
return [false, start];
// ❗❗ 최초 isPending은 false 입니다.
}
위 코드는 const [isPending, startTransition] = useTransition()의 구문을 설명합니다.
또한 mountTransition()이 내부에 2개의 훅을 생성하는 것을 볼 수 있습니다:
isPending:boolean을 보유하는 state 훅startTransition()을 보유하는 또 다른 훅
이는 다음 섹션에서 중요합니다.
2.2 startTransition()은 2개의 state 업데이트 (하나의 normal 와 하나의 under transition)를 트리거합니다.
function startTransition<S>(
fiber: Fiber,
queue: UpdateQueue<S | Thenable<S>, BasicStateAction<S | Thenable<S>>>,
pendingState: S,
// ❗❗ ↖ true, mountTransition() 에서 바인딩된 값
finishedState: S,
// ❗❗ ↖ false, mountTransition() 에서 바인딩된 값
callback: () => mixed,
// ❗❗ ↖ 전달 받은 callback
options?: StartTransitionOptions,
): void {
const previousPriority = getCurrentUpdatePriority();
setCurrentUpdatePriority(
higherEventPriority(previousPriority, ContinuousEventPriority),
);
const prevTransition = ReactCurrentBatchConfig.transition;
if (enableAsyncActions) {
// We don't really need to use an optimistic update here, because we
// schedule a second "revert" update below (which we use to suspend the
// transition until the async action scope has finished). But we'll use an
// optimistic update anyway to make it less likely the behavior accidentally
// diverges; for example, both an optimistic update and this one should
// share the same lane.
dispatchOptimisticSetState(fiber, false, queue, pendingState);
// ❗❗ ↖ 낙관적 업데이트는 다른 주제이므로 지금은 건너뛰겠습니다.
} else {
ReactCurrentBatchConfig.transition = null;
// ❗❗ ↖ 여기서 ReactCurrentBatchConfig.transition이 null로 설정되어 있음을 주목하세요.
dispatchSetState(fiber, queue, pendingState);
// ❗❗ ↖ 이건 기본적으로 setStat(true)와 같습니다.
}
const currentTransition = (ReactCurrentBatchConfig.transition =
({}: BatchConfigTransition));
// ❗❗ 이제부터 ReactCurrentBatchConfig.transition이 Non-null로 설정되어 있음을 확인하십시오.
...
try {
if (enableAsyncActions) {
...
} else {
// Async actions are not enabled.
dispatchSetState(fiber, queue, finishedState);
// ❗❗ 이것이 false면 isPending이 false로 되돌아갑니다.
// ❗❗ setState(true) 은 이미 호출됐습니다, 그러니 여기 있는것 두 번째 setState() 호출입니다.
// ❗❗ 하지만 이번 호출은 좀 다른데, ReactCurrentBatchConfig.transition이 null이 아니라는것 때문입니다.
callback();
// ❗❗ ↖ 콜백은 null이 아닌 ReactCurrentBatchConfig.transition 아래에서 실행된다는 점에 유의하세요.
}
} catch (error) {
...
}
}
여기서 까다로운 부분은, setState()를 두 번 호출하는 것인데, setState()가 파이버에 다시 실행해야 한다는 플래그를 설정하고, 그리고 다음으로 루트에서 리-렌더링이 예약 되는 것을 알기 때문입니다. 이는 동기식이 아니므로 두 번의 setState() 호출이 모두 처리됩니다.
그러나 이 두 호출은 내부 데이터 구조에서 서로 다른 우선순위를 가지며, 서로 다른 Lanes을 가지고 있습니다. 리-렌더링할 때마다 가장 높은 Lane의 업데이트가 선택됩니다. (이에 대한 이해는 React 소스 코드에서 레인이란 무엇인가요? 를 참조하세요.
이 두 호출의 유일한 차이점은 ReactCurrentBatchConfig.transition뿐이므로 Lane 설정을 변경하려면 어딘가에서 이 호출을 사용해야 합니다.
2.3 requestUpdateLane()은 ReactCurrentBatchConfig.transition이 설정된 경우 Transition Lanes를 반환합니다.
setState() 내에서 requestUpdateLane() 이 호출됩니다.
function dispatchSetState<S, A>(
// ❗❗ ↗ 이것은 setState()의 내부 구현입니다.
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
): void {
const lane = requestUpdateLane(fiber);
// ❗❗ ↗ 업데이트의 레인(우선순위)은 고정되지 않고 동적으로 결정됩니다.
const update: Update<S, A> = {
lane, // ❗❗ lane
revertLane: NoLane,
action,
hasEagerState: false,
eagerState: null,
next: (null: any),
};
...
}
이제 requestUpdateLane() 자체를 살펴보겠습니다.
export function requestCurrentTransition(): Transition | null {
return ReactCurrentBatchConfig.transition;
// ❗❗ ReactCurrentBatchConfig.transition은 전역 플래그처럼 작동하여,
// ❗❗ 전환(transition)을 나타냅니다.
}
export function requestUpdateLane(fiber: Fiber): Lane {
...
const isTransition = requestCurrentTransition() !== NoTransition;
if (isTransition) {
const actionScopeLane = peekEntangledActionLane();
return actionScopeLane !== NoLane
? // We're inside an async action scope. Reuse the same lane.
actionScopeLane
: // We may or may not be inside an async action scope. If we are, this
// is the first update in that scope. Either way, we need to get a
// fresh transition lane.
requestTransitionLane();
// ❗❗ ↖ 만약 isTransition이면, Transition Lane을 반환합니다.
}
...
}
2.4 Transition Lanes는 낮은 우선순위의 레인입니다.
export function requestTransitionLane(): Lane {
// The algorithm for assigning an update to a lane should be stable for all
// updates at the same priority within the same event. To do this, the
// inputs to the algorithm must be the same.
//
// The trick we use is to cache the first of each of these inputs within an
// event. Then reset the cached values once we can be sure the event is
// over. Our heuristic for that is whenever we enter a concurrent work loop.
if (currentEventTransitionLane === NoLane) {
// All transitions within the same event are assigned the same lane.
currentEventTransitionLane = claimNextTransitionLane();
}
return currentEventTransitionLane;
}
export function claimNextTransitionLane(): Lane {
// Cycle through the lanes, assigning each new transition to the next lane.
// In most cases, this means every transition gets its own lane, until we
// run out of lanes and cycle back to the beginning.
const lane = nextTransitionLane;
nextTransitionLane <<= 1;
// ❗❗ ↖ 왼쪽 shifting(<<)으로 다음 transition lane으로 비트 이동합니다.
if ((nextTransitionLane & TransitionLanes) === NoLanes) {
nextTransitionLane = TransitionLane1;
// ❗❗ ↖ 만약 더이상의 transition lane이 없으면, 제일 처음 trasition lane을 사용합니다.
}
return lane;
}
export const NoLanes: Lanes = /* */ 0b0000000000000000000000000000000;
export const NoLane: Lane = /* */ 0b0000000000000000000000000000000;
export const SyncHydrationLane: Lane = /* */ 0b0000000000000000000000000000001;
export const SyncLane: Lane = /* */ 0b0000000000000000000000000000010;
export const InputContinuousHydrationLane: Lane = /* */ 0b0000000000000000000000000000100;
export const InputContinuousLane: Lane = /* */ 0b0000000000000000000000000001000;
export const DefaultHydrationLane: Lane = /* */ 0b0000000000000000000000000010000;
export const DefaultLane: Lane = /* */ 0b0000000000000000000000000100000;
export const SyncUpdateLanes: Lane = /* */ 0b0000000000000000000000000101010;
const TransitionLanes: Lanes = /* */ 0b0000000011111111111111110000000;
// ❗❗ ↗ 총 16개의 transition lane이 있으며,
// ❗❗ 이 레인들은 SyncLane 등 보다 우선순위가 낮습니다.
const TransitionLane1: Lane = /* */ 0b0000000000000000000000010000000;
const TransitionLane2: Lane = /* */ 0b0000000000000000000000100000000;
const TransitionLane3: Lane = /* */ 0b0000000000000000000001000000000;
const TransitionLane4: Lane = /* */ 0b0000000000000000000010000000000;
const TransitionLane5: Lane = /* */ 0b0000000000000000000100000000000;
const TransitionLane6: Lane = /* */ 0b0000000000000000001000000000000;
const TransitionLane7: Lane = /* */ 0b0000000000000000010000000000000;
const TransitionLane8: Lane = /* */ 0b0000000000000000100000000000000;
const TransitionLane9: Lane = /* */ 0b0000000000000001000000000000000;
const TransitionLane10: Lane = /* */ 0b0000000000000010000000000000000;
const TransitionLane11: Lane = /* */ 0b0000000000000100000000000000000;
const TransitionLane12: Lane = /* */ 0b0000000000001000000000000000000;
const TransitionLane13: Lane = /* */ 0b0000000000010000000000000000000;
const TransitionLane14: Lane = /* */ 0b0000000000100000000000000000000;
const TransitionLane15: Lane = /* */ 0b0000000001000000000000000000000;
const TransitionLane16: Lane = /* */ 0b0000000010000000000000000000000;
Transition 레인은 우선순위가 낮기 때문에 중단될 수 있습니다. 이는 동시성 모드의 핵심(💬gold 의역)이며, 어떻게 중단될 수 있는지 알아보려면 스케줄러 작동 방식을 참조하세요.
2.5 updateTransition()
이는 최초 마운트 후 useTransition()이 호출되는 경우입니다.
function updateTransition(): [
boolean,
(callback: () => void, options?: StartTransitionOptions) => void,
] {
const [booleanOrThenable] = updateState(false);
const hook = updateWorkInProgressHook();
const start = hook.memoizedState;
const isPending =
typeof booleanOrThenable === 'boolean'
? booleanOrThenable
: // This will suspend until the async action scope has finished.
useThenable(booleanOrThenable);
return [isPending, start];
}
따라서 코드를 보면, 기본적으로 훅에서 데이터를 반환하는 것을 알 수 있습니다.
2.6 startTransition()
startTransition()은 컴포넌트 외부에서 사용할 수 있는 전역 명령형 API입니다.
export function startTransition(
scope: () => void,
options?: StartTransitionOptions,
) {
const prevTransition = ReactCurrentBatchConfig.transition;
ReactCurrentBatchConfig.transition = ({}: BatchConfigTransition);
// ❗❗ ↗ transition을 위한 글로벌 플래그만을 설정합니다.
const currentTransition = ReactCurrentBatchConfig.transition;
try {
scope();
} finally {
ReactCurrentBatchConfig.transition = prevTransition;
}
}
이건 꽤 간단합니다, 마치 isPending이 없는 useTransition() 함수의 일부 같습니다.
3. 학습 내용을 통해 데모를 더 잘 이해하기
공식 문서에 나열된 사용 사례를 살펴보고 어떻게 이게 가능한지 알아보겠습니다.
3.1 사용 사례 1 - state 업데이트를 논-블록 transition으로 표시하기
useTransition()을 사용하지 않으면 다음과 같은 일이 발생합니다.
'Posts(slow)'를 클릭합니다:
SyncLane(클릭 이벤트이므로
DiscreteEventPriority에서 매핑됨)의 업데이트가 파이버에 설정됩니다.루트에서 리-렌더링이 예약됩니다.
루트에서 리-렌더링합니다:
SyncLane이기 때문에 동시성 모드가 아니며, React는 모든 것을 한 번에 렌더링하려고 시도합니다.
PostList 렌더링이 차단되어, 다른 버튼 클릭이 작동하지 않습니다.
위의 설명을 이해하기 위해 리-렌더링 예약이 어떻게 작동하는지 보여주는 다음 코드를 읽어보겠습니다.
ℹ React 스케줄러는 어떻게 작동하는가? 에서도 관련 주제를 설명합니다.
// Use this function to schedule a task for a root. There's only one task per
// root; if a task was already scheduled, we'll check to make sure the priority
// of the existing task is the same as the priority of the next level that the
// root has work on. This function is called on every update, and right before
// exiting a task.
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
const existingCallbackNode = root.callbackNode;
// Check if any lanes are being starved by other work. If so, mark them as
// expired so we know to work on those next.
markStarvedLanesAsExpired(root, currentTime);
// Determine the next lanes to work on, and their priority.
const nextLanes = getNextLanes(
// ❗❗ ↗ getNextLanes() 은 최우선순위 레인을 반환 합니다.
// ❗❗ 만약 SyncLane과 Transition Lanes들이 같이 있으면, SyncLane이 선택될 것입니다.
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
);
if (nextLanes === NoLanes) {
// Special case: There's nothing to work on.
if (existingCallbackNode !== null) {
cancelCallback(existingCallbackNode);
}
root.callbackNode = null;
root.callbackPriority = NoLane;
return;
}
...
if (existingCallbackNode != null) {
// Cancel the existing callback. We'll schedule a new one below.
cancelCallback(existingCallbackNode);
// ❗❗ ↖ 이게 중요합니다!!
// ❗❗ 만약 리-렌더링이 끝나지 않으면, 우리는 새로운 것을 예약하고,
// ❗❗ 오래된 것은 취소되게 됩니다.
// ❗❗ 이게 바로 중단(interruption)이 발생하는 방식입니다.
// Schedule a new callback.
let newCallbackNode;
if (newCallbackPriority === SyncLane) {
// Special case: Sync React callbacks are scheduled on a special
// internal queue
if (root.tag === LegacyRoot) {
scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));
} else {
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
// ❗❗ ↗ SyncLane의 경우, 조정(reconciliation)은 동시성 모드가 아닌 동기화 작업입니다.
// ❗❗ 즉, 메인 스레드에 양보(yield)하지 않으며 잠재적으로 차단이 될 수 있습니다.
}
if (supportsMicrotasks) {
scheduleMicrotask(() => {
// In Safari, appending an iframe forces microtasks to run.
// https://github.com/facebook/react/issues/22459
// We don't support running callbacks in the middle of render
// or commit so we need to check against that.
if (
(executionContext & (RenderContext | CommitContext)) ===
NoContext
) {
// Note that this would still prematurely flush the callbacks
// if this happens outside render or commit phase (e.g. in an event).
flushSyncCallbacks();
}
});
} else {
// Flush the queue in an Immediate task.
scheduleCallback(ImmediateSchedulerPriority, flushSyncCallbacks);
}
newCallbackNode = null;
} else {
let schedulerPriorityLevel;
switch (lanesToEventPriority(nextLanes)) {
case DiscreteEventPriority:
schedulerPriorityLevel = ImmediateSchedulerPriority;
break;
case ContinuousEventPriority:
schedulerPriorityLevel = UserBlockingSchedulerPriority;
break;
case DefaultEventPriority:
schedulerPriorityLevel = NormalSchedulerPriority;
break;
case IdleEventPriority:
schedulerPriorityLevel = IdleSchedulerPriority;
break;
default:
schedulerPriorityLevel = NormalSchedulerPriority;
break;
}
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
// ❗❗ 만약 SyncLane이 아니면, 동시성 모드가 사용되고,
// ❗❗ 조정은 때떄로 메인 스레드에 양보(yield)합니다.
// ❗❗ UI가 익터렉티브 해지고 이전 리-렌더링을 취소할 수 있습니다.
);
}
root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;
}
위의 지식이 있으면, useTransition()을 사용할 때 데모가 다르게 동작하는 이유를 쉽게 이해할 수 있습니다.
"Posts(slow)"를 클릭합니다.
TransitionLane(SyncLane 아님)의 업데이트가 파이버에 설정됩니다.
루트에서 리-렌더링이 스케줄링됩니다.
루트에서 리-렌더링합니다:
SyncLane이 아니기 때문에 동시 모드가 사용되며, PostList를 계속 렌더링합니다.
비록 PostList가 무겁지만, 동시 모드는 버튼을 클릭할 수 있도록 때때로 메인 스레드에 양보(yield)합니다.
"Contact"를 클릭합니다.
TransitionLane의 업데이트가 파이버에 다시 설정됩니다.
루트에서 리-렌더링이 예약되었지만, 기존 리-렌더링이 아직 완료되지 않았으므로 취소합니다.
이전 리렌더링이 중단되고, 새로고침 리-렌더링이 발생하여, 최신 state가 렌더링됩니다.
3.2 사용 사례 2 - transition에서 부모 컴포넌트 업데이트하기
사용 사례 1에서 트랜지션은 스케줄러의 작업에 대한 용어이므로 특정 파이버가 아닌 전체 리-렌더링에 관한 것이므로 부모에 대한 useTransition()도 작동합니다.
실제로 글로벌 startTransition() API를 사용하면 어디서나 전환(transition)을 트리거할 수 있습니다.
3.3 사용 사례 3 - transition 중에 보류 중인 시각적 state 표시
2.2절에서 useTransition() 내부에는 의미상 두 번의 setState() 호출이 있는데, 첫 번째 호출은 트랜지션이 아니므로 SyncLane이고 isPending을 유지하는 state가 성공적으로 설정된다는 점을 기억하세요.
이것이 바로 indicator(표시기)를 볼 수 있는 이유입니다.
3.4 사용 사례 4 - 원치 않는 로딩 표시기 방지하기
이것은 더 흥미로운데, 위의 사용 사례들과는 다른 의미를 가지고 있습니다. React.dev에 더 자세한 설명이 있습니다.
Suspense 콘텐츠를 폴백으로 전환해야 하는 시나리오에 대한 내용이므로, Suspense와 관련된 코드를 읽어보겠습니다.
ℹ 서스펜스 자체에 대한 자세한 내용은 동시 모드에서 서스펜스가 내부적으로 작동하는 방식을 참조하세요.
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
switch (workInProgress.tag) {
...
case SuspenseComponent: {
popSuspenseContext(workInProgress);
const nextState: null | SuspenseState = workInProgress.memoizedState;
}
const nextDidTimeout = nextState !== null;
const prevDidTimeout =
current !== null &&
(current.memoizedState: null | SuspenseState) !== null;
...
// If the suspended state of the boundary changes, we need to schedule
// a passive effect, which is when we process the transitions
if (nextDidTimeout !== prevDidTimeout) {
if (enableTransitionTracing) {
const offscreenFiber: Fiber = (workInProgress.child: any);
offscreenFiber.flags |= Passive;
}
// If the suspended state of the boundary changes, we need to schedule
// an effect to toggle the subtree's visibility. When we switch from
// fallback -> primary, the inner Offscreen fiber schedules this effect
// as part of its normal complete phase. But when we switch from
// primary -> fallback, the inner Offscreen fiber does not have a complete
// phase. So we need to schedule its effect here.
//
// We also use this flag to connect/disconnect the effects, but the same
// logic applies: when re-connecting, the Offscreen fiber's complete
// phase will handle scheduling the effect. It's only when the fallback
// is active that we have to do anything special.
if (nextDidTimeout) {
const offscreenFiber: Fiber = (workInProgress.child: any);
offscreenFiber.flags |= Visibility;
// TODO: This will still suspend a synchronous tree if anything
// in the concurrent tree already suspended during this render.
// This is a known bug.
if ((workInProgress.mode & ConcurrentMode) !== NoMode) {
// TODO: Move this back to throwException because this is too late
// if this is a large tree which is common for initial loads. We
// don't know if we should restart a render or not until we get
// this marker, and this is too late.
// If this render already had a ping or lower pri updates,
// and this is the first time we know we're going to suspend we
// should be able to immediately restart from within throwException.
const hasInvisibleChildContext =
current === null &&
(workInProgress.memoizedProps.unstable_avoidThisFallback !==
true ||
!enableSuspenseAvoidThisFallback);
if ( // ❗❗ 여기서 부터 아래 ❗❗ 까지에 대한 설명
hasInvisibleChildContext ||
hasSuspenseContext(
suspenseStackCursor.current,
(InvisibleParentSuspenseContext: SuspenseContext),
)
) {
// If this was in an invisible tree or a new render, then showing
// this boundary is ok.
renderDidSuspend();
} else {
// Otherwise, we're going to have to hide content so we should
// suspend for longer if possible.
renderDidSuspendDelayIfPossible();
// ❗❗ ↖ 이름에서 알 수 있듯이 React는 폴백으로 돌아가는 것을 피하려고 시도 합니다.
}
}
}
}
}
}
}
export function renderDidSuspendDelayIfPossible(): void {
if (
workInProgressRootExitStatus === RootInProgress ||
workInProgressRootExitStatus === RootSuspended ||
workInProgressRootExitStatus === RootErrored
) {
workInProgressRootExitStatus = RootSuspendedWithDelay;
// ❗❗ ↖ 이것은 전체 리-렌더링 결과를 저장하는 전역 변수입니다.
}
...
}
따라서 React는 콘텐츠가 이미 공개된 후에 Suspense 폴백이 렌더링되는 것을 좋아하지 않으며, 이는 매우 합리적입니다.
RootSuspendedWithDelay는 React가 DOM에 변경 사항을 커밋하기 직전에 검사하는 상태입니다.
function finishConcurrentRender(
root: FiberRoot,
exitStatus: RootExitStatus,
finishedWork: Fiber,
lanes: Lanes,
) {
// TODO: The fact that most of these branches are identical suggests that some
// of the exit statuses are not best modeled as exit statuses and should be
// tracked orthogonally.
switch (exitStatus) {
case RootInProgress:
case RootFatalErrored: {
throw new Error('Root did not complete. This is a bug in React.');
}
case RootSuspendedWithDelay: {
if (includesOnlyTransitions(lanes)) {
// This is a transition, so we should exit without committing a
// placeholder and without scheduling a timeout. Delay indefinitely
// until we receive more data.
markRootSuspended(root, lanes);
return;
// ❗❗ ↗ 여기서 모든 레인들이 RootSuspendedWithDelay 아래의 트랜지션인 경우 반환되며,
// ❗❗ 의미상으로는 Suspense 폴백으로 콘텐츠를 다시 깜박이는 것을 좋아하지 않으며,
// ❗❗ 우리는 이미 낮은 우선순위(트랜지션)인 상태라는 것을 알고 있습니다.
}
// Commit the placeholder.
break;
}
case RootErrored:
case RootSuspended:
case RootCompleted: {
break;
}
default: {
throw new Error('Unknown root exit status.');
}
}
...
commitRootWhenReady(
root,
finishedWork,
workInProgressRootRecoverableErrors,
workInProgressTransitions,
lanes,
);
}
여기서 마법을 볼 수 있습니다. 트랜지션 중에 React가 Suspense 콘텐츠를 폴백으로 되돌려야 하는 상황이 발생하면, React는 이를 무시하고 변경 사항을 DOM에 커밋하지 않습니다. 그리고 Suspense가 작동하는 방식 때문에, 던져진(throw) thenable이 해결(resolve)되면 올바른 콘텐츠를 렌더링하는 리-렌더링이 예약됩니다.
4. 요약
useTransition()은 특정 업데이트의 우선순위를 낮출 수 있는 강력한 도구입니다. 클릭과 같은 이벤트는 실제로 동시성 모드에서 업데이트를 트리거하기 때문에 useTransition()은 동시성 모드의 명시적 opt-in 역할을 합니다.
업데이트 차단을 피하는 것보다 더 나은 사용 사례는 불필요한 Suspense 폴백을 피하는것이므로 가능한 한 많이 사용해야 한다고 생각합니다.
About the author
Software engineer for web tech. Interested in sustainable growth as software engineer.
Comments
Join the discussionNo comments yet. Be the first to comment.
React Internals Deep Dive 번역 시리즈
[번역] 리액트 내부 작동원리에 대한 JSer의 시리즈입니다. https://jser.dev/series/react-source-code-walkthrough

