C
React/Hooks/Lesson 09

useEffect

35분·theory
이 챕터
1/5
JavaScript

useEffect

💡 왜 배워야 할까요?

🎯 서버에서 데이터를 받아오는 가장 기본적인 방법입니다.
💼 페이지 로드 시 한 번만 실행하거나 특정 값이 바뀔 때만 실행하는 제어가 가능합니다.
React 개발자라면 매일 쓰는 필수 기능입니다.
🏢 실무에서는
실무에서는 컴포넌트가 화면에 나타나면 useEffect로 API를 호출해 상품 목록, 사용자 정보 등을 받아옵니다. 거의 모든 웹 앱이 이 패턴으로 만들어집니다.

useEffect — 컴포넌트 생애주기, API 호출/타이머에 사용

실생활 비유: 입장/퇴장 안내원

컴포넌트가 화면에 나타날 때(마운트)와 사라질 때(언마운트)
자동으로 실행되는 코드를 등록하는 Hook.

왜 배우나요?

  • 데이터 로딩 (API 호출)
  • 타이머 설정
  • 이벤트 리스너 등록/해제

기본 구조

js
useEffect(() => {
  // 실행할 코드

  return () => {
    // 정리(cleanup) 코드 — 언마운트 시 실행
  };
}, [의존성 배열]);

의존성 배열 패턴

배열실행 시점
없음매 렌더링 후
[]마운트 시 한 번만
[id]id가 바뀔 때마다
💻 마운트 시 데이터 fetch + 언마운트 정리
import { useState, useEffect } from 'react';

// ===== API 데이터 fetch 예제 =====
function UserList() {
  // 입력: 없음 (마운트 시 자동 실행)
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // 처리: 마운트 시 한 번만 실행 ([] 의존성)
    async function fetchUsers() {
      const response = await fetch('https://jsonplaceholder.typicode.com/users');
      const data = await response.json();
      setUsers(data);
      setLoading(false);
    }
    fetchUsers();
  }, []);  // ← 빈 배열: 마운트 시 한 번만

  // 출력: 로딩 중 or 사용자 목록
  if (loading) return <p>로딩 중...</p>;
  return (
    <ul>
      {users.map(user => <li key={user.id}>{user.name}</li>)}
    </ul>
  );
}

// ===== 타이머 + 정리(cleanup) 예제 =====
function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    // 처리: 1초마다 카운트 증가
    const timer = setInterval(() => {
      setSeconds(prev => prev + 1);
    }, 1000);

    // 정리: 컴포넌트 언마운트 시 타이머 제거 (메모리 누수 방지)
    return () => clearInterval(timer);
  }, []);  // 마운트 시 한 번만 설정

  return <p>경과 시간: {seconds}초</p>;
}
💻 좋은 예시 - 2025 권장 패턴 (AbortController + 정확한 의존성)
// ✅ 모범 사례: React 19 + 2025 패턴
const UserProfile = ({ userId }: { userId: string }) => {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const abortController = new AbortController();
    
    const loadUser = async () => {
      try {
        setLoading(true);
        setError(null);
        
        // AbortController로 경쟁 조건 해결
        const userData = await fetchUser(userId, {
          signal: abortController.signal
        });
        
        // 컴포넌트가 언마운트되었거나 다른 요청이 시작된 경우 중단
        if (!abortController.signal.aborted) {
          setUser(userData);
        }
      } catch (err) {
        if (!abortController.signal.aborted) {
          setError('사용자 정보를 불러오는데 실패했습니다.');
        }
      } finally {
        if (!abortController.signal.aborted) {
          setLoading(false);
        }
      }
    };

    loadUser();

    // 클린업 함수: 컴포넌트 언마운트 또는 userId 변경 시 실행
    return () => {
      abortController.abort();
    };
  }, [userId]); // userId 의존성 명시

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  return <div>{user?.name}</div>;
};
💻 심화 예시 - 커스텀 Hook으로 재사용성 향상
// ✅ 2025 고급 패턴: 커스텀 Hook + Suspense 호환
function useAsyncData<T>(fetchFn: () => Promise<T>, deps: any[]) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    let cancelled = false;
    
    const loadData = async () => {
      try {
        setLoading(true);
        setError(null);
        
        const result = await fetchFn();
        
        if (!cancelled) {
          setData(result);
          setLoading(false);
        }
      } catch (err) {
        if (!cancelled) {
          setError(err as Error);
          setLoading(false);
        }
      }
    };

    loadData();

    return () => {
      cancelled = true;
    };
  }, deps);

  return { data, loading, error };
}

// 사용 예시
const UserProfile = ({ userId }: { userId: string }) => {
  const { data: user, loading, error } = useAsyncData(
    () => fetchUser(userId),
    [userId]
  );

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  return <div>{user?.name}</div>;
};

💡 ⚠️ 흔한 실수

  • 의존성 배열에 필요한 값을 누락하여 ESLint 경고를 무시하는 경우 (exhaustive-deps 규칙 위반)
  • 클린업 함수 없이 setInterval, addEventListener 등을 사용하여 메모리 누수 발생
  • async/await을 useEffect 콜백에 직접 사용하기 (useEffect는 cleanup 함수만 반환해야 함)
  • 객체나 배열을 의존성으로 사용할 때 참조 동등성 문제로 무한 렌더링 발생

💡 🎯 면접 준비

Q: useEffect의 의존성 배열이 빈 배열일 때와 없을 때의 차이점을 설명해주세요
Q: useEffect에서 API 호출 시 경쟁 조건(Race Condition)을 어떻게 방지할 수 있나요?
Q: useEffect 클린업 함수는 언제 실행되고 왜 필요한가요?

힌트: 의존성 배열의 3가지 패턴(없음/빈배열/값있음)을 명확히 구분하고, AbortController나 cleanup 플래그를 이용한 경쟁 조건 해결 방법을 코드와 함께 설명. 메모리 누수 방지를 위한 클린업의 중요성을 실무 사례로 답변

⚛️ React 패턴 — useEffect

useEffect을 React에서 어떻게 쓰는지 코드와 함께 단계별로 익혀보세요.
1 🎯 1. Effect 정의
렌더 후 실행할 부수효과
useEffect(() => {
  console.log('마운트 또는 count 변경');
}, [count]);
2 📋 2. 의존성 배열
[] = 한 번만, [x] = x 변경 시, 없음 = 매번
3 🧹 3. Cleanup 함수
return으로 정리 함수 — 언마운트 시 실행
useEffect(() => {
  const t = setInterval(...);
  return () => clearInterval(t);  // cleanup
}, []);
4 ⚠️ 4. 흔한 실수
의존성 빠뜨림 → stale closure / 무한 루프

⏰ useEffect — 마운트 + 의존성 + cleanup

코드를 직접 수정하면 0.7초 후 자동 반영됩니다. React 18 + Babel로 브라우저에서 즉시 실행.
🖥️ 실행 결과 — 렌더링된 React 컴포넌트
✏️ React 코드 수정하기 (클릭해서 열기)
⚛️ React 18 + Babel Standalone — 결과 먼저 확인 후 코드 편집기에서 직접 수정해볼 수 있습니다.

확인 퀴즈

useEffect의 의존성 배열이 [] (빈 배열)일 때 실행 시점은?
💡 빈 배열 []은 "의존하는 값이 없다"는 의미입니다. 따라서 처음 마운트될 때 딱 한 번만 실행됩니다. API 초기 데이터 로딩에 가장 많이 사용되는 패턴입니다!
먼저 읽으면 좋은 개념: 폼 처리
useEffect - React