React

useEffect와 useLayoutEffect의 차이점에 대해서 설명해주세요.

Chrysans 2025. 3. 19. 14:50
728x90
반응형

리액트 로고, useEffect vs useLayoutEffect

목차

  1. 소개
  2. 실행 순서와 타이밍
  3. useEffect 심층 이해
  4. useLayoutEffect 심층 이해
  5. 실제 사용 사례
  6. 성능 고려사항
  7. 트러블슈팅
  8. 요약 및 결론

소개

리액트를 사용하면서 사이드 이펙트를 처리하는 방법으로 가장 먼저 접하게 되는 훅은 useEffect입니다. 그런데 useLayoutEffect라는 비슷해 보이는 훅도 존재합니다. 이름만 봐도 둘은 분명 유사한 목적을 가지고 있지만, 실제로는 중요한 차이가 있습니다.

오늘은 이 두 훅의 차이점을 정확히 이해하고, 각각 언제 사용해야 하는지를 명확하게 알아보겠습니다. 프론트엔드 개발자로서 이 차이를 제대로 이해하면 UI 렌더링 관련 버그를 방지하고, 애플리케이션 성능을 최적화하는 데 큰 도움이 됩니다.


실행 순서와 타이밍

두 훅의 가장 핵심적인 차이는 실행 타이밍입니다.

 

리액트 렌더링 프로세스

리액트의 일반적인 렌더링 프로세스를 간략히 살펴보면:

  1. 리액트가 컴포넌트를 렌더링 (가상 DOM 업데이트)
  2. 브라우저가 화면을 페인팅 (실제 DOM 업데이트)
  3. 사이드 이펙트 실행

이때 두 훅은 다음과 같은 차이가 있습니다:

  • useEffect: 화면 페인팅 이후에 비동기적으로 실행
  • useLayoutEffect: 화면 페인팅 이전에 동기적으로 실행

이를 시각적으로 표현하면:

React가 DOM 업데이트 → useLayoutEffect 실행 → 브라우저 화면 페인팅 → useEffect 실행

이 차이가 왜 중요할까요? 바로 사용자 경험과 성능에 직접적인 영향을 미치기 때문입니다.


useEffect 심층 이해

기본 작동 방식

useEffect는 React 16.8에서 도입된 가장 기본적인 훅 중 하나로, 컴포넌트 렌더링 이후에 어떤 작업을 수행하고 싶을 때 사용합니다.

import React, { useEffect, useState } from 'react';

function ExampleComponent() {
  const [data, setData] = useState<string | null>(null);

  useEffect(() => {
    // 브라우저 페인팅 후 비동기적으로 실행됩니다
    console.log('useEffect가 실행되었습니다');

    // 데이터 페칭과 같은 비동기 작업에 적합
    fetchData().then(result => {
      setData(result);
    });

    // 클린업 함수
    return () => {
      console.log('컴포넌트가 언마운트되거나 의존성이 변경되었습니다');
    };
  }, [/* 의존성 배열 */]);

  return <div>{data ? data : 'Loading...'}</div>;
}

주요 특징

  1. 비동기적 실행: 화면 페인팅 후 실행되므로 사용자는 페인팅된 UI를 볼 수 있습니다.
  2. 클린업 함수: 의존성 변경 또는 컴포넌트 언마운트 시 실행됩니다.
  3. 사용 권장 사례: 데이터 페칭, 이벤트 리스너 등록, 타이머 설정 등 비동기 작업에 적합합니다.

useLayoutEffect 심층 이해

기본 작동 방식

useLayoutEffectuseEffect와 거의 동일한 API를 가지고 있지만, 실행 타이밍이 다릅니다.

import React, { useLayoutEffect, useState, useRef } from 'react';

