React 교재 · 중급 11편

React useEffect — side effect · cleanup

외부 세계와 동기화하는 Hook. 데이터 fetch·구독·timer — 그리고 가장 흔한 무한 루프 함정.

컴포넌트 카드 옆 동기 시계와 외부 데이터 파이프 일러스트 — useEffect 사이드 이펙트 컨셉

지금까지 다룬 컴포넌트는 "props 와 state 만 보고 JSX 그리기" 만 했다. 순수 함수. 그런데 실전 앱은 외부 세계와 통신해야 한다 — API 호출, WebSocket 구독, timer 시작, DOM 직접 조작, 로깅. 이런 걸 React 는 side effect (부작용) 라 부른다.

useEffect 는 side effect 를 안전한 시점에 실행하기 위한 Hook. 잘못 쓰면 무한 루프, 메모리 누수, 옛 데이터 사용 등 신나는 사고가 줄줄이. 이번 11편에서 한 번에 정리.

1. 언제 실행되나 — 렌더 직후

useEffect 첫 인자는 "렌더 후 실행할 함수". 두 번째 인자는 "언제 다시 실행할지" dependency array.

import { useEffect, useState } from 'react'; function UserProfile({ userId }) { const [user, setUser] = useState(null); useEffect(() => { fetch(`/api/users/${userId}`) .then(r => r.json()) .then(setUser); }, [userId]); // userId 가 바뀔 때마다 다시 fetch if (!user) return <p>로딩...</p>; return <h1>{user.name}</h1>; }

흐름 — 컴포넌트가 처음 마운트 → 화면에 "로딩..." 그려짐 → 그 직후 useEffect 실행 → fetch → setUser → state 변화 → 재렌더 → "user.name" 표시.

2. dependency array — 3가지 경우

두 번째 인자에 무엇을 넣냐에 따라 실행 빈도 결정.

// 패턴 1 — 매 렌더마다 실행 (거의 안 씀, 무한 루프 위험) useEffect(() => { ... }); // 패턴 2 — 마운트 시 1번만 (초기화·구독 시작) useEffect(() => { ... }, []); // 패턴 3 — 의존 값이 바뀔 때만 (가장 흔함) useEffect(() => { ... }, [userId, isActive]);
핵심 규칙 — effect 안에서 사용하는 모든 props·state 는 dependency 배열에 포함해야. ESLint rule (react-hooks/exhaustive-deps) 가 자동 검사. 빠뜨리면 옛 값(stale closure) 사용 버그.

3. cleanup — return 함수의 진짜 의미

구독·timer 처럼 시작했으면 멈춰야 하는 작업은 cleanup function 으로. useEffect 콜백이 다른 함수 를 return 하면 그게 cleanup.

function Clock() { const [time, setTime] = useState(new Date()); useEffect(() => { const id = setInterval(() => setTime(new Date()), 1000); return () => clearInterval(id); // ← cleanup }, []); return <p>{time.toLocaleTimeString()}</p>; }

cleanup 이 호출되는 시점 — ① 컴포넌트가 언마운트될 때, ② dependency 가 바뀌어 effect 가 다시 실행되기 직전. ②가 핵심 — 옛 effect 의 구독을 끊고 새 effect 의 구독을 시작.

cleanup 안 쓰면 — Clock 컴포넌트가 사라져도 setInterval 이 계속 돌고 setTime 호출 시도 → React 콘솔 "Can't perform state update on unmounted component" 경고 + 메모리 누수.

4. 무한 루프 — 가장 흔한 함정 3종

모든 React 개발자가 한 번은 겪는다. 패턴 3개 박제.

함정 1 — dependency 빠뜨림 + state 변경

useEffect(() => { setCount(count + 1); // state 변경 → 재렌더 → effect 다시 → 무한 }); // dependency 없음!

함정 2 — 객체/배열을 dependency 에

function Component({ filters }) { useEffect(() => { ... }, [filters]); // 부모가 매 렌더마다 새 filters 객체 전달 → 참조 항상 새것 → 무한 }

해결 — 부모가 useMemo 로 객체 메모이즈 (16편), 또는 효과 안에서 객체의 원시 값만 dependency 로:

useEffect(() => { ... }, [filters.category, filters.minPrice]);

함정 3 — state 를 다른 state 로 동기화

// ❌ 안티패턴 useEffect(() => { setFullName(`${first} ${last}`); }, [first, last]); // ✅ 그냥 렌더 중에 계산 const fullName = `${first} ${last}`;

이건 무한 루프는 안 일어나지만 불필요한 재렌더 2번. "파생 값은 effect 가 아니라 계산" 이 React 의 정공법. effect 는 외부 세계 동기화 전용.

요즘 트렌드 — 단순 데이터 fetch 는 useEffect 대신 TanStack Query 또는 SWR 같은 라이브러리가 표준. 캐싱·재시도·중복 호출 방지·로딩 상태 모두 제공. 17편 Suspense + 데이터 fetch 에서 다시 등장. 그래도 useEffect 의 기본은 알아야 — 그 라이브러리도 내부에 effect 가 있다.

useEffect 가 손에 잡히면 인터랙티브 React 앱의 모든 부품이 완성. 12편부터는 같은 로직을 여러 컴포넌트에서 재사용하는 커스텀 Hook 패턴. React 의 진짜 우아함이 보이는 구간.

다음 글

React 교재 12편 — 커스텀 Hook 만들기. use- prefix, 로직 추출, 재사용 패턴. useEffect 의 진짜 응용.

© 2026 주나이테크(주) @JUNAITECH