[React] useEffect
개요
useEffect는 컴포넌트가 렌더링된 후 사이드 이펙트(Side Effect)를 실행하기 위한 Hook입니다. 사이드 이펙트란 API 호출, DOM 직접 조작, 타이머 설정, 이벤트 구독 등 렌더링 외부의 작업을 말합니다.
기본 구조
import { useEffect } from "react";
useEffect(() => {
// 실행할 코드
return () => {
// (선택) 정리(cleanup) 코드
};
}, [/* 의존성 배열 */]);
의존성 배열
의존성 배열에 따라 실행 시점이 달라집니다.
import { useState, useEffect } from "react";
function App() {
const [count, setCount] = useState(0);
const [name, setName] = useState("Alice");
// 1. 의존성 배열 생략: 렌더링마다 실행
useEffect(() => {
console.log("매 렌더링마다 실행");
});
// 2. 빈 배열: 마운트(최초 1회)에만 실행
useEffect(() => {
console.log("최초 1회만 실행");
}, []);
// 3. 특정 값: 해당 값이 변경될 때마다 실행
useEffect(() => {
console.log("count가 변경됨:", count);
}, [count]);
// 4. 여러 값: 하나라도 변경되면 실행
useEffect(() => {
console.log("count 또는 name이 변경됨");
}, [count, name]);
return (
<button onClick={() => setCount(count + 1)}>count: {count}</button>
);
}
API 데이터 가져오기
import { useState, useEffect } from "react";
interface Post {
id: number;
title: string;
body: string;
}
function App() {
const [posts, setPosts] = useState<Post[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
// 비동기 함수는 useEffect 안에서 정의 후 호출
const fetchPosts = async () => {
try {
setLoading(true);
const res = await fetch("https://jsonplaceholder.typicode.com/posts?_limit=5");
if (!res.ok) throw new Error("데이터를 불러오지 못했습니다");
const data: Post[] = await res.json();
setPosts(data);
} catch (err) {
setError(err instanceof Error ? err.message : "오류 발생");
} finally {
setLoading(false);
}
};
fetchPosts();
}, []); // 마운트시 1회 실행
if (loading) return <p>로딩 중...</p>;
if (error) return <p>오류: {error}</p>;
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
의존성에 따른 재호출
import { useState, useEffect } from "react";
function App() {
const [userId, setUserId] = useState(1);
const [user, setUser] = useState<{ name: string; email: string } | null>(null);
useEffect(() => {
const fetchUser = async () => {
const res = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
const data = await res.json();
setUser(data);
};
fetchUser();
}, [userId]); // userId가 변경될 때마다 재호출
return (
<div>
<div>
{[1, 2, 3, 4, 5].map((id) => (
<button key={id} onClick={() => setUserId(id)}>
사용자 {id}
</button>
))}
</div>
{user && (
<div>
<p>이름: {user.name}</p>
<p>이메일: {user.email}</p>
</div>
)}
</div>
);
}
Cleanup (정리)
컴포넌트가 화면에서 사라질 때(언마운트) 또는 effect가 다시 실행되기 전에 정리 작업을 수행합니다.
import { useState, useEffect } from "react";
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds((prev) => prev + 1);
}, 1000);
// cleanup: 컴포넌트가 사라질 때 타이머 정리
return () => {
clearInterval(interval); // 메모리 누수 방지
console.log("타이머 정리!");
};
}, []);
return <p>경과: {seconds}초</p>;
}
// 이벤트 구독 정리 예시
useEffect(() => {
const handleResize = () => console.log(window.innerWidth);
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize); // 구독 해제
};
}, []);
흔한 실수
useEffect 안에서 State 무한 업데이트
// ❌ 무한 루프: count를 의존성에 넣고 count를 업데이트
useEffect(() => {
setCount(count + 1);
}, [count]); // count 변경 → effect 실행 → count 변경 → ...
// ✅ 함수형 업데이트로 의존성 제거
useEffect(() => {
setCount((prev) => prev + 1);
}, []);
빈 배열인데 외부 변수 참조
const [userId, setUserId] = useState(1);
// ❌ userId가 변해도 effect는 최초만 실행, 항상 userId=1로 요청
useEffect(() => {
fetch(`/api/users/${userId}`);
}, []);
// ✅ userId 의존성 추가
useEffect(() => {
fetch(`/api/users/${userId}`);
}, [userId]);
useEffect vs 이벤트 핸들러
| 상황 | 사용 방법 |
|---|---|
| 버튼 클릭에 반응 | 이벤트 핸들러 (onClick) |
| 컴포넌트 표시 시 데이터 로드 | useEffect |
| 특정 값 변경 감지 | useEffect + 의존성 배열 |
| 외부 시스템 연동(웹소켓, 타이머) | useEffect + cleanup |