한우의 개발일기

useEffect vs useLayoutEffect 본문

React

useEffect vs useLayoutEffect

한우코딩 2025. 3. 21. 14:24

useLayoutEffect vs useEffect: React 내부 동작과 실용적 차이점

React 개발자라면 useEffect는 매우 친숙한 훅일 것입니다. 하지만 그의 사촌 격인 useLayoutEffect는 상대적으로 덜 사용되고 이해되는 경향이 있죠. 이 두 훅은 언뜻 보기에 비슷하지만, React 내부적으로는 상당히 다른 타이밍에 실행됩니다. 이 글에서는 두 훅의 차이점, 내부 동작 원리, 그리고 언제 어떤 훅을 사용해야 하는지 알아보겠습니다.

실행 타이밍의 차이

먼저 두 훅의 가장 큰 차이점은 실행 타이밍입니다:

  • useEffect: 브라우저가 화면을 그린 후에 비동기적으로 실행됩니다.
  • useLayoutEffect: 브라우저가 화면을 그리기 전에 동기적으로 실행됩니다.

이 차이를 시각적으로 나타내면 다음과 같습니다:

React 렌더링 → DOM 업데이트 → useLayoutEffect 실행 → 브라우저 페인팅 → useEffect 실행

React 내부 동작 들여다보기

React의 소스 코드를 살펴보면 이 두 훅이 어떻게 다르게 처리되는지 이해할 수 있습니다. 렌더링 후 커밋 단계에서 다음과 같은 순서로 처리됩니다:

// 간소화된 React 내부 구현
function commitRootImpl(root, renderPriorityLevel) {
  // 1. DOM 요소 업데이트, 참조 분리 등 변경사항을 커밋
  commitMutationEffects(root, renderPriorityLevel);

  // 2. Layout 효과 커밋 (useLayoutEffect 콜백 호출)
  commitLayoutEffects(root, lanes);

  // 브라우저는 여기서 화면을 그립니다 (paint)

  // 3. 비동기적으로 Effect 스케줄링 (useEffect는 나중에 호출됨)
  schedulePassiveEffects(pendingPassiveHookEffectsUnmount);
  schedulePassiveEffects(pendingPassiveHookEffectsMount);
}

실제 React 내부 코드를 보면 useLayoutEffect의 콜백은 commitLayoutEffectOnFiber 함수 내에서 직접 호출되는 반면, useEffect의 콜백은 schedulePassiveEffects를 통해 나중에 실행되도록 스케줄링됩니다.

간단한 예제로 차이점 이해하기

다음 예제를 통해 두 훅의 동작 차이를 확인해 봅시다:

function Example() {
  const [count, setCount] = useState(0);
  const divRef = useRef(null);

  // 버전 1: useEffect 사용
  useEffect(() => {
    if (divRef.current) {
      divRef.current.style.color = 'red';
      divRef.current.style.fontSize = '24px';
      console.log('useEffect 실행');
    }
  }, [count]);

  /*
  // 버전 2: useLayoutEffect 사용
  useLayoutEffect(() => {
    if (divRef.current) {
      divRef.current.style.color = 'red';
      divRef.current.style.fontSize = '24px';
      console.log('useLayoutEffect 실행');
    }
  }, [count]);
  */

  return (
    <div>
      <div ref={divRef}>{count}</div>
      <button onClick={() => setCount(count + 1)}>증가</button>
    </div>
  );
}

useEffect로 스타일 변경 시:
사용자는 잠깐 동안 원래 스타일이 적용된 화면을 볼 수 있습니다. 그 후에 DOM이 업데이트되어 글자 색상과 크기가 변경됩니다. 이것은 브라우저가 먼저 화면을 그린 후에 useEffect 내부의 코드가 실행되기 때문입니다.

useLayoutEffect로 스타일 변경 시:
사용자는 항상 업데이트된 스타일만 보게 됩니다. 잠깐의 깜빡임도 발생하지 않습니다. 이것은 브라우저가 화면을 그리기 전에 useLayoutEffect 내부의 코드가 이미 실행되었기 때문입니다.

성능 고려사항

두 훅을 선택할 때 고려해야 할 중요한 성능 측면이 있습니다:

  • useLayoutEffect는 브라우저의 페인팅을 차단합니다. 따라서 내부에 무거운 연산이 있으면 화면 업데이트가 지연될 수 있습니다.
  • useEffect는 화면 업데이트를 차단하지 않아 사용자 경험이 더 부드럽습니다.

이런 이유로 React 팀은 특별한 이유가 없다면 useEffect를 사용할 것을 권장합니다.

실제 사용 사례

useLayoutEffect가 적합한 경우:

  1. DOM 측정이 필요할 때
