useEffect와 useLayoutEffect는 모두 React가 변경 사항을 DOM에 반영한 뒤 실행돼요. 핵심 차이는 Effect가 browser paint를 막는지에 있어요.
대부분의 작업에는 useEffect가 적합해요. DOM의 크기나 위치를 측정한 결과를 첫 paint 전에 반영해야 할 때만 useLayoutEffect를 선택하는 편이 안전해요.
한눈에 비교하기
| 기준 | useEffect | useLayoutEffect |
|---|---|---|
| 주된 목적 | 외부 시스템과 컴포넌트 동기화 | paint 전 DOM 측정과 화면 보정 |
| 실행 시점 | commit 이후, 일반적으로 paint 이후 | commit 이후, paint 이전 |
| browser paint | 차단하지 않음 | Effect와 내부 state update가 끝날 때까지 차단 |
| 대표 사례 | event listener, timer, 외부 widget, network 연결 | tooltip 위치, element 크기 측정, scroll 위치 보정 |
| SSR | 실행되지 않음 | 실행되지 않으며 server에는 layout 정보가 없음 |
| 기본 선택 | 우선 사용 | 꼭 필요한 시각적 작업에만 사용 |
두 Hook은 dependency와 cleanup 규칙이 같아요. dependency가 바뀌면 이전 cleanup을 먼저 실행한 다음 새로운 setup을 실행해요. 컴포넌트가 DOM에서 제거될 때도 마지막 cleanup이 실행돼요.
render, commit, paint 흐름
실행 시점을 정확히 이해하려면 React의 render와 commit, browser의 paint를 구분해야 해요.
- Render: React가 컴포넌트를 호출해 화면에 무엇을 표시할지 계산해요.
- Commit: 계산한 결과를 실제 DOM에 반영해요.
- Paint: browser가 변경된 DOM과 style을 화면에 그려요.
Effect까지 포함해 흐름을 단순화하면 다음과 같아요.

