갈고리 소재 고갈
이전까지는 상태 자체거나, 상태와 직접적으로 연관이 있는 훅들을 살펴보았다. 이번에 작성할 useEffect
는 상태와는 별개로 존재하는 훅이다. 내부 동작도 기존의 상태 기반 훅들과는 다르므로 한 편을 모두 할애해서 알아본다.
내부 원리를 살펴보기에 앞서 겉햝기를 먼저 해보자. 이름에서 알 수 있듯이 useEffect
는 Effect를 위한 훅이다. 리액트에서 Effect란 컴퓨터 용어에서 사용하는 side effect를 의미하는데, 이는 입력값을 토대로 결과값을 도출하는 과정 외에 관찰할 수 있는 부가 효과를 말한다. 리액트의 함수 컴포넌트에 대입해보면 props와 state에 따라 JSX를 반환하는 렌더링이 주요 과정이고, 렌더링 외부에서 이루어지는 모든 작업이 side effect이다.
useEffect is a React Hook that lets you synchronize a component with an external system.
공식 문서에서 useEffect
를 외부 시스템과 엮어서 소개하는 이유가 여기 있다. 외부 시스템은 보통 DOM, 네트워크 통신, 서드파티 프로그램 등이 해당된다.
위 소개글에는 외부 시스템 말고도 동기화라는 단어가 등장한다. useEffect
의 목적은 렌더링 과정과 외부 시스템의 동작을 동기화시키는 것이다. 동기화가 필요한 이유는 외부 시스템을 React의 props로 조작할 수 없기 때문이다.
React로 만들어진 컴포넌트는 보통 상위 컴포넌트에서 받는 속성으로 동작을 제어한다. 모달을 예시로 들자면 isOpen
로 이름 지은 속성을 넘겨서 열고 닫도록 구현해본 경험이 있을 것이다. 이와 달리 외부 시스템은 별개의 API로 동작한다. alert()
가 대표적 예시인데, 동작을 위해선 API 코드를 직접 실행해주어야 한다.
만약 속성으로 외부 시스템을 제어하기 위해 하위 컴포넌트 렌더링 코드에 API를 사용한다면 렌더링 과정이 순수하지 않게되어 의도치 않은 부작용이 나타난다.
때문에 외부 시스템의 API는 이벤트 핸들러에서 처리하기도 한다. 외부 시스템의 동작은 유저의 인터렉션에 유발되는 경우가 잦으므로 적절하다. 하지만 때로는 렌더링이 선행되어야 하는 경우가 있다. 채팅 기능을 생각해보면 외부 시스템인 채팅 서버 연결은 어떤 버튼 클릭이 아닌 채팅방 컴포넌트의 렌더링과 함께 이루어져야 한다. 이처럼 렌더링의 결과물에게 side effect를 촉발할 수 있도록 하는 훅이 useEffect
이다.
리액트 내부에 존재하는 렌더링이 리액트 외부 시스템에 영향을 미칠 수 있도록 하는 탈출로 역할을 하므로 Escape Hatch로 소개하기도 한다.
내부 동작이나 목적은 차치하고 내부 코드를 렌더링 이후에 실행한다는 결과로 인해 useEffect
는 지연실행을 위한 용도로 사용되기도 한다. 하지만 외부 시스템과 동기화라는 확고한 목적을 가진 훅인 만큼 이를 보조하도록 동작이 짜여있다. 실행 시점이나 clean-up 기능, 개발 모드 시 두번 실행 등이 해당된다. 때문에 동기화 이외의 용도로 useEffect
를 사용하면 성능에 악영향을 주거나 에러가 나기 쉽다.
주요 오남용 케이스를 바로 잡는 방법은 리액트 공식문서에서 소개하고 있다.
리액트 공식문서의 첫번째 탭인
Learn React
에 한 문서를 차지하는 만큼 React 팀에서 중요하게 생각하는 내용임을 유추할 수 있다.
방금 useEffect
는 렌더링과 외부 시스템을 동기화시키는 훅이라고 소개했다. 그런데 리액트의 훅 목록을 보면 더 노골적인 이름을 가진 훅을 볼 수 있다. 바로 useSyncExternalStore
이다. 시스템은 아니고 스토어이긴 하지만 '외부'와 '동기화'라는 키워드를 포함하고 있어 해당 용도에 더 적합한 훅이 아닐까 기대하게 된다. 하지만 useSyncExternalStore
는 useEffect
와는 정반대의 역할을 가진다.
useEffect
는 리액트의 렌더링 과정에 맞게 외부 시스템이 동작하도록 하는 훅이라고 했다. useSyncExternalStore
는 반대로 외부 시스템의 상태에 따라 리액트의 렌더링이 작동하도록 한다. 동기화의 방향이 반대라고 할 수 있다.
사실 주된 목적은 외부 시스템의 상태를 리액트의 concurrent 렌더링에 흡수시키는 것이다.
처음은 다른 훅들과 마찬가지로 훅 구현체에서 시작한다. 업데이트 구현체가 동작 특성을 잘 보여준다.
/**@ updateEffect => updateEffectImpl **/
function updateEffectImpl(
fiberFlags: Flags,
hookFlags: HookFlags,
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const effect: Effect = hook.memoizedState;
const inst = effect.inst;
// currentHook is null on initial mount when rerendering after a render phase
// state update or for strict mode.
if (currentHook !== null) {
if (nextDeps !== null) {
const prevEffect: Effect = currentHook.memoizedState;
const prevDeps = prevEffect.deps;
if (areHookInputsEqual(nextDeps, prevDeps)) {
hook.memoizedState = pushEffect(hookFlags, create, inst, nextDeps);
return;
}
}
}
currentlyRenderingFiber.flags |= fiberFlags;
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
inst,
nextDeps,
);
}
우리가 useEffect
에 인자로 전달하는 callback과 의존성 배열은 hook의 memoizedState
에 effect
라는 객체로 저장된다.
상태가 아닌데 memoizedState?
지난 시리즈에선
memoizedState
가 상태 관련 훅이 저장되는 속성이라고 소개했다. 그런데useEffect
에서도 사용하고 있는 걸 보면 이상하게 느껴진다. 일단memoizedState
는 두 개가 존재한다. 하나는 fiber 객체의 속성이고, 다른 하나는 hook 객체의 속성이다. fiber의 것은 hook 객체가 Linked List 형태로 저장되는 시작점 역할을 한다. hook의 것은 연결된 훅이 실행된 결과물을 저장하는 역할을 한다.useEffect
도effect
를 결과물로 생성하므로 hook의memoizedState
에 저장된다. 다만 hook 객체 내부에서memoizedState
와 함께 있는queue
같은 상태관련 속성은 사용하지 않는다.
직전 렌더링 때 저장된 의존성 배열과 비교해서 다르면 HookHasEffect
라는 flag를 붙여서 저장한다. pushEffect
는 fiber의 updateQueue
속성에 Circular Linked List 형태로 effect
를 추가하고 추가한 effect
를 반환한다.
function pushEffect(
tag: HookFlags,
inst: EffectInstance,
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): Effect {
const effect: Effect = {
tag,
create,
deps,
inst,
// Circular
next: (null: any),
};
let componentUpdateQueue: null | FunctionComponentUpdateQueue =
(currentlyRenderingFiber.updateQueue: any);
if (componentUpdateQueue === null) {
componentUpdateQueue = createFunctionComponentUpdateQueue();
currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
}
const lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
return effect;
}
pushEffect
가 실행되는 시점은 렌더링 중이므로 인자로 전달한 콜백 함수인 create
는 실행하지 않은 채로 effect
안에 저장한다. 함께 저장되는 inst
에는 클린업 함수를 위한 저장 공간이 있는데 create
가 실행되지 않았으므로 지금은 undefined
값을 가진다.
effect
를memoizedState
와updateQueue
에 동시에 저장하고 있다. 이후 살펴보겠지만updateQueue
에 저장된effect
는 commit 단계 이후에 소비되면서 초기화되는 차이점이 있다.
function updateEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
updateEffectImpl(PassiveEffect, HookPassive, create, deps);
}
effect
객체를 추가하는 것과 별개로 fiber에는 PassiveEffect
, effect에는 HookPassive | HookHasEffect
라는 flag를 설정한다. fiber에 설정된 PassiveEffect
는 렌더링 작업 직후에 실행되는 completeUnitOfWork
에서 root fiber로 전달된다.
PassiveEffect
는ReactFiberHooks.js
파일 내 구분을 위한 alias로 원래 명칭은Passive
다. 다른 파일에서 사용될 때 참고할 것.
/** completeUnitOfWork -> completeWork **/
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
/**@ 생략 **/
switch (workInProgress.tag) {
case LazyComponent:
case SimpleMemoComponent:
case FunctionComponent:
case ForwardRef:
case Fragment:
case Mode:
case Profiler:
case ContextConsumer:
case MemoComponent:
bubbleProperties(workInProgress);
return null;
function bubbleProperties(completedWork: Fiber) {
/**@ 생략 **/
let subtreeFlags = NoFlags;
/**@ 생략 **/
} else {
let child = completedWork.child;
while (child !== null) {
/**@ 생략 **/
subtreeFlags |= child.subtreeFlags;
subtreeFlags |= child.flags;
// Update the return pointer so the tree is consistent. This is a code
// smell because it assumes the commit phase is never concurrent with
// the render phase. Will address during refactor to alternate model.
child.return = completedWork;
child = child.sibling;
}
}
completedWork.subtreeFlags |= subtreeFlags;
bubbleProperty
는 자식 fiber들의 flag를 부모 fiber로 가져오는 역할을 한다. 이걸 root까지 끌어올리는 건 completeWork
를 사용하는 completeUnitOfWork
이 한다.
function completeUnitOfWork(unitOfWork: Fiber): void {
// Attempt to complete the current unit of work, then move to the next
// sibling. If there are no more siblings, return to the parent fiber.
let completedWork: Fiber = unitOfWork;
do {
/**@ 생략 **/
// The current, flushed, state of this fiber is the alternate. Ideally
// nothing should rely on this, but relying on it here means that we don't
// need an additional field on the work in progress.
const current = completedWork.alternate;
const returnFiber = completedWork.return;
let next; /**@ 일부 생략 **/
next = completeWork(current, completedWork, entangledRenderLanes);
if (next !== null) {
// Completing this fiber spawned new work. Work on that next.
workInProgress = next;
return;
}
const siblingFiber = completedWork.sibling;
if (siblingFiber !== null) {
// If there is more work to do in this returnFiber, do that next.
workInProgress = siblingFiber;
return;
}
// Otherwise, return to the parent
// $FlowFixMe[incompatible-type] we bail out when we get a null
completedWork = returnFiber;
// Update the next thing we're working on in case something throws.
workInProgress = completedWork;
} while (completedWork !== null);
// We've reached the root.
if (workInProgressRootExitStatus === RootInProgress) {
workInProgressRootExitStatus = RootCompleted;
}
}
completeWork
는 Suspense와 관련된 경우가 아니면 null
을 리턴한다. 따라서 형제 fiber가 있다면 workInProgress
를 형제 노드로 바꾸고, 없다면 부모 fiber로 순회한다.
function performUnitOfWork(unitOfWork: Fiber): void {
// The current, flushed, state of this fiber is the alternate. Ideally
// nothing should rely on this, but relying on it here means that we don't
// need an additional field on the work in progress.
const current = unitOfWork.alternate;
let next; /**@ 일부 생략 **/
next = beginWork(current, unitOfWork, entangledRenderLanes);
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// If this doesn't spawn new work, complete the current work.
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
}
렌더링 작업이 수행되는 beginWork
는 자식 fiber가 리렌더링이 필요한 상태라면 자식 fiber를, 아니면 null
을 리턴한다. 이 메커니즘을 통해 렌더링이 일어나는 fiber 트리를 순회하면서 root로 effect의 flag를 전달한다.
추가한 effect와 flag는 커밋 단계에서 소비한다. 커밋 단계의 작업을 담당하는 건 finishConcurrentRender
이다.
export function performWorkOnRoot(
root: FiberRoot,
lanes: Lanes,
forceSync: boolean,
): void {
/**@ 생략 **/
let exitStatus = shouldTimeSlice
? renderRootConcurrent(root, lanes)
: renderRootSync(root, lanes, true);
/** renderRoot** -> workLoop** -> performUnitOfWork **/
/**@ 생략 **/
do {
/**@ 생략 **/
const finishedWork: Fiber = (root.current.alternate: any);
/**@ 생략: Suspense 관련 작업 **/
// We now have a consistent tree. The next step is either to commit it,
// or, if something suspended, wait to commit it after a timeout.
finishConcurrentRender(
root,
exitStatus,
finishedWork,
lanes,
renderEndTime,
);
}
break;
} while (true);
ensureRootIsScheduled(root);
}
Suspense 작업 중이 아니라면 finishConcurrentRender
는 commitRoot
로 바로 연결된다.
function commitRootImpl(
root: FiberRoot,
/**@ 생략 **/
) {
/**@ 생략 **/
// If there are pending passive effects, schedule a callback to process them.
// Do this as early as possible, so it is queued before anything else that
// might get scheduled in the commit phase. (See #16714.)
if (
(finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||
(finishedWork.flags & PassiveMask) !== NoFlags
) {
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects(true);
// This render triggered passive effects: release the root cache pool
// *after* passive effects fire to avoid freeing a cache pool that may
// be referenced by a node in the tree (HostRoot, Cache boundary etc)
return null;
});
}
}
/**@ 생략: 타 commit 단계 실행 **/
PassiveMask
는 flag의 비트 중 PassiveEffect
이외 부분을 마스킹한다. PassiveEffect
flag가 있다면 flushPassiveEffects
를 예약한다. scheduleCallback
은 setTimeout
등을 활용해 작업을 예약한다.
function flushPassiveEffectsImpl(wasDelayedCommit: void | boolean) {
/**@ 생략 **/
commitPassiveUnmountEffects(root.current);
commitPassiveMountEffects(
root,
root.current,
lanes,
transitions,
pendingPassiveEffectsRenderEndTime,
);
/**@ 생략 **/
return true;
}
cleanup 작업을 위해 Unmount를 먼저 실행한다.
/**@ commitPassiveUnmountEffects -> commitPassiveUnmountOnFiber **/
function commitPassiveUnmountOnFiber(finishedWork: Fiber): void {
const prevEffectStart = pushComponentEffectStart();
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
recursivelyTraversePassiveUnmountEffects(finishedWork);
if (finishedWork.flags & Passive) {
commitHookPassiveUnmountEffects(
finishedWork,
finishedWork.return,
HookPassive | HookHasEffect,
);
}
break;
}
/**@ 생략 **/
재귀 호출을 통해 전체 트리의 effect를 처리한다. 재귀가 먼저 호출되므로 자식 fiber가 먼저 처리된다.
function recursivelyTraversePassiveUnmountEffects(parentFiber: Fiber): void {
/**@ 생략 **/
if (parentFiber.subtreeFlags & PassiveMask) {
let child = parentFiber.child;
while (child !== null) {
commitPassiveUnmountOnFiber(child);
child = child.sibling;
}
}
}
각 fiber에선 useEffect
에서 updateQueue
에 추가한 effect
객체를 소비한다.
/**@ commitHookPassiveUnmountEffects -> commitHookEffectListUnmount **/
export function commitHookEffectListUnmount(
flags: HookFlags,
finishedWork: Fiber,
nearestMountedAncestor: Fiber | null,
) {
try {
const updateQueue: FunctionComponentUpdateQueue | null =
(finishedWork.updateQueue: any);
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
if ((effect.tag & flags) === flags) {
// Unmount
const inst = effect.inst;
const destroy = inst.destroy;
if (destroy !== undefined) {
inst.destroy = undefined;
safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy);
}
}
effect = effect.next;
} while (effect !== firstEffect);
}
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
HookPassive | HookHasEffect
flag가 설정된 effect
의 destory
를 실행하고 undefined
로 초기화한다.
mount에서는 useEffect
에서 create
에 저장했던 콜백 함수를 실행한다.
export function commitHookEffectListMount(
flags: HookFlags,
finishedWork: Fiber,
) {
/**@ 생략: Unmount와 동일 **/
do {
if ((effect.tag & flags) === flags) {
// Unmount
const create = effect.create;
const inst = effect.inst;
destroy = create();
inst.destroy = destroy;
effect = effect.next;
} while (effect !== firstEffect);
/**@ 생략: Unmount와 동일 **/
콜백 함수는 cleanup 함수를 반환하므로 destroy
에는 cleanup 함수가 저장된다. destroy
가 mount에서 저장되고 unmount에서 소비되므로 이전 렌더링 시점의 effect
를 정리할 수 있다. 또한 destroy
가 실행 후 초기화되면서 혹시 모를 중복 실행 가능성을 제거한다.
cleanup은 다음 effect 실행 전 뿐 아니라 연결된 컴포넌트가 unmount될 때도 실행되어야 한다. unmount는 변경점을 host 트리에 반영하는 Mutation
단계에서 처리한다.
/**@ commitMutationEffects -> commitMutationEffectsOnFiber -> recursivelyTraverseMutationEffects **/
function recursivelyTraverseMutationEffects(
root: FiberRoot,
parentFiber: Fiber,
lanes: Lanes,
) {
// Deletions effects can be scheduled on any fiber type. They need to happen
// before the children effects have fired.
const deletions = parentFiber.deletions;
if (deletions !== null) {
for (let i = 0; i < deletions.length; i++) {
const childToDelete = deletions[i];
commitDeletionEffects(root, parentFiber, childToDelete);
}
}
deletions
에는 제거할 자식 fiber의 목록이 저장되어있다.
/**@ commitDeletionEffects -> commitDeletionEffectsOnFiber **/
function commitDeletionEffectsOnFiber(
finishedRoot: FiberRoot,
nearestMountedAncestor: Fiber,
deletedFiber: Fiber,
) {
switch (deletedFiber.tag) {
/**@ 생략 **/
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent: {
if (
enableHiddenSubtreeInsertionEffectCleanup ||
!offscreenSubtreeWasHidden
) {
// TODO: Use a commitHookInsertionUnmountEffects wrapper to record timings.
commitHookEffectListUnmount(
HookInsertion,
deletedFiber,
nearestMountedAncestor,
);
}
/**@ 생략 **/
destroy
를 실행하던 commitHookEffectListUnmount
를 호출하고 있다.
리액트의 훅을 찾아봤다면 useEffect
와 아주 유사한 기능을 하는 훅이 있다는 걸 알고있을 것이다. useLayoutEffect
는 useEffect
와 달리 브라우저의 repaint 전에 실행되기 때문에 브라우저의 깜빡임을 제거하는 용도로 사용된다.
두 훅의 차이점은 실행 시점인데 이는 설정하는 flag의 차이로 유발된다.
function mountLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return mountEffectImpl(UpdateEffect | LayoutStaticEffect, HookLayout, create, deps);
}
function updateLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return updateEffectImpl(UpdateEffect, HookLayout, create, deps);
}
구현체는 동일하지만 flag가 useEffect
는 PassiveEffect
와 HookPassive
였고, useLayoutEffect
는 UpdateEffect
와 HookLayout
이다.
fiber에 달리는 UpdateEffect
는 commit 단계 진입 시 사용된다. 위 commitRootImpl
에서 생략되었던 부분에 해당한다.
function commitRootImpl(
root: FiberRoot,
/**@ 생략 **/
) {
/**@ 생략: PassiveEffect 처리 **/
// Check if there are any effects in the whole tree.
const subtreeHasEffects =
(finishedWork.subtreeFlags &
(BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !==
NoFlags;
const rootHasEffect =
(finishedWork.flags &
(BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !==
NoFlags;
if (subtreeHasEffects || rootHasEffect) {
/**@ 생략 **/
commitBeforeMutationEffects(
root,
finishedWork,
);
commitMutationEffects(root, finishedWork, lanes);
// The work-in-progress tree is now the current tree.
root.current = finishedWork;
commitLayoutEffects(finishedWork, root, lanes);
/**@ 생략 **/
// Tell Scheduler to yield at the end of the frame, so the browser has an
// opportunity to paint.
requestPaint();
/**@ 생략 **/
}
UpdateEffect
는 BeforeMutationMask | MutationMask | LayoutMask
에 다 해당되어서 if 문 내부로 진입할 수 있다. 내부의 세 가지 commit 단계 모두 requestPaint
이전에 동기적으로 실행되므로 scheduleCallback
을 통해 예약되는 PassiveEffect보다 먼저 처리된다.
이름부터가 Layout인 만큼 주 작업은 commitLayoutEffects
에서 처리된다.
/**@ commitLayoutEffects -> commitLayoutEffectsOnFiber **/
function commitLayoutEffectOnFiber(
finishedRoot: FiberRoot,
current: Fiber | null,
finishedWork: Fiber,
committedLanes: Lanes,
): void {
/**@ 생략 **/
const flags = finishedWork.flags;
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
recursivelyTraverseLayoutEffects(
finishedRoot,
finishedWork,
committedLanes,
);
if (flags & Update) {
commitHookLayoutEffects(finishedWork, HookLayout | HookHasEffect);
}
break;
export function commitHookLayoutEffects(
finishedWork: Fiber,
hookFlags: HookFlags,
) {
// At this point layout effects have already been destroyed (during mutation phase).
// This is done to prevent sibling component effects from interfering with each other,
// e.g. a destroy function in one component should never override a ref set
// by a create function in another component during the same commit.
commitHookEffectListMount(hookFlags, finishedWork);
}
effect
의 create
를 실행하던 commitHookEffectListMount
를 hookFlags
를 달리해서 사용하는 걸 볼 수 있다. PassiveEffect와 다르게 commitHookEffectListUnmount
가 없는데, 주석에 설명된 것처럼 형제 컴포넌트간 간섭을 막기 위해 Mutation 단계에서 처리한다.
/** commitMutationEffects -> commitMutationEffectsOnFiber **/
function commitMutationEffectsOnFiber(
finishedWork: Fiber,
root: FiberRoot,
lanes: Lanes,
) {
const current = finishedWork.alternate;
const flags = finishedWork.flags;
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
if (flags & Update) {
/**@ 생략 **/
commitHookLayoutUnmountEffects(
finishedWork,
finishedWork.return,
HookLayout | HookHasEffect,
);
}
break;
export function commitHookLayoutUnmountEffects(
finishedWork: Fiber,
nearestMountedAncestor: null | Fiber,
hookFlags: HookFlags,
) {
// Layout effects are destroyed during the mutation phase so that all
// destroy functions for all fibers are called before any create functions.
// This prevents sibling component effects from interfering with each other,
// e.g. a destroy function in one component should never override a ref set
// by a create function in another component during the same commit.
commitHookEffectListUnmount(
hookFlags,
finishedWork,
nearestMountedAncestor,
);
}
The code inside useLayoutEffect and all state updates scheduled from it block the browser from repainting the screen. When used excessively, this makes your app slow. When possible, prefer useEffect.
공식문서를 보면 useLayoutEffect
는 useEffect
와는 다르게 브라우저의 repaint를 막아 성능 이슈가 있을 수 있으니 주의해서 사용하라고 한다. create
실행 시점 차이를 앞서 보았으므로 이젠 원인을 알 수 있다.