⏱️ useEffect vs useLayoutEffect 완벽 정리 - 실행 시점과 사용법
리액트는 컴포넌트 렌더링 과정에서 발생하는 부수 효과(side effect)를 처리하기 위해 useEffect 와 useLayoutEffect 훅을 제공한다.
둘 다 렌더링 이후에 실행되지만 실행 시점과 사용 목적에서 차이가 있다.
1) useEffect
- 렌더링 결과가 실제 DOM에 반영되고 브라우저가 화면을 그린 이후 실행된다.
- 실행이 비동기적으로 예약되기 때문에 렌더링 과정을 막지 않고 UI가 먼저 사용자에게 보인다.
2) useLayoutEffect
- 렌더링 후 DOM 업데이트 직후 브라우저가 화면을 그리기 전에 동기적으로 실행된다.
- 실행이 끝날 때까지 브라우저는 페인트를 멈추므로 화면 깜빡임(flicker)을 방지할 수 있다.
따라서 useEffect는 데이터 가져오기(fetching), 이벤트 리스너 등록/해제, 로그 기록처럼 화면에 즉각적인 레이아웃 변경이 필요 없는 작업에 적합하다.
실행 시점이 브라우저 페인트 이후이기 때문에 UI를 먼저 사용자에게 보여주고 그 뒤에 작업이 이루어진다.
import { useEffect, useState } from "react";
function PostList() {
const [posts, setPosts] = useState([]);
// 컴포넌트가 렌더링 된 후 실행
useEffect(() => {
fetch("https://bluecool.pyomin.com/posts")
.then((res) => res.json())
.then((data) => setPosts(data));
}, []);
return (
// ...렌더링
);
}
export default PostList;
반면 useLayoutEffect는 DOM의 크기나 위치를 측정한 후 레이아웃을 보정해야 할 때 혹은 애니메이션 초기화 같은 상황에서 유용하다.
DOM이 실제로 준비된 직후 화면에 그리기 전에 실행되므로 UI에 직접적이고 즉각적인 영향을 주는 작업에 유용하다.
import { useLayoutEffect, useRef, useState } from "react";
function Box() {
const boxRef = useRef(null);
const [width, setWidth] = useState(0);
// DOM이 업데이트된 직후 브라우저가 그리기 전에 실행
useLayoutEffect(() => {
if (boxRef.current) {
const rect = boxRef.current.getBoundingClientRect();
setWidth(rect.width);
}
}, []);
return (
<div>
<div
ref={boxRef}
style={{ width: "50%", height: "100px", background: "lightblue" }}
>
박스
</div>
<p>박스의 너비: {width}px</p>
</div>
);
}
export default Box;
브라우저 흐름을 단순화하면 아래와 같다.
1. 리액트가 DOM 변경 (가상 DOM → 실제 DOM 업데이트)
2. useLayoutEffect 실행 (동기)
3. 브라우저 화면에 그리기 (paint)
4. useEffect 실행 (비동기)
주의할 점은 useLayoutEffect의 경우 동기적으로 실행되기 때문에 너무 남용할 경우 렌더링이 느려질 수 있다. 따라서 기본적으로 useEffect를 사용하되 레이아웃에 영향을 주는 작업만 useLayoutEffect로 처리하는 것이 좋다.