기본 콘텐츠로 건너뛰기

React에서 useEffect와 useLayoutEffect, 언제 무엇을 써야 할까

React에서 useEffect와 useLayoutEffect, 언제 무엇을 써야 할까

빠른 답

  • 기본 선택은 useEffect다. 네트워크 요청, 구독, 이벤트 등록처럼 화면 표시를 막을 이유가 없는 작업에 맞다.
  • useLayoutEffect는 DOM 크기 측정이나 위치 보정처럼 화면이 그려지기 전에 끝나야 하는 작업에만 좁게 쓴다.
  • useLayoutEffect는 브라우저 페인트를 막는 동기 작업이므로 무거우면 첫 화면 표시가 느려진다.
  • React 18 개발 모드에서는 Strict Mode 때문에 Effect가 두 번 실행된 것처럼 보일 수 있어 타이밍 로그를 해석할 때 주의해야 한다.

현재 초안의 구조는 이미 잡혀 있어서, React 공식 문서와 MDN 기준으로 실행 시점 표현만 다시 확인한 뒤 발행용 문장으로 정리하겠습니다. 비교 섹션과 시간축 섹션은 초반에 더 압축하고, 오래된 설명으로 보일 수 있는 부분은 현재 기준 용어로 바로잡겠습니다.공식 문서 기준선은 반영했습니다. 현재 React 문서는 react.dev의 최신 메이저인 React 19.2 기준이고, 오래된 글에서 자주 보이는 “항상 페인트 후”, “DOM 업데이트 직전” 같은 표현은 지금 문서의 설명과 어긋나는 부분이 있어 그 차이를 본문에 녹여 정리하겠습니다.# React에서 useEffect와 useLayoutEffect, 언제 무엇을 써야 할까

한눈에 비교

실행 시점
useLayoutEffect 는 DOM 커밋 직후, 브라우저 리페인트 전에 실행된다. useEffect 는 대체로 페인트 뒤에 실행된다.
브라우저 영향
useLayoutEffect 는 현재 프레임의 표시를 막을 수 있다. useEffect 는 이미 그려진 화면 뒤에서 이어지는 경우가 많다.
주로 맡는 일
useEffect 는 외부 시스템과의 동기화에 가깝고, useLayoutEffect 는 레이아웃 측정과 시각 보정에 가깝다.
잘못 골랐을 때 증상
위치 보정을 useEffect 에 두면 한 프레임 깜빡이거나 점프할 수 있고, 무거운 일을 useLayoutEffect 에 두면 초기 표시가 밀릴 수 있다.
서버 렌더링
두 훅 모두 서버에서는 실행되지 않는다. 특히 useLayoutEffect 는 서버에 레이아웃 정보가 없기 때문에 초기 HTML 전략과 잘 맞지 않는 경우가 많다.

시간 흐름으로 이해하기

렌더 단계
React가 컴포넌트를 실행해 다음 UI를 계산한다.
커밋 단계
계산된 변경이 실제 DOM에 반영된다.
useLayoutEffect
이 시점에 DOM을 읽고 위치를 보정할 수 있다.
브라우저 그리기
DOM과 CSSOM으로 렌더 트리를 만들고, 레이아웃, 페인트, 컴포지팅을 거쳐 프레임을 표시한다.
useEffect
보통 그 다음에 외부 동기화 작업이 이어진다.

왜 둘 다 렌더 후처럼 보일까

헷갈리는 이유는 React의 렌더와 브라우저의 렌더를 같은 말로 부르기 때문이다. React에서 렌더는 컴포넌트 함수를 실행해 어떤 UI가 필요한지 계산하는 과정에 가깝다. 반면 브라우저에서 렌더는 DOM과 CSSOM을 바탕으로 렌더 트리를 만들고, 레이아웃으로 크기와 위치를 정한 뒤, 페인트와 컴포지팅으로 실제 픽셀을 화면에 올리는 과정이다.

