부수 효과란?
렌더링 자체와 무관한 작업들입니다:
- API 데이터 불러오기
- 이벤트 리스너 등록/해제
- 타이머 설정/해제
- DOM 직접 조작
useEffect 기본
import { useEffect, useState } from "react";
function UserProfile({ userId }: { userId: number }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// userId가 바뀔 때마다 실행
async function fetchUser() {
setLoading(true);
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
setUser(data);
setLoading(false);
}
fetchUser();
}, [userId]); // 의존성 배열
if (loading) return <div>로딩 중...</div>;
if (!user) return <div>사용자 없음</div>;
return <div>{user.name}</div>;
}
의존성 배열 규칙
flowchart TB
subgraph DEPS["의존성 배열"]
D1["[] 빈 배열\n마운트 시 1번만 실행"]
D2["[value] 값 있음\nvalue가 바뀔 때마다 실행"]
D3["의존성 없음\n매 렌더링마다 실행"]
end
// 마운트 시 1번만 (componentDidMount)
useEffect(() => {
initializeSomething();
}, []);
// count가 바뀔 때마다
useEffect(() => {
document.title = `카운트: ${count}`;
}, [count]);
// 매 렌더링마다 (보통 의도하지 않음)
useEffect(() => {
console.log("렌더링됨");
});
클린업 함수
컴포넌트가 사라질 때(언마운트) 또는 다음 effect 실행 전에 정리합니다.
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
// 클린업: 컴포넌트 언마운트 시 실행
return () => clearInterval(interval);
}, []);
return <div>경과: {seconds}초</div>;
}
// 이벤트 리스너 클린업
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
실전: 데이터 페칭 패턴
interface FetchState<T> {
data: T | null;
loading: boolean;
error: Error | null;
}
function useApi<T>(url: string) {
const [state, setState] = useState<FetchState<T>>({
data: null,
loading: true,
error: null,
});
useEffect(() => {
let cancelled = false; // 경쟁 조건 방지
setState({ data: null, loading: true, error: null });
fetch(url)
.then(r => r.json())
.then(data => {
if (!cancelled) setState({ data, loading: false, error: null });
})
.catch(error => {
if (!cancelled) setState({ data: null, loading: false, error });
});
return () => { cancelled = true; };
}, [url]);
return state;
}
// 사용
function UserList() {
const { data: users, loading, error } = useApi<User[]>("/api/users");
if (loading) return <div>로딩 중...</div>;
if (error) return <div>오류: {error.message}</div>;
return <ul>{users?.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
useEffect 주의사항
// ❌ 무한 루프: 매 렌더링마다 객체 생성 → effect 재실행
useEffect(() => {
fetchData(options);
}, [options]); // options = { page: 1 } (매번 새 객체)
// ✅ 원시값을 의존성으로
useEffect(() => {
fetchData({ page });
}, [page]);
// ❌ effect 안에서 async 직접 사용 불가
useEffect(async () => { ... }); // 반환값이 함수여야 함
// ✅ 내부에서 async 함수 정의
useEffect(() => {
async function load() { ... }
load();
}, []);
정리
| 패턴 | 의존성 배열 | 실행 시점 |
|---|---|---|
| 초기화 | [] | 마운트 1회 |
| 값 감시 | [value] | 마운트 + value 변경 |
| 항상 실행 | 없음 | 매 렌더링 |
| 클린업 | - | 다음 effect 전 + 언마운트 |
다음 편에서는 조건부 렌더링과 리스트 — 동적으로 UI를 그리는 다양한 패턴을 배웁니다.