ReactReact 기초 · 4기초

useEffect — 부수 효과와 생명주기 처리

ReactuseEffect생명주기부수효과클린업

부수 효과란?

렌더링 자체와 무관한 작업들입니다:

  • 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를 그리는 다양한 패턴을 배웁니다.

궁금한 점이 있으신가요?

협업·의뢰는 아래로, 가벼운 소통은 인스타그램 @bluefox._.hi도 환영이에요.