useEffectuseLayoutEffect는 둘 다 React의 렌더 함수 실행 중에는 동작하지 않는다. 둘 다 DOM 커밋 이후에 실행되지만, 브라우저가 이번 프레임을 그리기 전인지 후인지가 갈린다. 이 경계 때문에 같은 “렌더 후 실행”이라는 표현도 체감이 전혀 달라진다.

오래된 설명 중에는 useLayoutEffect를 “DOM 업데이트 직전”이라고 적은 경우가 있다. 현재 기준으로는 “DOM 커밋 직후, 브라우저 리페인트 전”이 더 정확하다. 이 차이를 바로잡아 두면 왜 어떤 코드는 깜빡이고, 어떤 코드는 첫 화면이 느려지는지 이해하기 쉬워진다.

어떤 작업에 무엇을 써야 하나

useEffect는 화면을 그리는 일보다 외부 시스템과의 연결이 중심인 작업에 어울린다. 네트워크 요청, 웹소켓 연결, 브라우저 이벤트 구독, 타이머 등록, 로그 전송 같은 일이 여기에 들어간다. 이런 작업은 지금 프레임의 레이아웃을 맞추기 위해 페인트를 막을 이유가 거의 없다.

프레임워크가 데이터 로더나 Server Components를 제공한다면 그 경로가 먼저일 수 있다. 그래도 브라우저에서 마운트 이후 외부 요청을 붙여야 하는 컴포넌트라면 useEffect가 자연스럽다.

import { useEffect, useState } from 'react';

function Profile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    const controller = new AbortController();

    fetch(`/api/users/${userId}`, { signal: controller.signal })
      .then((response) => response.json())
      .then((data) => setUser(data))
      .catch((error) => {
        if (error.name !== 'AbortError') {
          console.error(error);
        }
      });

    return () => controller.abort();
  }, [userId]);

  return user ? <p>{user.name}</p> : <p>Loading...</p>;
}

반대로 useLayoutEffect는 DOM의 실제 크기나 위치를 읽고, 그 결과를 바로 현재 프레임에 반영해야 할 때 의미가 생긴다. getBoundingClientRect(), offsetHeight, scrollTop처럼 레이아웃 정보에 닿는 읽기 작업이 대표적이다. 툴팁과 팝오버 위치 계산, 스크롤 점프 보정, 첫 표시 전에 끝내야 하는 포커스 이동이 여기에 가깝다.

여기서 한 번 더 볼 점도 있다. 단순 계산이나 CSS로 풀 수 있다면 Effect 자체가 필요 없을 수 있다. 공간을 미리 예약하거나, topleft 대신 transformopacity로 처리할 수 있다면 레이아웃 비용을 줄이고 컴포지팅 단계에 더 가깝게 가져갈 수 있다.

깜빡임이 생기는 예제와 해결 방식

위치 보정을 useEffect에 두면 화면에 한 번 잘못된 상태가 보였다가 다시 움직이는 일이 생길 수 있다. 아래 코드는 팝오버를 일단 렌더한 뒤 높이를 읽어서 위쪽으로 옮긴다.

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

function Popover({ anchorRect, children }) {
  const ref = useRef(null);
  const [top, setTop] = useState(anchorRect.bottom);

  useEffect(() => {
    if (!ref.current) return;

    const { height } = ref.current.getBoundingClientRect();
    setTop(anchorRect.top - height);
  }, [anchorRect]);

  return (
    <div ref={ref} style={{ position: 'fixed', top, left: anchorRect.left }}>
      {children}
    </div>
  );
}

이 코드는 React가 DOM을 커밋하고 브라우저가 프레임을 그린 뒤에 useEffect가 돌 수 있으므로, 사용자는 잠깐 아래쪽에 나타난 팝오버가 위로 점프하는 장면을 볼 수 있다.

같은 작업을 useLayoutEffect로 옮기면 측정과 재렌더가 리페인트 전에 끝나므로 이런 깜빡임을 줄일 수 있다.

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

