React 를 사용하다 보면, useEffect 와 useLayoutEffect 의 차이를 알고는 있지만, 왜 존재하는 지, 내부적으로 어떻게 다르게 동작하는지까지 명확히 설명하기는 쉽지 않습니다. 이 글에서는 React 가 렌더링을 어떤 단계로 나누어 처리하는 지, 두 훅이 어느 단계에 개입하는 지, 그 차이가 성능과 UX 에 어떤 영향을 주는지 정리해보고자 합니다.
두 hook 의 차이를 이해하려면, 먼저 React 가 화면을 그리는 전체 흐름을 알아야 합니다.
// React 렌더링의 큰 흐름
상태 변경
↓
Render Phase (Virtual DOM 계산)
↓
Commit Phase (DOM 변경 반영)
↓
브라우저 Paint
여기서 중요한 점은 Render Phase 와 Commit Phase 가 분리되어 있다는 것입니다.
useEffect 와 useLayoutEffect 는 모두 Commit Phase 이후에 실행되지만, 브라우저 Paint 기준으로 서로 다른 위치에 실행됩니다.
Render
↓
DOM 변경 (Commit)
↓
useLayoutEffect 실행 (동기)
↓
브라우저 Paint
핵심 특징으론 DOM이 이미 업데이트된 상태에서 실행되고, Paint 이전에 동기적으로 실행되며,
effect 가 끝날 때까지 브라우저는 화면을 그리지 않습니다.
즉, useLayoutEffect 는 "화면이 그려지기 직전에 DOM을 마지막으로 조정하는 hook" 입니다.
이로 인해 DOM 크기 / 위치 측정 (getBoundingClientRect) 또는 측정 결과로 즉시 스타일 수정, 스크롤 위치 보정등의 작업은 Paint 이후에 실행되면 한 프레임이라도 잘못된 화면이 노출될 수 있어 useLayoutEffect 에서 처리하는 경우가 많습니다.
Render
↓
DOM 변경 (Commit)
↓
브라우저 Paint
↓
useEffect 실행 (비동기)
핵심 특징으론 화면이 이미 사용자에게 보여진 뒤 실행된다는 점, 브라우저 페인팅을 절대 블로킹하지 않는다는 점, 상대적으로 안전하고 성능 친화적이라는 점입니다. React 팀이 useEffect 를 기본 hook 으로 권장하는 이유도 같습니다.
import { useEffect } from "react";
useEffect(() => {
const rect = ref.current!.getBoundingClientRect();
setLeft(calcLeft(rect));
}, []);
첫 Paint 는 보정되지 않은 상태로 렌더링 -> effect 실행 후 상태 변경되어 재렌더링
즉, 사용자는 깜빡임(flicker)을 경험할 수 있습니다.
import { useLayoutEffect } from "react";
useLayoutEffect(() => {
const rect = ref.current!.getBoundingClientRect();
setLeft(calcLeft(rect));
}, []);
Paint 전에 측정 및 보정 완료되어 사용자는 완성된 화면만 보게 되며, 이 차이가 useLayoutEffect 가 존재하는 이유입니다.
여기서 주의할 점은 resize 이벤트에서 width 를 체크해야 하는 경우,
일반적인 경우 (브레이크포인트 계산, 조건부 렌더링, 반응형 UI 분기) 에는 useEffect 가 맞습니다.
resize 이벤트 자체가 paint 이후 발생하고, 다음 렌더에서 반영되어도 UX 에 문제가 없기 때문입니다.
다만, 예외적인 경우 (resize 시 DOM 실제 크기를 재고, 그 결과로 즉시 레이아웃 수정해야 되는 경우) 에는
useLayoutEffect 을 사용하면 됩니다.
useLayoutEffect는은 Paint 를 블로킹하고, 무거운 로직이 있으면 체감 성능이 저하될 수 있습니다.
그래서 React 공식 문서에서도 "useLayoutEffect should be used sparingly." 와 같이 명시합니다.
또한 useLayoutEffect 는 브라우저 전용 훅으로서, 실무에서는 다음 패턴을 자주 사용합니다.
import {useEffect, useLayoutEffect} from "react";
const useIsomorphicLayoutEffect =
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
판단 기준은 이 effect 가 브라우저 Paint 를 막아야 하는가? 입니다.
대부분의 경우 useEffect 로 충분하고, 사용자가 보게 될 첫 화면을 정확히 제어하여 보호해야 할 때만
useLayoutEffect 를 선택하는 것이 가장 안전한 전략입니다.