useEffect — 화면을 그린 뒤에 할 일
Article
13강에서 렌더 함수는 순수해야 한다고 했다. 바깥 세상을 건드리면 안 된다.
그런데 실제 앱은 바깥 세상과 대화해야 한다. 서버에서 데이터를 받고, 타이머를 걸고, 브라우저 이벤트를 듣는다.
그 일을 화면을 다 그린 뒤에 하는 곳이 useEffect다.
바깥 세상이란
React의 세계는 좁다. state를 받아 화면을 반환하는 것까지다. 그 밖은 전부 바깥이다.
- 바깥 세상
- 서버 통신
- 타이머
- 브라우저 API
- 직접 DOM 조작
- 외부 라이브러리
이런 것들을 렌더 중에 하면 안 된다. React가 함수를 몇 번 실행할지 모르기 때문이다. 13강의 StrictMode가 두 번 부르는 것만 봐도 그렇다.
기본 형태
import { useEffect } from "react";
function Title({ count }) {
useEffect(() => {
document.title = `${count}개`;
});
return <p>{count}</p>;
}
첫 인자는 렌더링이 끝난 뒤 실행할 함수다.
화면이 사용자에게 보인 뒤에 실행된다. 그래서 무거운 일을 해도 화면이 늦게 뜨지 않는다.
의존성 배열 — 언제 다시 실행할지
두 번째 인자로 배열을 준다. 이 배열이 전부를 결정한다.
| 형태 | 언제 실행되나 | 쓰는 곳 |
|---|---|---|
| useEffect(fn) | 매 렌더링마다 | 거의 안 쓴다 |
| useEffect(fn, []) | 첫 렌더링에 한 번 | 초기 설정 · 이벤트 등록 |
| useEffect(fn, [a, b]) | a 나 b 가 바뀔 때 | 대부분의 경우 |
useEffect(() => {
document.title = `${count}개`;
}, [count]);
count가 바뀔 때만 실행된다. name이 바뀌어 리렌더링돼도 count가 그대로면 건너뛴다.
비교 방법은 12강에서 본 **Object.is**다. 객체를 넣으면 매 렌더링마다 새 주소라서 매번 실행된다.
빈 배열이 만드는 것
useEffect(() => {
console.log("한 번만");
}, []);
의존하는 값이 없으니 다시 실행할 이유도 없다. 첫 렌더링에서 딱 한 번 실행된다.
StrictMode 개발 환경에서는 두 번 실행된다. 13강에서 본 그 검사다. 정리 함수를 제대로 썼는지 확인하는 것이다.
정리 함수 — 뒷정리
effect가 함수를 반환하면, React는 다음 effect를 실행하기 전과 컴포넌트가 사라질 때 그 함수를 부른다.
useEffect(() => {
const id = setInterval(() => {
console.log("1초마다");
}, 1000);
return () => clearInterval(id);
}, []);
return () => clearInterval(id)가 정리 함수다. 없으면 어떻게 될까.
- 01
컴포넌트가 화면에서 사라진다
다른 페이지로 이동했다.
- 02
타이머는 계속 돈다
React 는
setInterval을 모른다. 멈추라고 시킨 적이 없다. - 03
메모리가 새고 에러가 난다
없어진 컴포넌트의 state 를 바꾸려 들면 경고가 뜬다. 페이지를 오갈수록 타이머가 쌓인다.
자바스크립트 편 14강에서 "클로저가 메모리를 붙잡는다"고 했던 이야기다. 등록한 것은 반드시 해제한다.
| 등록 | 정리 |
|---|---|
| setInterval | clearInterval |
| setTimeout | clearTimeout |
| addEventListener | removeEventListener |
| 구독 (subscribe) | 구독 해제 (unsubscribe) |
| fetch | AbortController 로 취소 |
이벤트 리스너를 예로 들면 이렇다.
useEffect(() => {
function handleResize() {
setWidth(window.innerWidth);
}
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
데이터 가져오기
가장 흔한 쓰임이다. 자바스크립트 편 20강의 async/await를 쓴다.
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
let ignore = false;
async function load() {
setIsLoading(true);
const res = await fetch(`/api/users/${userId}`);
const data = await res.json();
if (!ignore) {
setUser(data);
setIsLoading(false);
}
}
load();
return () => {
ignore = true;
};
}, [userId]);
if (isLoading) return <Spinner />;
return <h2>{user.name}</h2>;
}
두 가지를 짚는다.
ignore 플래그가 두 번째 포인트다. userId가 빠르게 바뀌면 요청 두 개가 겹치고, 먼저 보낸 요청이 나중에 도착할 수 있다. 그러면 옛 데이터가 화면에 남는다. 정리 함수에서 ignore = true로 표시해 낡은 응답을 무시한다.
이걸 **경쟁 상태(race condition)**라 부른다.
안 써도 되는 경우
useEffect를 남용하는 게 초보자의 가장 흔한 실수다.
필요하다
바깥 세상과 동기화
- 서버에서 데이터 받기
- 타이머 · 구독 등록
- document.title 변경
- 외부 라이브러리 연결
필요 없다
React 안에서 끝나는 일
- props 로 state 계산하기
- 이벤트에 반응하기
- 다른 state 로 값 만들기
- 렌더링 직전 데이터 가공
가장 흔한 잘못은 이것이다.
function Cart({ items }) {
const [total, setTotal] = useState(0);
useEffect(() => {
setTotal(items.reduce((acc, i) => acc + i.price, 0));
}, [items]);
}
렌더링 → effect 실행 → setState → 또 렌더링. 화면을 두 번 그린다. 8강에서 말한 그대로, 계산할 수 있는 값은 그냥 계산한다.
function Cart({ items }) {
const total = items.reduce((acc, i) => acc + i.price, 0);
return <p>{total}원</p>;
}
이벤트에 반응하는 것도 effect가 아니다.
useEffect(() => {
if (isSubmitted) sendAnalytics();
}, [isSubmitted]);
function handleSubmit() {
sendAnalytics();
}
버튼을 눌러서 일어난 일이면 핸들러에 쓴다. effect는 "화면에 나타났기 때문에" 해야 하는 일에 쓴다.
무한 루프
가장 자주 만나는 사고다.
useEffect(() => {
setCount(count + 1);
}, [count]);
effect가 count를 바꾸고, count가 바뀌어 effect가 또 돈다. 끝이 없다.
의존성 배열을 빠뜨려도 같다.
useEffect(() => {
setUser({ name: "지원" });
});
매 렌더링마다 실행되고, setUser가 새 객체를 넣어 또 렌더링한다. 12강에서 본 주소 비교 때문에 내용이 같아도 새 객체다.
정리하면
useEffect는 화면을 그린 뒤에 바깥 세상과 대화하는 곳이다.- 두 번째 인자인 의존성 배열이 실행 시점을 정한다.
[]는 한 번,[a]는a가 바뀔 때. - 의존성 비교는
Object.is다. 객체와 함수를 넣으면 매번 실행된다. - 등록한 것은 정리 함수로 해제한다. 타이머, 리스너, 구독 전부.
- effect 함수에
async를 붙이지 않는다. 안에 async 함수를 만들어 부른다. - 데이터를 받아올 때는 경쟁 상태를 막는다. 정리 함수에서 낡은 응답을 무시한다.
- 계산할 수 있는 값과 이벤트 반응에는 effect를 쓰지 않는다.
다음 강의는 훅의 규칙이다. 왜 if 문 안에서 훅을 부르면 안 되는지, 그 이유가 놀랍도록 단순하다.