function Popover({ anchorRect, children }) {
  const ref = useRef(null);
  const [top, setTop] = useState(anchorRect.bottom);

  useLayoutEffect(() => {
    if (!ref.current) return;

    const { height } = ref.current.getBoundingClientRect();
    setTop(anchorRect.top - height);
  }, [anchorRect]);

  return (
    <div ref={ref} style={{ position: 'fixed', top, left: anchorRect.left }}>
      {children}
    </div>
  );
}

다만 이 방식은 공짜가 아니다. DOM을 쓴 직후 레이아웃을 읽고, 다시 상태를 바꾸면 브라우저는 스타일 재계산과 레이아웃을 즉시 수행해야 할 수 있다. 이 패턴이 커지면 깜빡임은 줄어도 첫 표시가 무거워질 수 있다. 그래서 useLayoutEffect는 범위를 작게 두고, 읽기와 쓰기를 최소 횟수로 묶는 쪽이 보통 더 낫다.

React 19.2 기준으로 다시 볼 점

2026년 4월 기준 React 공식 문서는 최신 메이저를 React 19.2로 안내한다. 이 기준에서 useEffect 문서useLayoutEffect 문서를 같이 보면 오래된 설명과 다른 지점이 몇 가지 분명하다.

  • 오래된 설명: useEffect는 항상 페인트 후 실행된다.
  • 현재 문서: 상호작용이 아닌 경우에는 대체로 페인트 뒤지만, 클릭 같은 상호작용 이후에는 더 이른 시점에 관찰될 수도 있다.
  • 오래된 설명: useLayoutEffect는 DOM 업데이트 직전 실행된다.
  • 현재 문서: DOM 커밋 이후, 브라우저가 다시 그리기 전에 실행된다.
  • 오래된 패턴: 스타일 삽입 순서를 맞추려고 useLayoutEffect를 쓰기도 했다.
  • 현재 기준: CSS-in-JS 라이브러리의 스타일 삽입은 useInsertionEffect로 역할이 분리됐다.

또 하나 자주 만나는 차이는 개발 모드의 StrictMode다. React 18 이후 문서 기준으로 개발 환경에서는 Effect의 setup과 cleanup을 한 번 더 실행해 정리 로직의 누락을 드러낸다. 로그가 두 번 찍힌다고 해서 곧바로 프로덕션 버그라고 보기보다, cleanup이 setup과 대칭인지 먼저 확인하는 편이 해석에 도움이 된다. 관련 설명은 StrictMode 문서에서 확인할 수 있다.

SSR과 하이드레이션에서의 처리

useLayoutEffect는 서버에서 의미를 가지기 어렵다. 서버에는 실제 레이아웃 정보가 없고, 브라우저가 아직 DOM을 그리지도 않았기 때문이다. 그래서 현재 React 문서에는 아예 “useLayoutEffect does nothing on the server”라는 트러블슈팅 항목이 따로 있다.

레이아웃 정보가 꼭 필요한 컴포넌트라면, 처음부터 서버 HTML에 정확한 위치를 담으려 하기보다 클라이언트에서만 보이게 하거나 하이드레이션 이후에 렌더하는 편이 더 자연스러운 경우가 많다. Server Components 기반 프레임워크에서는 이 훅을 쓰는 컴포넌트가 클라이언트 경계 안에 있어야 한다.

'use client';

import { useEffect, useState } from 'react';

export default function ClientOnlyPopover(props) {
  const [mounted, setMounted] = useState(false);

  useEffect(() => {
    setMounted(true);
  }, []);

  if (!mounted) return null;
  return <Popover {...props} />;
}

이 방식은 서버와 하이드레이션 단계에서는 레이아웃 의존 컴포넌트를 숨기고, 클라이언트 마운트 이후에만 실제 측정 로직을 돌리게 한다. 툴팁, 드롭다운, 측정 기반 오버레이처럼 사용자 상호작용 뒤에 나타나는 UI와 잘 맞는 편이다.

성능과 DevTools에서 확인할 지점