function MeasureExample() {
  const [width, setWidth] = useState(0);
  const divRef = useRef(null);

  useLayoutEffect(() => {
    if (divRef.current) {
      // 요소의 크기를 측정하여 상태 업데이트
      const divWidth = divRef.current.getBoundingClientRect().width;
      setWidth(divWidth);
    }
  }, []);

  return (
    <div>
      <div ref={divRef}>내용</div>
      <p>측정된 너비: {width}px</p>
    </div>
  );
}

이 경우 useEffect를 사용하면 사용자가 잠깐 동안 width가 0인 상태를 볼 수 있으며, 이는 바람직하지 않습니다.

  1. DOM 기반 애니메이션
function AnimationExample() {
  const boxRef = useRef(null);

  useLayoutEffect(() => {
    if (boxRef.current) {
      // 시작 위치 설정
      boxRef.current.style.transform = 'translateX(0px)';

      // 애니메이션 시작
      requestAnimationFrame(() => {
        boxRef.current.style.transition = 'transform 1s ease';
        boxRef.current.style.transform = 'translateX(500px)';
      });
    }
  }, []);

  return <div ref={boxRef} className="box" />;
}

useEffect를 사용하면 박스가 시작 위치에서 잠깐 깜빡인 후 애니메이션이 시작될 수 있습니다.

  1. 툴팁이나 모달 위치 조정
function Tooltip({ targetRef, content }) {
  const tooltipRef = useRef(null);

  useLayoutEffect(() => {
    if (tooltipRef.current && targetRef.current) {
      const targetRect = targetRef.current.getBoundingClientRect();

      // 타겟 요소 위에 툴팁 위치시키기
      tooltipRef.current.style.top = `${targetRect.top - tooltipRef.current.offsetHeight}px`;
      tooltipRef.current.style.left = `${targetRect.left + targetRect.width / 2 - tooltipRef.current.offsetWidth / 2}px`;
    }
  }, [targetRef.current]);

  return <div ref={tooltipRef} className="tooltip">{content}</div>;
}

useEffect가 적합한 경우:

  1. 외부 API 호출
function DataFetcher({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    const fetchUser = async () => {
      const response = await fetch(`/api/users/${userId}`);
      const data = await response.json();
      setUser(data);
    };

    fetchUser();
  }, [userId]);

  return <div>{user ? user.name : '로딩 중...'}</div>;
}
  1. 구독 설정 및 해제
function EventListener() {
  useEffect(() => {
    const handleResize = () => {
      console.log('창 크기가 변경되었습니다');
    };

    window.addEventListener('resize', handleResize);

    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);

  return <div>창 크기를 변경해보세요</div>;
}
  1. 타이머 설정
function Timer() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const timer = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);

    return () => clearInterval(timer);
  }, []);

  return <div>카운트: {count}</div>;
}

useLayoutEffect와 SSR(서버 사이드 렌더링)

useLayoutEffect를 사용할 때 고려해야 할 또 다른 측면은 서버 사이드 렌더링(SSR)입니다. SSR 환경에서 useLayoutEffect는 브라우저 DOM이 없기 때문에 예상대로 작동하지 않습니다. 이 경우 다음과 같은 경고를 보게 됩니다:

Warning: useLayoutEffect does nothing on the server, because its effect cannot be encoded into the server renderer's output format. This will lead to a mismatch between the initial, non-hydrated UI and the intended UI. To avoid this, useLayoutEffect should only be used in components that render exclusively on the client.

SSR을 사용하는 경우 useEffect를 사용하거나, 다음과 같은 패턴을 고려할 수 있습니다:

const useIsomorphicLayoutEffect = typeof window !== 'undefined' 
  ? useLayoutEffect 
  : useEffect;

결론

useEffectuseLayoutEffect의 차이를 이해하는 것은 React 애플리케이션의 성능과 사용자 경험을 최적화하는 데 중요합니다. 요약하자면:

  • useEffect: 화면 업데이트 후 비동기적으로 실행됩니다. 대부분의 경우 이 훅을 사용하세요.
  • useLayoutEffect: 화면 업데이트 전 동기적으로 실행됩니다. DOM 측정이나 시각적 깜빡임 방지가 필요할 때 사용하세요.

React의 내부 동작 원리를 이해하면 이 두 훅을 더 효과적으로 활용할 수 있으며, 각각의 적절한 사용 사례를 식별하는 데 도움이 됩니다. 항상 성능 영향을 고려하고, 특별한 이유가 없다면 기본적으로 useEffect를 선택하는 것이 좋습니다.

React 앱을 더 효율적으로 구축하는 데 이 지식이 도움이 되길 바랍니다!

'React' 카테고리의 다른 글

Suspense 알아보기  (1) 2025.03.24
Errorboundary 내부동작 이해하기  (1) 2025.03.23
useAsync 파해치기  (0) 2025.03.18
useRef() 파해치기  (0) 2025.03.18
리액트의 초기 마운트  (0) 2025.02.13