언어·프레임워크/React.js

[React.js] `useEffect`와 `useLayoutEffect`의 차이: 깜박임 현상과 중간 값 노출

DandyNow 2025. 5. 23. 01:18
728x90
반응형

React에서 useEffectuseLayoutEffect의 차이: 깜박임 현상과 중간 값 노출

React 애플리케이션을 개발하다 보면 useEffectuseLayoutEffect라는 두 가지 훅을 자주 만나게 된다. 두 훅 모두 컴포넌트 렌더링 후 특정 작업을 수행하는 데 쓰이지만, 실행 시점에 미묘하지만 중요한 차이가 있다. 이러한 차이는 특히 상태값이 연속적으로 빠르게 변경될 때 "화면 깜박임"이나 "중간 값 노출"과 같은 시각적인 현상으로 나타난다.

이번 포스팅에서는 간결한 예제를 통해 두 훅의 동작 방식과 그 차이를 명확하게 이해한다.


useEffectuseLayoutEffect는 무엇인가?

  • useEffect는 React의 모든 DOM 업데이트가 완료된 후, 그리고 브라우저가 화면을 그린 후에 비동기적으로 실행되는 훅이다. 이는 사용자에게 먼저 현재 상태의 화면이 보인 후, useEffect 내부의 로직이 실행되어 필요하다면 다시 화면을 업데이트하게 됨을 의미한다. 이러한 특성 때문에 useEffect 내부에서 상태를 업데이트하면, 업데이트되기 전의 중간 상태가 잠시 화면에 노출되는 현상이 있을 수 있다.
  • useLayoutEffect는 모든 DOM 변경이 일어난 후에, 하지만 브라우저가 화면을 그리기 전에 동기적으로 실행되는 훅이다. 즉, useLayoutEffect 내부의 모든 로직이 실행되고 나서야 브라우저가 화면을 그리기 시작한다. 따라서 이 훅 내부에서 상태를 업데이트하면, React는 해당 업데이트를 포함하여 화면을 한 번만 그리므로 중간 값이 사용자에게 보이지 않는다.

예제를 통한 이해: 숫자의 변화는 어떠한가?

우리는 버튼 클릭 시 숫자가 0에서 1로 변경되었다가, 곧바로 3으로 변경되는 상황을 시뮬레이션해 본다. 이 과정에서 useEffectuseLayoutEffect가 어떻게 다른 결과를 보여주는지 확인해 보자.

import React, { useState, useEffect, useLayoutEffect } from "react";

function NumberChangeExample() {
  const [count, setCount] = useState(0);

  // useEffect의 경우 버튼 클릭 시 0에서 1로 잠시 보인 후 3으로 변경될 수 있음
  // (깜박임/중간 값 노출)
  useEffect(() => {
    if (count === 1) {
      console.log("useEffect: count is 1, setting to 3 (after paint)");
      setCount(3); // 이 상태 업데이트는 다음 렌더링 사이클에 반영된다.
    }
  }, [count]);

  // useLayoutEffect의 경우 버튼 클릭 시 0에서 3으로 바로 변경됨
  // (중간 값 노출 없음)
  /*
  useLayoutEffect(() => {
    if (count === 1) {
      console.log("useLayoutEffect: count is 1, setting to 3 (before paint)");
      setCount(3); // 이 상태 업데이트는 현재 렌더링 사이클에 즉시 반영된다.
    }
  }, [count]);
  */

  const handleClick = () => {
    setCount(1); // 먼저 1로 설정한다.
  };

  const handleInitialClick = () => {
    setCount(0);
  };

  return (
    <div
      style={{
        padding: "20px",
        border: "1px solid #ccc",
        marginBottom: "20px",
      }}
    >
      <h1>{count}</h1>
      <button onClick={handleClick} style={{ marginRight: "10px" }}>
        시작
      </button>
      <button onClick={handleInitialClick}>초기화</button>
      <p style={{ marginTop: "10px", color: "gray" }}>
        현재는 useEffect가 활성화되어 있다. 시작 버튼을 눌러 중간 값 노출을 확인해 보자.
        <br />
        코드에서 useLayoutEffect를 활성화하고 useEffect를 주석 처리하여 비교해 볼 수 있다.
      </p>
    </div>
  );
}

