[React] Hook 낯설게하기 - useEffect

thru·2024년 10월 28일
0

React-Hook

목록 보기
3/3

갈고리 소재 고갈

이전까지는 상태 자체거나, 상태와 직접적으로 연관이 있는 훅들을 살펴보았다. 이번에 작성할 useEffect는 상태와는 별개로 존재하는 훅이다. 내부 동작도 기존의 상태 기반 훅들과는 다르므로 한 편을 모두 할애해서 알아본다.


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 팀에서 중요하게 생각하는 내용임을 유추할 수 있다.

useSyncExternalStore?

방금 useEffect는 렌더링과 외부 시스템을 동기화시키는 훅이라고 소개했다. 그런데 리액트의 훅 목록을 보면 더 노골적인 이름을 가진 훅을 볼 수 있다. 바로 useSyncExternalStore이다. 시스템은 아니고 스토어이긴 하지만 '외부'와 '동기화'라는 키워드를 포함하고 있어 해당 용도에 더 적합한 훅이 아닐까 기대하게 된다. 하지만 useSyncExternalStoreuseEffect와는 정반대의 역할을 가진다.

useEffect는 리액트의 렌더링 과정에 맞게 외부 시스템이 동작하도록 하는 훅이라고 했다. useSyncExternalStore는 반대로 외부 시스템의 상태에 따라 리액트의 렌더링이 작동하도록 한다. 동기화의 방향이 반대라고 할 수 있다.

사실 주된 목적은 외부 시스템의 상태를 리액트의 concurrent 렌더링에 흡수시키는 것이다.

동작 원리

Effect 추가

처음은 다른 훅들과 마찬가지로 훅 구현체에서 시작한다. 업데이트 구현체가 동작 특성을 잘 보여준다.

/**@ 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의 memoizedStateeffect라는 객체로 저장된다.

상태가 아닌데 memoizedState?

지난 시리즈에선 memoizedState가 상태 관련 훅이 저장되는 속성이라고 소개했다. 그런데 useEffect에서도 사용하고 있는 걸 보면 이상하게 느껴진다. 일단 memoizedState는 두 개가 존재한다. 하나는 fiber 객체의 속성이고, 다른 하나는 hook 객체의 속성이다. fiber의 것은 hook 객체가 Linked List 형태로 저장되는 시작점 역할을 한다. hook의 것은 연결된 훅이 실행된 결과물을 저장하는 역할을 한다. useEffecteffect를 결과물로 생성하므로 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 값을 가진다.

effectmemoizedStateupdateQueue에 동시에 저장하고 있다. 이후 살펴보겠지만 updateQueue에 저장된 effect는 commit 단계 이후에 소비되면서 초기화되는 차이점이 있다.

Flag 전달

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로 전달된다.

PassiveEffectReactFiberHooks.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 실행

추가한 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 작업 중이 아니라면 finishConcurrentRendercommitRoot로 바로 연결된다.

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를 예약한다. scheduleCallbacksetTimeout 등을 활용해 작업을 예약한다.

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가 설정된 effectdestory를 실행하고 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를 호출하고 있다.

useLayoutEffect

리액트의 훅을 찾아봤다면 useEffect와 아주 유사한 기능을 하는 훅이 있다는 걸 알고있을 것이다. useLayoutEffectuseEffect와 달리 브라우저의 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가 useEffectPassiveEffectHookPassive였고, useLayoutEffectUpdateEffectHookLayout이다.

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();
    /**@ 생략 **/
  }

UpdateEffectBeforeMutationMask | 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);
}

effectcreate를 실행하던 commitHookEffectListMounthookFlags를 달리해서 사용하는 걸 볼 수 있다. 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.

공식문서를 보면 useLayoutEffectuseEffect와는 다르게 브라우저의 repaint를 막아 성능 이슈가 있을 수 있으니 주의해서 사용하라고 한다. create 실행 시점 차이를 앞서 보았으므로 이젠 원인을 알 수 있다.


참조

profile
프론트 공부 중

0개의 댓글