useEffect의 동작 원리와 실행 시점, 의존성 배열의 역할, 클린업 함수가 왜 필요한지를 구체적인 예시로 깊이 이해합니다.
🔄 사이드 이펙트란 무엇인가
리액트 컴포넌트의 주된 역할은 데이터(state, props)를 받아 JSX를 반환하는 것입니다. 그런데 실제 앱에서는 컴포넌트가 렌더링 이외의 작업도 해야 합니다. 예를 들면:
- 서버에서 데이터 가져오기 (fetch API, axios)
- DOM 직접 조작 (스크롤 위치, 포커스)
- 타이머 설정 (
setTimeout,setInterval) - 이벤트 리스너 등록/해제
- 로컬 스토리지 읽기/쓰기
이처럼 렌더링과 관계없이 "외부 세계"와 상호작용하는 코드를 사이드 이펙트(Side Effect)라고 합니다. useEffect는 이런 사이드 이펙트를 안전하게 처리하기 위한 훅입니다.
⚙️ useEffect 기본 문법과 실행 시점
import { useEffect, useState } from 'react'
function Timer() {
const [seconds, setSeconds] = useState(0)
useEffect(() => {
console.log('이 코드는 렌더링 후 실행됩니다')
})
return <p>{seconds}초</p>
}
useEffect는 컴포넌트가 렌더링된 이후에 실행됩니다. 첫 번째 인수는 실행할 함수(이펙트 함수), 두 번째 인수는 의존성 배열입니다.
📦 의존성 배열 — 세 가지 형태
의존성 배열([])이 언제 실행될지를 제어합니다:
// 1. 배열 없음 — 매 렌더마다 실행 (거의 사용 안 함)
useEffect(() => {
console.log('매번 실행')
})
// 2. 빈 배열 — 마운트 시 한 번만 실행
useEffect(() => {
console.log('컴포넌트가 처음 화면에 나타날 때만 실행')
// API 호출, 이벤트 리스너 등록 등
}, [])
// 3. 값이 있는 배열 — 그 값이 바뀔 때마다 실행
useEffect(() => {
console.log(`userId가 ${userId}로 바뀌었을 때 실행`)
// userId에 해당하는 데이터 재패치
}, [userId])
의존성 배열에 있는 값이 이전 렌더의 값과 다를 때 이펙트가 다시 실행됩니다. []는 의존성이 없으니 처음 한 번만 실행됩니다.
🧹 클린업 함수 — 뒷정리
이펙트 함수에서 함수를 반환하면, 그것이 클린업(cleanup) 함수입니다. 컴포넌트가 언마운트되거나 이펙트가 다시 실행되기 직전에 호출됩니다. 이것이 없으면 메모리 누수(memory leak)가 발생합니다.
function Stopwatch() {
const [time, setTime] = useState(0)
const [running, setRunning] = useState(false)
useEffect(() => {
if (!running) return
const interval = setInterval(() => {
setTime((prev) => prev + 1)
}, 1000)
// 클린업: 다음 이펙트 실행 전 또는 언마운트 시 인터벌 제거
return () => {
clearInterval(interval)
}
}, [running]) // running이 바뀔 때마다 실행
return (
<div>
<p>{time}초</p>
<button onClick={() => setRunning(!running)}>{running ? '정지' : '시작'}</button>
</div>
)
}
클린업이 없다면 running이 바뀔 때마다 새 인터벌이 추가되어 시간이 빠르게 흐르고, 컴포넌트가 제거된 후에도 인터벌이 계속 동작합니다.
🌐 데이터 패칭과 useEffect
가장 흔한 useEffect 사용 사례는 데이터 패칭입니다:
interface Post { id: number; title: string; body: string }
function PostDetail({ postId }: { postId: number }) {
const [post, setPost] = useState<Post | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
setLoading(true)
setError(null)
fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`)
.then((res) => {
if (!res.ok) throw new Error('서버 오류')
return res.json()
})
.then((data: Post) => setPost(data))
.catch((e) => setError(e.message))
.finally(() => setLoading(false))
}, [postId]) // postId가 바뀔 때마다 재패치
if (loading) return <div>불러오는 중...</div>
if (error) return <div>오류: {error}</div>
if (!post) return null
return (
<article>
<h2>{post.title}</h2>
<p>{post.body}</p>
</article>
)
}
⚠️ 흔한 실수 — 무한 루프
객체나 배열을 의존성 배열에 넣으면 무한 루프가 발생할 수 있습니다. 렌더마다 새 객체/배열이 생성되고, 참조가 다르다고 판단해 이펙트를 계속 실행합니다.
// ❌ 매 렌더마다 새 객체 → 무한 루프
useEffect(() => {
fetchData(options)
}, [{ page: 1, limit: 10 }]) // 매번 새 객체 참조!
// ✅ 원시값을 의존성으로
useEffect(() => {
fetchData({ page, limit })
}, [page, limit])
✅ 정리
useEffect는 렌더링 이후 사이드 이펙트를 처리합니다.- 의존성 배열: 없음(매 렌더) /
[](마운트 1회) /[값](값 변경 시). - 클린업 함수로 타이머·이벤트 리스너를 해제해 메모리 누수를 방지합니다.
- 객체/배열을 의존성으로 넣으면 무한 루프 — 원시값으로 분해해서 넣으세요.
관련 주제
- useEffect 훅
- 의존성 배열
- 마운트 업데이트 시점
- 클린업 함수
- 사이드 이펙트
- 개발·프로그래밍
- 개발·프로그래밍 강의
- 리액트 입문 — 컴포넌트로 만드는 웹
- 무료강의
- 무료 온라인 강의
- NUGUNA
- 누구나
댓글
불러오는 중...