useLayoutEffect는 DOM이 갱신된 다음 실행되므로 element의 크기와 위치를 측정할 수 있어요. React는 Effect와 그 안에서 발생한 state update를 모두 처리한 뒤 browser가 화면을 그리도록 보장해요.
반면 useEffect가 항상 paint 뒤에 실행된다고 이해하면 정확하지 않아요. interaction과 무관하게 발생한 Effect라면 React는 일반적으로 browser가 먼저 화면을 그리도록 해요. 클릭 같은 interaction에서 시작된 Effect는 event system이 결과를 관찰할 수 있도록 paint 전에 실행될 수도 있어요. 따라서 정확한 순서를 외우기보다 paint 전에 작업을 반드시 끝내야 하는가를 기준으로 선택하는 편이 좋아요.
useLayoutEffect는 동기, useEffect는 비동기라고만 구분하는 것도 정확하지 않아요. 두 setup 함수의 문법이 자동으로 동기 또는 비동기로 바뀌는 것은 아니에요. React가 commit과 paint 사이에서 Effect를 언제 처리하는지, 그 과정에서 paint를 차단하는지가 핵심이에요.
useEffect를 사용하는 경우
useEffect는 React 바깥의 시스템과 컴포넌트를 동기화할 때 사용해요. browser event, timer, network connection, third-party widget처럼 React가 직접 관리하지 않는 대상이 여기에 해당해요.
아래 컴포넌트는 Escape key event를 구독해요. 컴포넌트가 사라지거나 onEscape가 바뀌면 기존 event listener를 제거해 중복 실행과 memory leak을 막아요.
import { useEffect } from "react";
type PropsWithEscapeKeyListener = {
onEscape: () => void;
};
export function EscapeKeyListener({ onEscape }: PropsWithEscapeKeyListener) {
useEffect(() => {
function handleKeyDown(event: KeyboardEvent) {
if (event.key === "Escape") onEscape();
}
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [onEscape]);
return null;
}이 작업은 다음 paint 전에 끝나야 할 이유가 없어요. useLayoutEffect로 바꾸더라도 결과는 같지만 browser paint만 불필요하게 막게 돼요.
client에서 API를 직접 호출해야 한다면 useEffect를 사용할 수 있어요. 다만 Next.js 같은 framework에서는 server data fetching이나 route loader, cache library를 먼저 검토하는 편이 좋아요. Effect에서 직접 요청할 때는 컴포넌트가 사라지거나 dependency가 바뀐 뒤 이전 응답이 최신 state를 덮어쓰지 않도록 cleanup도 함께 설계해야 해요.
useLayoutEffect가 필요한 경우
useLayoutEffect는 DOM을 측정한 결과를 첫 paint에 바로 반영해야 할 때 필요해요. 대표적인 사례는 tooltip이나 popover의 위치 계산이에요.
아래 예제는 target과 tooltip의 크기를 측정한 뒤 tooltip을 target 위에 배치해요. 위쪽 공간이 부족하면 target 아래에 표시해요.
import type { ReactNode } from "react";
import { useLayoutEffect, useRef, useState } from "react";
type PropsWithTooltip = {
children: ReactNode;
target: HTMLElement | null;
};
type Position = {
left: number;
top: number;
};
export function Tooltip({ children, target }: PropsWithTooltip) {
const tooltipRef = useRef<HTMLDivElement>(null);
const [position, setPosition] = useState<Position | null>(null);
useLayoutEffect(() => {
if (!target || !tooltipRef.current) return;
const targetRect = target.getBoundingClientRect();
const tooltipRect = tooltipRef.current.getBoundingClientRect();
const top = targetRect.top - tooltipRect.height - 8;
setPosition({
left: targetRect.left + (targetRect.width - tooltipRect.width) / 2,
top: top >= 8 ? top : targetRect.bottom + 8,
});
}, [target]);
return (
<div
ref={tooltipRef}
role="tooltip"
style={{
position: "fixed",
left: position?.left ?? 0,
top: position?.top ?? 0,
visibility: position ? "visible" : "hidden",
}}
>
{children}
</div>
);
}이 과정에서는 render가 두 번 일어나요.
- 위치를 모르는 상태로 tooltip을 DOM에 반영해요.
useLayoutEffect에서 실제 크기와 위치를 측정해요.- 측정 결과를 state에 저장하고 즉시 다시 render해요.
- browser는 최종 위치에 놓인 tooltip만 화면에 그려요.

같은 코드를 useEffect에 넣으면 browser가 tooltip의 초기 위치를 먼저 그릴 수 있어요. 측정이 끝난 뒤 tooltip이 올바른 위치로 이동하면서 화면이 순간적으로 튀거나 깜빡여 보일 수 있어요.
CSS만으로 위치를 정할 수 있거나 첫 frame의 변화가 눈에 띄지 않는다면 useEffect로도 충분해요. DOM을 읽는다는 이유만으로 useLayoutEffect를 선택할 필요는 없어요.
위 예제는 두 Hook의 차이를 보여주기 위해 초기 위치만 계산해요. 실제 tooltip은 viewport resize, scroll, 내용 크기 변경에도 대응해야 해요. production code에서는 검증된 positioning library나 ResizeObserver 같은 browser API도 함께 검토하는 편이 좋아요.
useLayoutEffect를 피해야 하는 이유
useLayoutEffect 안의 코드와 여기서 발생한 state update는 browser paint를 막아요. 큰 DOM tree를 반복해서 측정하거나 오래 걸리는 연산을 실행하면 첫 화면과 interaction 반응이 늦어질 수 있어요.
useLayoutEffect(() => {
const startedAt = performance.now();
while (performance.now() - startedAt < 1000) {
// 이 작업이 끝날 때까지 browser paint가 지연돼요.
}
}, []);이런 계산은 useEffect로 옮긴다고 자동으로 빨라지지 않아요. render에 필요한 순수 계산은 render 단계에서 처리하고, 비용이 큰 계산에는 memoization이나 Web Worker 같은 별도의 최적화 전략을 검토해야 해요. useLayoutEffect에는 DOM 측정과 즉시 반영해야 하는 최소한의 state update만 남기는 편이 좋아요.
theme과 redirect에는 무엇을 써야 할까?
화면 깜빡임을 막는다는 이유로 모든 style 변경이나 redirect에 useLayoutEffect를 적용하면 안 돼요.
초기 테마
client rendering에서는 useLayoutEffect로 theme class를 바꾸면 첫 paint 전에 변경 사항이 반영되므로 깜빡임을 막을 수 있어요.
하지만 server rendering에서는 server HTML이 JavaScript보다 먼저 paint될 수 있어요. useLayoutEffect는 hydration 과정에서 실행되므로, localStorage의 theme을 읽는 작업만 맡기면 이미 표시된 초기 화면까지 되돌릴 수는 없어요.
초기 theme은 CSS의 prefers-color-scheme, server에서 만든 HTML class, hydration 전에 실행하는 짧은 초기화 script로 맞추는 편이 안전해요. static export처럼 request마다 theme을 server에서 결정할 수 없다면 <head>의 동기 script로 DOM class를 먼저 적용할 수 있어요. 사용자가 theme button을 누른 뒤 class를 바꾸는 동작은 event handler에서 직접 처리하면 돼요.
Redirect와 접근 제어
redirect는 router나 framework의 route 단계에서 처리하는 것이 우선이에요. 특히 로그인 여부처럼 접근 제어와 관련된 판단은 server나 middleware에서 검증해야 해요. useLayoutEffect로 화면을 가린 뒤 이동하는 방식은 보안 경계가 될 수 없어요.
client에서만 알 수 있는 값을 기준으로 이동해야 한다면 useEffect와 조건부 render를 함께 사용할 수 있어요. 이때 이동하기 전까지 감춰야 하는 내용은 JSX에 포함하지 않는 것이 중요해요.
SSR과 Next.js에서 주의할 점
useEffect와 useLayoutEffect는 모두 client에서만 실행돼요. Next.js App Router에서 두 Hook을 사용하는 컴포넌트는 "use client"로 client component 경계를 선언해야 해요.
server에는 viewport나 element 크기 같은 layout 정보가 없어요. server에서 만든 초기 HTML이 useLayoutEffect의 측정 결과에 의존한다면 hydration이 끝난 뒤 위치가 바뀌거나 경고가 발생할 수 있어요.
이 문제가 생기면 다음 순서로 검토해요.
- layout 측정 없이 CSS로 해결할 수 있는지 확인해요.
- 첫 화면에 측정 결과가 필요하지 않다면
useEffect로 바꿔요. - interaction 이후에만 필요한 tooltip처럼 초기 HTML에 없어도 되는 UI는 client에서 열릴 때 render해요.
- client 전용 UI라면 크기가 정해진 placeholder를 제공해 layout shift를 줄여요.
개발 환경에서 Strict Mode를 사용하면 React는 첫 번째 실제 setup 전에 setup → cleanup → setup 과정을 한 번 더 실행해요. Effect가 두 번 실행되는 것처럼 보인다면 Hook을 바꾸기보다 cleanup이 setup을 정확히 되돌리는지 먼저 확인해야 해요.
선택 기준
Effect를 작성하기 전에 아래 순서로 판단하면 선택이 단순해져요.
- React 바깥의 시스템과 동기화하는 작업이 아니라면 Effect 자체가 필요한지 먼저 확인해요.
- event listener, timer, network connection, 외부 widget 연동에는
useEffect를 사용해요. - DOM 측정 결과를 첫 paint 전에 반영해야 하고 실제로 깜빡임이 보인다면
useLayoutEffect를 사용해요. - CSS나 router, framework 기능으로 해결할 수 있다면 Effect보다 그 방법을 우선해요.
useLayoutEffect를 선택했다면 실행 시간을 짧게 유지하고 layout read와 write를 반복하지 않아요.
결론은 단순해요. 기본은 useEffect이고, paint 전에 끝내야 하는 layout 측정에만 useLayoutEffect를 사용해요. 잘못된 첫 frame이 사용자에게 보이는지, 그리고 browser paint를 막을 분명한 이유가 있는지를 기준으로 판단하면 돼요.