function LayoutEffectExample() {
  const [width, setWidth] = useState(0);
  const elementRef = useRef<HTMLDivElement>(null);

  useLayoutEffect(() => {
    // DOM이 업데이트된 직후, 브라우저 페인팅 전에 동기적으로 실행됩니다
    console.log('useLayoutEffect가 실행되었습니다');

    // DOM 측정과 같은 동기 작업에 적합
    if (elementRef.current) {
      const newWidth = elementRef.current.getBoundingClientRect().width;
      setWidth(newWidth);
    }
  }, [/* 의존성 배열 */]);

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

주요 특징

  1. 동기적 실행: 브라우저 페인팅 전에 실행되므로 DOM을 읽고 즉시 업데이트할 수 있습니다.
  2. 렌더링 차단: 완료될 때까지 브라우저 페인팅을 차단합니다.
  3. 사용 권장 사례: DOM 요소의 위치나 크기에 따라 레이아웃을 조정해야 하는 경우 적합합니다.

실제 사용 사례

두 훅을 언제 사용해야 할지 대표적인 사례를 통해 알아보겠습니다.

useEffect 사용 사례

1. 데이터 페칭

function DataFetchingComponent() {
  const [data, setData] = useState<any[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // 비동기 데이터 페칭은 useEffect가 적합
    const fetchData = async () => {
      try {
        setLoading(true);
        const response = await fetch('https://api.example.com/data');
        const result = await response.json();
        setData(result);
      } catch (error) {
        console.error('데이터 로딩 실패:', error);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, []);

  if (loading) return <div>로딩 중...</div>;
  return <div>{/* 데이터 렌더링 */}</div>;
}

2. 이벤트 리스너 등록

function EventListenerComponent() {
  useEffect(() => {
    const handleResize = () => {
      console.log('창 크기가 변경되었습니다');
    };

    window.addEventListener('resize', handleResize);

    // 클린업에서 이벤트 리스너 제거
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);

  return <div>창 크기 변경을 확인하세요</div>;
}

useLayoutEffect 사용 사례

1. 플리커링(깜빡임) 방지

사용자에게 보여지기 전에 레이아웃 계산이 필요한 경우:

function NoFlickerComponent() {
  const [position, setPosition] = useState({ left: 0, top: 0 });
  const elementRef = useRef<HTMLDivElement>(null);

  // useLayoutEffect를 사용하여 DOM 업데이트 직후, 화면 표시 전에 위치 계산
  useLayoutEffect(() => {
    if (elementRef.current) {
      const { height } = elementRef.current.getBoundingClientRect();

      // 위치 계산 및 업데이트 - 사용자는 이 계산의 중간 과정을 보지 않음
      setPosition({
        left: window.innerWidth / 2 - 150, // 중앙 정렬
        top: window.innerHeight / 2 - height / 2, // 중앙 정렬
      });
    }
  }, []);

  return (
    <div
      ref={elementRef}
      style={{
        position: 'absolute',
        width: '300px',
        left: `${position.left}px`,
        top: `${position.top}px`,
        padding: '20px',
        backgroundColor: '#f0f0f0',
      }}
    >
      모달 내용
    </div>
  );
}

 

2. DOM 측정 후 즉시 위치 조정

툴팁이나 팝오버와 같이 DOM 요소의 위치에 따라 배치해야 하는 UI 요소:

function TooltipPositioner({ targetRef, children }) {
  const [tooltipPosition, setTooltipPosition] = useState({ left: 0, top: 0 });
  const tooltipRef = useRef<HTMLDivElement>(null);

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

      // 타겟 요소 위에 툴팁 위치시키기
      setTooltipPosition({
        left: targetRect.left + (targetRect.width - tooltipRect.width) / 2,
        top: targetRect.top - tooltipRect.height - 10,
      });
    }
  }, [targetRef]);

  return (
    <div
      ref={tooltipRef}
      style={{
        position: 'absolute',
        left: `${tooltipPosition.left}px`,
        top: `${tooltipPosition.top}px`,
        padding: '8px',
        backgroundColor: '#333',
        color: 'white',
        borderRadius: '4px',
      }}
    >
      {children}
    </div>
  );
}

성능 고려사항

두 훅 중 어떤 것을 선택할지는 성능에도 영향을 미칩니다:

useEffect의 성능 영향

  • 장점: 브라우저 페인팅을 차단하지 않아 초기 렌더링이 더 빠르게 느껴질 수 있습니다.
  • 단점: UI 업데이트가 있다면 사용자가 잠깐 동안 중간 상태를 볼 수 있습니다(플리커링 현상).

useLayoutEffect의 성능 영향

  • 장점: 화면에 보이기 전에 모든 DOM 업데이트가 완료되어 시각적 일관성을 유지합니다.
  • 단점: 실행 시간이 길면 화면 페인팅이 지연되어 성능 저하가 발생할 수 있습니다.

성능 관련 베스트 프랙티스

  1. 기본적으로 useEffect 사용: 특별한 이유가 없다면 useEffect를 사용하세요.
  2. 필요한 경우에만 useLayoutEffect 사용: 시각적 불일치나 플리커링을 방지해야 할 때만 사용하세요.
  3. 무거운 계산은 피하기: useLayoutEffect 내에서 복잡한 계산은 피하고, 필요한 DOM 측정과 레이아웃 조정 작업만 수행하세요.

트러블슈팅

일반적인 문제와 해결 방법

1. useLayoutEffect에서의 무한 루프

// 문제가 있는 코드
function ProblemComponent() {
  const [size, setSize] = useState(0);

  useLayoutEffect(() => {
    // 무한 루프 발생! 레이아웃 효과 내에서 상태 업데이트가 다시 레이아웃 효과를 트리거
    setSize(document.body.clientWidth);
  }, [size]); // size를 의존성으로 포함하면 무한 루프

  return <div>Width: {size}</div>;
}

// 해결책
function FixedComponent() {
  const [size, setSize] = useState(0);

  useLayoutEffect(() => {
    // 초기에 한 번만 실행됨
    setSize(document.body.clientWidth);

    // 윈도우 리사이즈 시에만 업데이트
    const handleResize = () => {
      setSize(document.body.clientWidth);
    };

    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []); // 빈 의존성 배열

  return <div>Width: {size}</div>;
}

 

2. 서버 사이드 렌더링(SSR)에서의 문제

 

  • 서버는 HTML 문자열만 생성할 뿐, 실제 웹페이지와 DOM 트리가 존재하지 않습니다.
  • document.body.clientWidth 같은 코드는 서버에서 실행할 수 없습니다.

 useLayoutEffect는 DOM 조작을 위해 설계되었습니다

  • 이 훅은 DOM이 화면에 그려지기 직전에 실행됩니다.
  • 서버에서는 DOM이 그려지는 과정 자체가 없기 때문에 실행할 수 없습니다.

실생활 비유로 설명하면:

useLayoutEffect는 집을 지은 후 바로 가구를 배치하는 것과 같습니다. 그런데 서버는 집의 청사진(HTML)만 그릴 뿐, 실제 집(DOM)을 짓지 않습니다. 가구를 배치(DOM 조작)하려면 실제 집이 필요한데, 서버에는 청사진만 있어서 불가능한 거죠.

 

// 문제: useLayoutEffect는 SSR에서 경고를 발생시킵니다
function SSRComponent() {
  useLayoutEffect(() => {
    // 서버에는 DOM이 없어서 실행할 수 없음
    console.log(document.body.clientWidth);
  }, []);

  return <div>SSR Component</div>;
}

// 해결책: 조건부로 useEffect 또는 useLayoutEffect 사용
function SSRSafeComponent() {
  // isomorphic-unfetch 또는 유사한 라이브러리 사용
  const useIsomorphicLayoutEffect = 
    typeof window !== 'undefined' ? useLayoutEffect : useEffect;

  useIsomorphicLayoutEffect(() => {
    console.log(document.body.clientWidth);
  }, []);

  return <div>SSR Safe Component</div>;
}

 

3. 플리커링(깜빡임) 문제

// 문제: useEffect를 사용하면 플리커링이 발생할 수 있음
function FlickeringComponent() {
  const [position, setPosition] = useState({ left: 0, top: 0 });

  useEffect(() => {
    // 화면 페인팅 후 실행되므로 사용자는 잠시 원래 위치를 보게 됨
    setPosition({ left: 100, top: 100 });
  }, []);

  return (
    <div style={{ position: 'absolute', left: position.left, top: position.top }}>
      이 요소는 이동 시 깜빡일 수 있습니다
    </div>
  );
}

// 해결책: useLayoutEffect 사용
function NoFlickerComponent() {
  const [position, setPosition] = useState({ left: 0, top: 0 });

  useLayoutEffect(() => {
    // 화면 페인팅 전에 실행되므로 사용자는 최종 위치만 보게 됨
    setPosition({ left: 100, top: 100 });
  }, []);

  return (
    <div style={{ position: 'absolute', left: position.left, top: position.top }}>
      이 요소는 이동 시 깜빡이지 않습니다
    </div>
  );
}

 


요약 및 결론

두 훅의 주요 차이점을 다시 정리해보겠습니다:

특성 useEffect useLayoutEffect
실행 시점 브라우저 페인팅 후 브라우저 페인팅 전
실행 방식 비동기적 동기적
렌더링 차단 차단하지 않음 차단함
주요 사용 사례 데이터 페칭, 이벤트 리스너, 타이머 DOM 측정, 레이아웃 조정, 플리커링 방지
성능 영향 초기 렌더링 빠름 로직이 복잡할 경우 렌더링 지연 가능
SSR 호환성 호환됨 경고 발생 (대안 필요)

결론적으로:

  1. 기본적으로 useEffect 선택: 대부분의 사이드 이펙트에 적합하며, 리액트 팀도 이를 권장합니다.
  2. 시각적 불일치를 방지해야 할 때만 useLayoutEffect 사용: DOM 요소의 측정과 즉각적인 레이아웃 조정이 필요한 경우에 적합합니다.
  3. 성능을 주의하세요: useLayoutEffect는 브라우저 렌더링을 차단하므로 내부 로직은 가능한 가볍게 유지해야 합니다.

두 훅의 차이점을 이해하고 적절하게 활용하면 더 나은 사용자 경험과 성능을 갖춘 React 애플리케이션을 구축할 수 있습니다. 항상 프로젝트의 요구사항과 성능 목표를 고려하여 적절한 훅을 선택하세요.

 


전문 용어 설명

  • 브라우저 페인팅: 브라우저가 DOM 변경사항을 실제 화면에 그리는 과정
  • 사이드 이펙트: 컴포넌트의 렌더링 외에 추가로 발생하는 작업(데이터 페칭, DOM 조작 등)
  • 플리커링(Flickering): UI 요소가 잠시 깜빡이는 현상으로, 초기 렌더링과 후속 상태 업데이트 사이에 발생
  • 렌더링 차단: 특정 작업이 완료될 때까지 화면 업데이트를 지연시키는 것
  • SSR(Server-Side Rendering): 서버에서 컴포넌트를 렌더링하여 HTML을 생성하는 기술
  • 의존성 배열: useEffect나 useLayoutEffect의 두 번째 인자로, 이 배열의 값이 변경될 때만 효과가 다시 실행됨

 

useEffect 관련 참고 링크

 

 

728x90
반응형