useEffect()는 어떻게 동작할까

DongHyun Park·2024년 10월 3일
2

React

목록 보기
2/6
post-thumbnail

React 개발자로서 컴포넌트의 생명주기와 부수 효과 관리는 항상 중요한 주제입니다. 그 중에서도 useEffect는 함수형 컴포넌트에서 부수 효과를 다루는 핵심적인 Hook입니다. 이번 글에서는 useEffect의 실제 동작 방식을 React 소스 코드를 통해 깊이 있게 살펴보고, 최적화 전략까지 알아보겠습니다.

목차

  1. useEffect란?
  2. React의 소스 코드 구조
  3. useEffect의 공개 API
  4. useEffect의 내부 구현
  5. 의존성 배열의 동작
  6. 클린업 함수의 실행
  7. useEffect와 렌더링 사이클
  8. useEffect의 한계와 주의점
  9. 성능 최적화 전략
  10. 결론

useEffect란?

useEffect는 함수형 컴포넌트에서 부수 효과(side effects)를 수행하기 위한 Hook입니다. 부수 효과란 데이터 가져오기, 구독 설정, DOM 수동 조작 등 컴포넌트의 주 렌더링 프로세스 외의 작업들을 말합니다.

React의 소스 코드 구조

React의 소스 코드는 여러 패키지로 구성되어 있습니다. useEffect와 관련된 주요 파일들은 다음과 같습니다:

  1. packages/react/src/ReactHooks.js: Hooks의 공개 API를 정의합니다.
  2. packages/react-reconciler/src/ReactFiberHooks.js: Hooks의 실제 구현을 담당합니다.

이러한 구조는 React의 모듈화된 설계를 보여주며, 공개 API와 내부 구현을 분리하여 유지보수성과 확장성을 높입니다.

useEffect의 공개 API

packages/react/src/ReactHooks.js 파일에서 useEffect의 공개 API를 찾을 수 있습니다:

export function useEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  const dispatcher = resolveDispatcher();
  return dispatcher.useEffect(create, deps);
}

이 코드는 다음과 같은 중요한 점들을 보여줍니다:

  1. useEffectcreate 함수와 deps 배열을 인자로 받습니다.
  2. 실제 구현은 dispatcher.useEffect로 위임됩니다.
  3. resolveDispatcher 함수는 현재 React의 렌더링 단계에 따라 적절한 dispatcher를 반환합니다.

useEffect의 내부 구현

useEffect의 실제 구현은 packages/react-reconciler/src/ReactFiberHooks.js 파일에서 찾을 수 있습니다:

function mountEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  return mountEffectImpl(
    UpdateEffect | PassiveEffect,
    HookPassive,
    create,
    deps,
  );
}

function updateEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  return updateEffectImpl(
    UpdateEffect | PassiveEffect,
    HookPassive,
    create,
    deps,
  );
}

여기서 우리는 두 가지 주요 함수를 볼 수 있습니다:

  1. mountEffect: 컴포넌트가 처음 마운트될 때 호출됩니다.
  2. updateEffect: 이후 업데이트 시 호출됩니다.

두 함수 모두 create 함수(효과를 수행하는 함수)와 deps(의존성 배열)를 인자로 받습니다.

의존성 배열의 동작

의존성 배열은 useEffect의 두 번째 인자로 전달되며, 이 배열의 값들이 변경될 때만 효과를 재실행합니다. React는 이전 렌더링의 의존성 값들과 현재 렌더링의 값들을 비교합니다:

function areHookInputsEqual(
  nextDeps: Array<mixed>,
  prevDeps: Array<mixed> | null,
) {
  if (prevDeps === null) {
    return false;
  }
  for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
    if (Object.is(nextDeps[i], prevDeps[i])) {
      continue;
    }
    return false;
  }
  return true;
}

이 함수는 Object.is를 사용하여 각 의존성을 비교합니다. 모든 의존성이 동일하면 true를 반환하여 효과를 재실행하지 않습니다.

Object.is()의 중요성:

  • 참조 타입 비교: 객체나 배열같은 참조 타입의 경우, 내용이 같더라도 참조가 다르면 다른 것으로 간주됩니다.
  • 원시 타입 비교: 숫자, 문자열 등의 원시 타입은 값 자체를 비교합니다.
  • 특수한 경우 처리: NaN, +0, -0 등의 특수한 경우도 정확히 처리합니다.

클린업 함수의 실행

클린업 함수는 컴포넌트가 언마운트되거나 다음 효과가 실행되기 직전에 호출됩니다:

function commitHookEffectListUnmount(tag: number, 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 & tag) === tag) {
        // Unmount
        const destroy = effect.destroy;
        effect.destroy = undefined;
        if (destroy !== undefined) {
          destroy();
        }
      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}

이 함수는 모든 효과의 클린업 함수를 순회하며 실행합니다.

useEffect와 렌더링 사이클

useEffect는 렌더링이 완료된 후 비동기적으로 실행됩니다. 이는 브라우저가 화면을 그리는 것을 차단하지 않아 성능상 이점이 있습니다.

useEffect의 한계와 주의점

  1. 의존성 배열 누락: 모든 외부 변수를 의존성 배열에 포함시켜야 합니다.
  2. 무한 루프: 효과 내에서 상태를 업데이트할 때 주의가 필요합니다.
  3. 과도한 사용: 모든 상태 변화에 useEffect를 사용하는 것은 성능을 저하시킬 수 있습니다.

성능 최적화 전략

  1. 의존성 배열 최적화: 필요한 의존성만 포함시켜 불필요한 재실행을 방지합니다.

    useEffect(() => {
      // 효과 코드
    }, [dependency1, dependency2]);
  2. cleanup 함수 활용: 구독 해제 등의 정리 작업을 수행하여 메모리 누수를 방지합니다.

    useEffect(() => {
      const subscription = someAPI.subscribe();
      return () => {
        subscription.unsubscribe();
      };
    }, []);
  3. useCallback과 함께 사용: 효과 내에서 사용하는 함수를 메모이제이션하여 불필요한 재실행을 방지합니다.

    const memoizedCallback = useCallback(() => {
      doSomething(a, b);
    }, [a, b]);
    
    useEffect(() => {
      memoizedCallback();
    }, [memoizedCallback]);

결론

useEffect는 React 함수형 컴포넌트에서 부수 효과를 관리하는 강력한 도구입니다. 그 내부 동작을 이해하고 적절히 사용하는 것이 중요합니다. 이 글에서 살펴본 것처럼, React의 내부 구현은 복잡하지만 효율적으로 설계되어 있습니다.

개발자로서 우리는 이러한 도구의 장단점을 이해하고, 애플리케이션의 특성에 맞게 적절히 활용해야 합니다. useEffect를 통한 부수 효과 관리는 React 애플리케이션의 성능과 유지보수성을 크게 향상시킬 수 있지만, 항상 그 사용을 신중히 고려해야 합니다.

React의 지속적인 발전과 함께, 우리도 이러한 Hook들의 사용법과 최적화 기법들을 계속해서 학습하고 적용해 나가야 할 것입니다.

2개의 댓글

comment-user-thumbnail
2024년 10월 4일

이야... 대단하시군요 react도 분석하시고

1개의 답글