# [번역] React의 useEffect()는 어떻게 동작하나요?

> 영문 블로그 글을 번역했습니다. 허가를 받으면 시리즈를 이어갈 예정입니다.  
> 원문링크: [https://jser.dev/2023-07-08-how-does-useeffect-work](https://jser.dev/2023-07-08-how-does-useeffect-work)

---

> ℹ️ [React Internals Deep Dive](https://jser.dev/series/react-source-code-walkthrough.html) 에피소드 4, [유튜브에서 제가 설명하는 것](https://www.youtube.com/watch?v=Ggmdo7TORNc&list=PLvx8w9g4qv_p-OS-XdbB3Ux_6DMXhAJC3&index=4)을 시청해주세요.
> 
> ⚠ [React@18.2.0](https://github.com/facebook/react/releases/tag/v18.2.0) 기준, 최신 버전에서는 구현이 변경되었을 수 있습니다.

`useEffect()`는 React에서 `useState()`를 제외하고 가장 많이 사용되는 훅입니다.  
매우 강력하지만 때때로 혼란스러울 수 있는데, 내부적으로 어떻게 작동하는지 알아봅시다.

```jsx
useEffect(() => {
  // ...
}, [deps])
```

- [1. 최초 마운트에서 `useEffect()`](#heading-1-useeffect)
- [2. 리-렌더링의 `useEffect()`](#heading-2-useeffect)
- [3. Effect는 언제, 어떻게 실행되고 정리되나요?](#heading-3-effect)
  - [3.1 패시브 Effect에 대한 flushing은 `commitRoot()`내에서 트리거 됩니다.](#heading-31-effect-flushing-commitroot)
  - [3.2 `flushPassiveEffects()`](#heading-32-flushpassiveeffects)
  - [3.3 `commitPassiveUnmountEffects()`](#heading-33-commitpassiveunmounteffects)
  - [3.4 `commitPassiveMountEffects()`](#heading-34-commitpassivemounteffects)
- [4. 요약](#heading-4)
- [5. 퀴즈 챌린지](#heading-5)

## 1\. 최초 마운트에서 `useEffect()`

`useEffect()`는 최초 마운트에서 `mountEffect()`를 사용합니다.

> ***💬 역자 주석: Jser의 코멘트는 ❗❗로 표시 해뒀습니다.  
> 그 외 주석은 리액트 소스 코드 자체의 주석입니다.  
> ... 은 생략된 코드입니다.***

```typescript
function mountEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  return mountEffectImpl(
    PassiveEffect | PassiveStaticEffect,
    // ❗❗ ↖ 이 플래그를 사용하여 레이아웃 Effect와의 차이점을 구분할 수 있어 중요합니다.
    // ❗❗ PassiveStaticEffect는 무엇일까요? 이 것은 다른 에피소드에서 다룰 가치가 있을거 같습니다.
    HookPassive,
    create,
    deps,
  );
}
function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
  const hook = mountWorkInProgressHook();
  // ❗❗ ↗ 새로운 훅을 만듭니다.
  const nextDeps = deps === undefined ? null : deps;
  currentlyRenderingFiber.flags |= fiberFlags;
  hook.memoizedState = pushEffect(
  // ❗❗               ↗ pushEffect()는 Effect 객체를 만들고, 그것을 훅에 설정합니다.
    HookHasEffect | hookFlags,
    // ❗❗ ↖ 이 플래그도 중요한데, 이것은 이 이팩트를 최초 마운트에서 실행해야 한다는 것을 의미합니다.
    create,
    undefined,
    nextDeps,
  );
}
```

```typescript
function pushEffect(tag, create, destroy, deps) {
  const effect: Effect = {
    tag,
    // ❗❗ ↖ tag는 중요한데, 이 Effect를 실행해야 하는지 여부를 표시하는 데 사용됩니다.
    create,
    // ❗❗ 우리가 전달한 콜백 입니다
    destroy,
    // ❗❗ 콜백에서 반환한 정리 함수 입니다.
    deps,
    // ❗❗ 우리가 전달한 deps 배열입니다.
    // Circular
    next: (null: any),
    // ❗❗ 하나의 컴포넌트에 여러 Effect가 있을 수 있습니다. 그걸 체인으로 엮습니다.
  };
  let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);
  if (componentUpdateQueue === null) {
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
    // ❗❗ Effect는 fiber의 updateQueue에 저장됩니다.
    // ❗❗ 이것은 훅의 memoizedState와는 다릅니다.
    componentUpdateQueue.lastEffect = effect.next = effect;
  } else {
    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;
}
```

최초 마운트의 경우, useEffect()가 필요한 플래그를 사용하여 Effect 객체를 생성하는 것을 볼 수 있습니다. effect 객체들은 다른 타이밍에 처리될 것입니다.

## 2\. 리-렌더링의 `useEffect()`

```typescript
function updateEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  return updateEffectImpl(PassiveEffect, HookPassive, create, deps);
}
function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
  const hook = updateWorkInProgressHook();
  // ❗❗ ↗ 현재 훅을 가져옵니다
  const nextDeps = deps === undefined ? null : deps;
  let destroy = undefined;
  if (currentHook !== null) {
    const prevEffect = currentHook.memoizedState;
    // ❗❗                        ↗ memoizedState의 effect 훅이 Effect 객체라는 것을 기억하세요.
    destroy = prevEffect.destroy;
    if (nextDeps !== null) {
      const prevDeps = prevEffect.deps;
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps);
        return;
      }
      // ❗❗ deps가 변경되지 않으면, Effect 객체를 다시 생성하는 것만 수행합니다
      // ❗❗ 여기서 다시 생성해야 하는 이유는 아마도 단순히 updateQueue를 다시 생성해야 하기 때문일겁니다.
      // ❗❗ 그리고 우리는 업데이트 된 create()함수를 가져올 필요가 있습니다.
      // ❗❗ 여기서는 이전 destory() 함수를 사용하고 있음도 알 수 있습니다.
    }
  }
  currentlyRenderingFiber.flags |= fiberFlags;
  hook.memoizedState = pushEffect(
    HookHasEffect | hookFlags,
    // ❗❗ ↖ 만약 deps가 바뀌면, HookHasEffect 는 이 Effect를 실행 해야함을 나타내는 표시입니다.
    create,
    destroy,
    nextDeps,
  );
}
```

우리는 혼란스러운 deps 배열이 어떻게 작동하는지 알 수 있엇습니다. 리-렌더링할 때는 어떤 경우에는 Effect 객체를 다시 생성하지만, deps가 변경되면 생성된 Effect가 이전 정리(`destory()`) 함수를 사용하여 실행되도록 표시됩니다.

## 3\. Effect는 언제, 어떻게 실행되고 정리되나요?

위에서 우리는 `useEffect()`가 Fiber Node에 추가 데이터 구조를 생성할 뿐이라는 것을 알았습니다. 이제 이러한 Effect 객체가 어떻게 처리되는지 알아볼 필요가 있습니다.

### 3.1 패시브 Effect에 대한 flushing은 `commitRoot()`내에서 트리거 됩니다.

두 개의 Fiber Tree(조정)를 비교하여 서로 다른 결과를 얻은 후 "Commit" 단계에서 호스트 DOM에 변경 사항을 반영합니다. 패시브 Effect의 의 flush를 시작한 코드를 쉽게 찾을 수 있습니다.

```typescript
function commitRootImpl(
  root: FiberRoot,
  recoverableErrors: null | Array<CapturedValue<mixed>>,
  transitions: Array<Transition> | null,
  renderPriorityLevel: EventPriority,
) {
  // 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.)
  // TODO: Delete all other places that schedule the passive effect callback
  // They're redundant.
  if (
    (finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||
    (finishedWork.flags & PassiveMask) !== NoFlags
  ) {
    if (!rootDoesHavePassiveEffects) {
      rootDoesHavePassiveEffects = true;
      pendingPassiveEffectsRemainingLanes = remainingLanes;
      // workInProgressTransitions might be overwritten, so we want
      // to store it in pendingPassiveTransitions until they get processed
      // We need to pass this through as an argument to commitRoot
      // because workInProgressTransitions might have changed between
      // the previous render and commit if we throttle the commit
      // with setTimeout
      pendingPassiveTransitions = transitions;
      scheduleCallback(NormalSchedulerPriority, () => {
        flushPassiveEffects();
        // 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;
      });
      // ❗❗ 여기서 useEffect()에 의해 생겨난 패시드 Effect를 플러시 합니다.
      // ❗❗ 이렇게 하면 당장이 아닌 다음 틱(tick)에 플러싱이 예약됩니다.
      // ❗❗ 좀 더 자세한 정보는 EP20의 리액트 스케쥴러는 어떻게 동작하는가를 참고하세요
    }
  }
  ...
}
```

> [EP20 - 리액트 스케쥴러는 어떻게 동작하는가? scheduleCallback() 북마크](https://jser.dev/react/2022/03/16/how-react-scheduler-works/#41-schedulecallback---scheduler-schedules-tasks-by-exipriationtime)

### 3.2 `flushPassiveEffects()`

```typescript
function flushPassiveEffectsImpl() {
  if (rootWithPendingPassiveEffects === null) {
    return false;
  }
  // Cache and clear the transitions flag
  const transitions = pendingPassiveTransitions;
  pendingPassiveTransitions = null;
  const root = rootWithPendingPassiveEffects;
  const lanes = pendingPassiveEffectsLanes;
  rootWithPendingPassiveEffects = null;
  // TODO: This is sometimes out of sync with rootWithPendingPassiveEffects.
  // Figure out why and fix it. It's not causing any known issues (probably
  // because it's only used for profiling), but it's a refactor hazard.
  pendingPassiveEffectsLanes = NoLanes;
  const prevExecutionContext = executionContext;
  executionContext |= CommitContext;
  commitPassiveUnmountEffects(root.current);
  commitPassiveMountEffects(root, root.current, lanes, transitions);
  // ❗❗↗ 여기서 우리는 콜백이 실행되지 전에 Effect 정리가 먼저 실행되는 것을 명확하게 볼 수 있습니다.
  ...
}
```

### 3.3 `commitPassiveUnmountEffects()`

```typescript
export function commitPassiveUnmountEffects(finishedWork: Fiber): void {
  setCurrentDebugFiberInDEV(finishedWork);
  commitPassiveUnmountOnFiber(finishedWork); // ❗❗ commitPassiveUnmountOnFiber
  resetCurrentDebugFiberInDEV();
}
function commitPassiveUnmountOnFiber(finishedWork: Fiber): void {
  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case SimpleMemoComponent: {
      recursivelyTraversePassiveUnmountEffects(finishedWork);
      // ❗❗ ↗ 우리는 여기서 자식들의 effects들이 먼저 정리되는 것을 볼 수 있습니다.
      if (finishedWork.flags & Passive) {
        commitHookPassiveUnmountEffects( // ❗❗ commitHookPassiveUnmountEffects
          finishedWork,
          finishedWork.return,
          HookPassive | HookHasEffect,
          // ❗❗         ↗ 이 플래그(HookHasEffect)는 deps가 변경되지 않았을 때 훅이 실행되지 않는 것을 보장합니다
        );
      }
      break;
    }
    ...
  }
}
function commitHookPassiveUnmountEffects(
  finishedWork: Fiber,
  nearestMountedAncestor: null | Fiber,
  hookFlags: HookFlags,
) {
  if (shouldProfile(finishedWork)) {
    startPassiveEffectTimer();
    commitHookEffectListUnmount( // ❗❗ commitHookEffectListUnmount
      hookFlags,
      finishedWork,
      nearestMountedAncestor,
    );
    recordPassiveEffectDuration(finishedWork);
  } else {
    commitHookEffectListUnmount( // ❗❗ commitHookEffectListUnmount
      hookFlags,
      finishedWork,
      nearestMountedAncestor,
    );
  }
}
function commitHookEffectListUnmount(
  flags: HookFlags,
  finishedWork: Fiber,
  nearestMountedAncestor: Fiber | null,
) {
  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) { // ❗❗ (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);
    // ❗❗ 여기서는 updateQueue의 모든 effect를 단순히 루프 돌기만 하면 됩니다
    // ❗❗ 그러면서 플래그를 통해 필요한 항목들을 필터링 합니다.
  }
}
function safelyCallDestroy(
  current: Fiber,
  nearestMountedAncestor: Fiber | null,
  destroy: () => void,
) {
  try {
    destroy();
  } catch (error) {
    captureCommitPhaseError(current, nearestMountedAncestor, error);
  }
}
```

### 3.4 `commitPassiveMountEffects()`

`commitPassiveMountEffects()`는 같은 방식으로 작동합니다.

```typescript
export function commitPassiveMountEffects(
  root: FiberRoot,
  finishedWork: Fiber,
  committedLanes: Lanes,
  committedTransitions: Array<Transition> | null,
): void {
  setCurrentDebugFiberInDEV(finishedWork);
  commitPassiveMountOnFiber( // ❗❗ commitPassiveMountOnFiber
    root,
    finishedWork,
    committedLanes,
    committedTransitions,
  );
  resetCurrentDebugFiberInDEV();
}
function commitPassiveMountOnFiber(
  finishedRoot: FiberRoot,
  finishedWork: Fiber,
  committedLanes: Lanes,
  committedTransitions: Array<Transition> | null,
): void {
  // When updating this function, also update reconnectPassiveEffects, which does
  // most of the same things when an offscreen tree goes from hidden -> visible,
  // or when toggling effects inside a hidden tree.
  const flags = finishedWork.flags;
  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case SimpleMemoComponent: {
      recursivelyTraversePassiveMountEffects(
      // ❗❗ ↖ 우리는 여기서 자식들의 effects들이 먼저 실행되는 것을 볼 수 있습니다.
        finishedRoot,
        finishedWork,
        committedLanes,
        committedTransitions,
      );
      if (flags & Passive) {
        commitHookPassiveMountEffects( // ❗❗ commitHookPassiveMountEffects
          finishedWork,
          HookPassive | HookHasEffect,
          // ❗❗        ↗ 이 플래그(HookHasEffect)는 deps가 변경되지 않았을 때 훅이 실행되지 않는 것을 보장합니다

        );
      }
      break;
    }
    ...
  }
}
function commitHookPassiveMountEffects(
  finishedWork: Fiber,
  hookFlags: HookFlags,
) {
  if (shouldProfile(finishedWork)) {
    startPassiveEffectTimer();
    try {
      commitHookEffectListMount(hookFlags, finishedWork); // ❗❗ commitHookEffectListMount
    } catch (error) {
      captureCommitPhaseError(finishedWork, finishedWork.return, error);
    }
    recordPassiveEffectDuration(finishedWork);
  } else {
    try {
      commitHookEffectListMount(hookFlags, finishedWork); // ❗❗ commitHookEffectListMount
    } catch (error) {
      captureCommitPhaseError(finishedWork, finishedWork.return, error);
    }
  }
}
function commitHookEffectListMount(flags: HookFlags, finishedWork: Fiber) {
  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) {
        // Mount
        const create = effect.create;
        const inst = effect.inst;
        const destroy = create();
callback is run here!

        inst.destroy = destroy;
      }
      effect = effect.next;
    } while (effect !== firstEffect);
    // ❗❗ 다시, 필요한 Effect들을 필터링하고 다음을 실행합니다.
  }
}
```

## 4\. 요약

소스 코드를 살펴본 결과, `useEffect()`의 내부 구조는 매우 간단하는 것을 알 수 있었습니다.

1. `useEffect()` 는 Fiber에 저장되어 있는 Effect 객체를 생성합니다.
    
    * Effect 는 실행이 필요한지 여부를 나타내는 `tag` 를 가집니다.
        
    * Effect 는 첫번째 인수로 전달되는 `create()` 함수가 있습니다.
        
    * Effect `create()`의 정리인 `destroy()`가 있고, 이것은 `create()`가 실행될 때만 설정됩니다.
        
2. `useEffect()` 는 매번 새로운 Effect 객체들을 만들어냅니다, 하지만 이것들은 만약 deps 배열이 변경되면 다른 `tag`를 설정하게 됩니다.
    
3. 호스트 DOM에 업데이트를 커밋할 때, 다음 틱(tick)의 작업은 `tag`를 기반으로 모든 Effect를 다시 실행 하도록 예약됩니다.
    
    * 하위 컴포넌트의 Effect가 먼저 처리 됩니다.
        
    * 정리가 먼저 실행됩니다.
        

## 5\. 퀴즈 챌린지

이제 오늘 배운 내용을 바탕으로 [BFE.dev](http://BFE.dev)의 React 퀴즈를 풀 수 있습니다.

* 퀴즈 링크: [https://bigfrontend.dev/react-quiz/useeffect-iii](https://bigfrontend.dev/react-quiz/useeffect-iii)
    

원본 글 작성일: 2023-07-08