이 주제는 브라우저 렌더링 파이프라인과 직접 연결되기 때문에 DevTools에서 보면 차이가 더 분명해진다. 브라우저 관점에서 비용이 커지는 순간은 대개 “DOM 쓰기 → 레이아웃 읽기 → 다시 쓰기”가 같은 프레임 안에서 이어질 때다. 이때 Recalculate Style, Layout, Paint 구간이 길어지고, 애니메이션이나 상호작용이 둔해질 수 있다.

확인 순서는 비교적 단순하다.

  1. Chrome DevTools Performance panel로 문제 동작을 녹화한다.
  2. 타임라인에서 Recalculate Style, Layout, Paint가 길어지는 구간이 useLayoutEffect가 도는 순간과 겹치는지 본다.
  3. Performance monitor에서 Layouts / sec, Style recalculations / sec가 갑자기 늘어나는지 본다.
  4. Rendering 탭의 paint flashing으로 불필요한 repaint를 확인한다.
  5. 레이어 분리가 의심되면 Layers panel에서 컴포지팅 상태를 함께 본다.

브라우저 쪽 배경은 MDN의 Critical Rendering Path 문서를 같이 보면 흐름이 잘 맞는다. DOM과 CSSOM이 렌더 트리로 합쳐지고, 레이아웃으로 위치와 크기가 정해진 뒤, 페인트와 컴포지팅을 거쳐 화면에 나온다는 설명이 useLayoutEffect의 위치를 이해하는 데 바로 연결된다.

선택 기준을 짧게 남기면 이 정도다. 지금 프레임이 한 번 잘못 보여도 괜찮다면 useEffect 쪽이 부담이 적고, 그 한 프레임조차 보이면 안 되는 레이아웃 보정이라면 useLayoutEffect를 검토할 수 있다. 그래도 먼저 살펴볼 것은 “정말 Effect가 필요한가”, “CSS나 레이아웃 구조 변경으로 해결할 수 있는가”다. 브라우저 파이프라인에 덜 개입하는 쪽이 대체로 더 단순하고 가볍다.

원문 참고

https://www.maeil-mail.kr/question/46

댓글

이 블로그의 인기 게시물

아이콘 폰트 (icomoon 사용법)

 장난감 프로젝트를 만들다 보면, 아이콘이 필요한 경우가 있다. 간단하게 아이콘을 인터넷에서 검색하여, 이미지로 넣어두고 이미지 태그를 이용하여, 사용하는 경우가 일반적이였지만...  요즘에는 대부분 폰트를 이용하여 아이콘을 노출 한다. 나 같은 경우에도 기본적으로  https://material.io/resources/icons 를 참고하여 아이콘 폰트를 이용할 수 있도록 처리하고, 추가적으로 필요한 아이콘이고, 일상적으로 사용 되지 않는 아이콘의 경우에는  https://icomoon.io 에서 제작하여, 아이콘 폰트로 이용 하곤 한다.  그래서 이번에는 아이콘  https://icomoon.io 의 사용법을 간단히 공유하고자 한다.   들어가자 마자 위의 icoMoonApp버튼을 누르면 아래와 같은 화면이 나타난다.  icomoon에서 무료로 제공하는 아이콘들이 보이면 위에 파란색으로 표시 되어있는 집 모양 세가지를 선택한 후, 아래의 빨간색으로 표시되어있는 Generate Font를 눌러보자.  그리고 나서 바로 다운로드를 요청해보자. icomoon.zip이 다운로드가 될텐데, 압축을 해제해 보면, 아래의 폴더 및 파일들이 있다. 아래에서 중요한 것은 font 폴더와 style.css이다. demo-files fonts demo.html Read Me.txt selection.json style.css <!doctype html > <html> <head> <link rel ="stylesheet" href ="style.css" ></head> </head> <body> <span class ="icon-home" ></span> <span class ="icon-home2" ></span> <span class ="icon-hom...

Chart js와 amchart 비교

