useEffect는 렌더링 결과가 실제 DOM에 반영되고 브라우저가 화면을 그린 이후 실행되는 effect이다. 리액트에서는 이를 passive effect라고 부른다.
[특징]
이러한 특성 때문에 데이터 fetching, 이벤트 리스너 등록 및 해제와 같이 레이아웃에 즉각적인 영향을 주지 않는 작업에 적합하다.
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;
이 경우 UI가 먼저 렌더링된 뒤 데이터를 가져와 상태를 업데이트한다.
useLayoutEffect는 DOM 업데이트가 완료된 직후 브라우저가 화면을 그리기 전에 동기적으로 실행되는 effect이다. 리액트에서는 이를 layout effect라고 부른다.
[특징]
따라서 DOM의 크기나 위치를 측정하거나 레이아웃을 보정하는 작업에 유용하다.
useLayoutEffect 내부에서 상태 업데이트가 발생하면 브라우저 paint 이전에 동기적인 재렌더링이 발생한다.
예를 들어 다음 코드가 있다고 가정해보자.
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. 첫 번째 렌더링 수행 (width = 0)
2. DOM 업데이트 완료
3. useLayoutEffect 실행
4. setWidth(rect.width) 호출
5. 브라우저 paint 이전에 React가 동기적으로 재렌더링
6. 최종 DOM 업데이트
7. 브라우저가 화면을 paint
결과적으로 사용자는 width가 0인 상태를 보지 않게 되므로 깜빡임이 발생하지 않는다.
리액트 렌더링 과정을 단순화하면 다음과 같다.
즉 useLayoutEffect는 paint 이전, useEffect는 paint 이후 실행된다. 단 React 18에서는 클릭이나 키보드 입력과 같은 이산적 이벤트로 인해 업데이트가 발생할 경우 다음 이벤트가 처리되기 전에 상태 일관성을 보장하기 위해 useEffect가 브라우저 paint 이전에 동기적으로 플러시 될 수 있다.
리액트 공식 문서 기준 훅 선택 기준은 가급적 useEffect를 사용하고 레이아웃에 직접적인 영향을 주는 작업에만 useLayoutEffect를 사용할 것을 권장한다.