// App 컴포넌트
function App() {
  return (
    <div
      style={{
        top: "50%",
        left: "50%",
        transform: "translate(-50%, -50%)",
        position: "absolute",
        width: "70%",
        textAlign: "center",
      }}
    >
      <h1>useEffect vs useLayoutEffect</h1>
      <NumberChangeExample />
    </div>
  );
}

export default App;

코드 실행 및 결과 확인은 어떻게 되는가?

위 코드를 React 프로젝트에 적용하고 실행해 본다.

  1. useEffect 활성화 상태:
    • NumberChangeExample 컴포넌트에서 useEffect만 활성화된 상태(useLayoutEffect는 주석 처리)이다.
    • "시작" 버튼을 클릭하면, 숫자가 0에서 1잠시 보였다가 3으로 변경되는 것을 확인할 수 있다. 1이라는 중간 값이 화면에 노출되는 것이다.
  2. useLayoutEffect 활성화 상태:
    • NumberChangeExample 컴포넌트에서 useEffect를 주석 처리하고 useLayoutEffect를 활성화한다.
    • "시작" 버튼을 클릭하면, 숫자가 0에서 3으로 바로 변경되는 것을 확인할 수 있다. 1이라는 중간 값은 사용자에게 보이지 않는다.

왜 이런 차이가 발생하는가?

이 현상의 핵심은 두 훅의 실행 시점에 있다.

  • useEffect는 브라우저가 DOM을 업데이트하고 화면을 그린 후에 실행되는 훅이다. 따라서 setCount(1)이 호출되면 React는 먼저 count1인 상태를 렌더링하여 사용자에게 보여준다. 그 후에 useEffect가 실행되어 setCount(3)을 호출하고, 비로소 count3인 상태가 다시 렌더링된다.
  • useLayoutEffect는 DOM 변경이 일어난 후에, 하지만 브라우저가 화면을 그리기 전에 동기적으로 실행되는 훅이다. setCount(1)이 호출되면 React는 count1인 상태로 DOM을 준비하지만, 브라우저가 화면을 그리기 직전 useLayoutEffect가 실행된다. 이 안에서 setCount(3)이 호출되면, React는 이미 준비된 DOM 변경을 즉시 count3인 상태로 업데이트하고, 브라우저는 최종적으로 count3인 상태만 화면에 그린다. 사용자에게는 중간 값이 보일 틈이 없는 것이다.

언제 어떤 훅을 사용해야 하는가?

  • useEffect를 사용하는 경우는 대부분의 사이드 이펙트(데이터 가져오기, 구독 설정, 타이머 설정 등)가 해당된다. 비동기적으로 실행되므로 애플리케이션의 성능에 영향을 주지 않고, UI를 블로킹하지 않는다.
  • useLayoutEffect를 사용하는 경우는 DOM을 측정하거나 조작하여 레이아웃에 영향을 미치는 작업(예: 스크롤 위치 조정, 요소 크기 측정 후 스타일 적용, 애니메이션 준비)이 해당된다. useLayoutEffect는 동기적으로 실행되므로 화면 깜박임이나 레이아웃 깨짐 현상을 방지할 수 있지만, 남용할 경우 성능 저하를 초래할 수 있으므로 주의해야 한다.

간단히 말해, 화면을 깜박이게 하거나 레이아웃을 깨트릴 수 있는 작업이라면 useLayoutEffect를 고려하는 것이 일반적인 권장 사항이며, 그렇지 않다면 useEffect를 사용하는 것이 좋다.

728x90
반응형