Chart js 특징은 위의 그림으로 대체 할 수 있을 듯 하다. 오픈 소스이고, 기본으로 제공하는 차트 종류가 8가지 Canv a s를 이용해서 차트를 그리고, 반응형을 지원한다. amchart amchart는 기본적으로 유료이며, 기본으로 제공하는 차트 종류가 기본적인 차트 + 주식 처럼 보이는 차트 + 지도에 관련된 차트(?) 까지 하면, 기본 제공 하는 종류가 20개 내외 이려나, 일일이 세기에는 양이 좀 많아 보인다. 렌더링은 svg를 통하여 그려지고, 당연 반응형도 지원이 된다. 그러면, 이 둘중에 어떤것이 내 프로젝트에 적합 하냐는 것이 문제이다. 일단, 주식 처럼 보이는 차트나 지도에 관련된 차트(?)가 필요하면, amchart를 선택해야 되는 것은 맞다. 그건 당연한 것이니 빼고 얘기 해보자! 여러 종류의 차트가 필요하다면, 일단은 amchart를 염두해 두는 것이 좋다. 돈 낸 만큼은 하는 듯 하다. 하지만, 기본적인 막대 그래프, 도넛 차트 등, 아주 기본적인 차트들인데, Chart js도 amchart도 그러한 차트가 없을 때가 문제가 된다. 그렇다면, 조금이라도 커스텀이 용이한 것을 찾는 것이 좋을 것이다.  일단 amchart에서 custom이라고 검색 하였을 때, 검색 결과가 61가지가 나온다. 차트의 종류도 많고, 각 차트마다 들어가는 속성이 매우 많기 때문에, 웬만한 내용들은 속성 값을 어떻게 주느냐에 따라서 변경이 가능 하게 된다. 커스텀의 예를 들면, 기본적으로 도넛 파이의 형태를 띄면서, 화살표로 목표를 표시해주는 차트가 필요하다고 생각 해보자. 이것은 amchart로 만든 그래프이고 이것은 chart js로 만든 그래프이다. 모양이 살짝 다르긴 하지만, 완벽하게 똑같이 구현 할 수도 있다. amchart로 만든 그래프의 경우, 저것은 도넛그래프가 아닌 guage 그래프이다. 원래 게이지 그래프는 이와 같...

javascript 압축 파일 다운로드

이번에는 전 게시글의 응용판? 이라고 해야하나....? 어쨋든! 우리는 각각의 파일들을 다운로드 해보았다. 그런데 생각보다 귀찮음?을 느꼇을 것이다. 파일을 각각 다운 받아야 한다는 현실때문에! 그래 파일 두개야 뭐 그렇다 치지... 하지만, 개발자도 사용자도 게으름뱅이이다. 자 결국, 우리가 해야 하는 것은 파일을 한 번에 둘다! 다운 받는 것이다. 물론, 클릭 한번에 여러개의 함수를 엮어서 다운받게 하면 되지만! 크롬에서 자주 봤듯이, 여러개의 파일을 다운로드를 시도하면 <- 여러개의 파일을 다운로드 합니다. 허용 합니까? 하고 물어보는 것을 볼 수 있다. 게다가 다운로드 한 파일들을 찾기도 귀찮다는 것. 자 해결책을 제시해보자면, https://github.com/Stuk/jszip 클라이언트 단에서 파일을 zip파일로 압축을 할 수가 있다! 필요한 작업은 아래와 같다. 0. 데이터 준비 1. BLOB(binary large object)를 만든다. 2. Blob을 URL.createObjectURL을 사용하여, 해당 binary의 주소를 생성. 3. 다운로드가 필요한 파일들을 Zip 객체에 셋팅! 4. a태그를 이용하여, 해당 url 셋팅 하고, 다운로드. 전 게시물과 별로 달라진게 없네... 자 그럼 샘플! 샘플을 보자! http://embed.plnkr.co/NMprnRxqYG0fkHa2J55D/ var util = {} function fixBinary(bin) { //binary to arrayBuffer var length = bin.length var buf = new ArrayBuffer(length) var arr = new Uint8Array(buf) for (var i = 0; i < length; i++) { arr[i] = bin.charCodeAt(i) } return buf } window.onload = function() { ...