July 5, 2025

πŸ”„ useEffect의 κ³Όλ„ν•œ λ‚¨μš©: μ˜λ„μΉ˜ μ•Šμ€ μž¬μ‹€ν–‰κ³Ό λ³΅μž‘ν•œ μ˜μ‘΄μ„± 관리

JavaScript
React
μƒνƒœκ΄€λ¦¬
λΉ„λ™κΈ°μ²˜λ¦¬
μ»΄ν¬λ„ŒνŠΈ
μ•„ν‚€ν…μ²˜
μ„±λŠ₯

Summary

React useEffect 훅을 μ‚¬μš©ν•  λ•Œ, μ‹€μ œ 둜직 μˆ˜ν–‰μ— ν•„μš” μ—†λŠ” μ˜μ‘΄μ„±μ„ μΆ”κ°€ν•˜κ±°λ‚˜ μ‚¬μš©μž μ•‘μ…˜μ— μ˜ν•΄ νŠΈλ¦¬κ±°λ˜μ–΄μ•Ό ν•  λ‘œμ§μ„ useEffect에 묢어두면 λΆˆν•„μš”ν•œ μž¬μ‹€ν–‰, μ„±λŠ₯ μ €ν•˜, 예츑 λΆˆκ°€λŠ₯ν•œ λ™μž‘μ„ μœ λ°œν•©λ‹ˆλ‹€. μ΅œμ†Œν•œμ˜ ν•„μˆ˜ μ˜μ‘΄μ„±λ§Œ ν¬ν•¨ν•˜κ³ , 이벀트 기반 λ‘œμ§μ€ 이벀트 ν•Έλ“€λŸ¬μ—μ„œ 직접 μ²˜λ¦¬ν•˜λ©°, useCallback λ“±μœΌλ‘œ ν•¨μˆ˜ μ°Έμ‘° μ•ˆμ •μ„±μ„ ν™•λ³΄ν•˜μ—¬ 효율적인 데이터 흐름을 λ§Œλ“€μ–΄μ•Ό ν•©λ‹ˆλ‹€.

Why Wrong?

λ§Žμ€ κ°œλ°œμžλ“€μ΄ useEffectλ₯Ό μ‚¬μš©ν•˜μ—¬ 데이터λ₯Ό κ°€μ Έμ˜¬ λ•Œ, μ‹€μ œ API μš”μ²­μ— 직접적인 영ν–₯을 μ£Όμ§€ μ•ŠλŠ” μƒνƒœλ‚˜ propsκΉŒμ§€ μ˜μ‘΄μ„± 배열에 ν¬ν•¨μ‹œν‚€λŠ” κ²½μš°κ°€ ν”ν•©λ‹ˆλ‹€. μ΄λŠ” ν•΄λ‹Ή propsλ‚˜ μƒνƒœκ°€ 변경될 λ•Œλ§ˆλ‹€ λΆˆν•„μš”ν•˜κ²Œ 데이터 μž¬μš”μ²­μ΄ λ°œμƒν•˜μ—¬ μ„±λŠ₯ μ €ν•˜, λ„€νŠΈμ›Œν¬ λ¦¬μ†ŒμŠ€ λ‚­λΉ„, 그리고 μ„œλ²„μ— λΆˆν•„μš”ν•œ λΆ€ν•˜λ₯Ό μ΄ˆλž˜ν•©λ‹ˆλ‹€. 예λ₯Ό λ“€μ–΄, μ‚¬μš©μž ν”„λ‘œν•„μ˜ userId에 따라 데이터λ₯Ό 가져와야 ν•˜λŠ”λ°, λ‹¨μˆœνžˆ UI ν‘œμ‹œμš©μœΌλ‘œ μ‚¬μš©λ˜λŠ” profileType 같은 propsλ₯Ό useEffect의 μ˜μ‘΄μ„± 배열에 μΆ”κ°€ν•˜λŠ” κ²½μš°μž…λ‹ˆλ‹€.

λ˜ν•œ, useEffectλŠ” 주둜 μ»΄ν¬λ„ŒνŠΈ 생λͺ…μ£ΌκΈ° 및 μ˜μ‘΄μ„± 변화에 따라 'λ°˜μ‘μ μœΌλ‘œ' λ™κΈ°ν™”λ˜μ–΄μ•Ό ν•˜λŠ” λΆ€μˆ˜ 효과λ₯Ό μ²˜λ¦¬ν•˜λŠ” 데 μ ν•©ν•©λ‹ˆλ‹€. κ·ΈλŸ¬λ‚˜ μ‚¬μš©μž μΈν„°λž™μ…˜(λ²„νŠΌ 클릭, 폼 제좜 λ“±)κ³Ό 같이 'λͺ…μ‹œμ μœΌλ‘œ νŠΈλ¦¬κ±°λ˜μ–΄μ•Ό ν•˜λŠ”' λ‘œμ§κΉŒμ§€ useEffect 내뢀에 λ°°μΉ˜ν•˜λŠ” κ²½μš°κ°€ λ§ŽμŠ΅λ‹ˆλ‹€. μ΄λŠ” μ½”λ“œμ˜ μ˜λ„λ₯Ό λͺ¨ν˜Έν•˜κ²Œ λ§Œλ“€κ³ , νŠΉμ • μ‹œμ μ—λ§Œ μ‹€ν–‰λ˜μ–΄μ•Ό ν•  둜직이 예츑 λΆˆκ°€λŠ₯ν•œ μ‹œμ μ— μž¬μ‹€ν–‰λ  μœ„ν—˜μ„ λ†’μž…λ‹ˆλ‹€. 결과적으둜 디버깅을 μ–΄λ ΅κ²Œ ν•˜κ³ , μ‚¬μš©μžκ°€ 직접 데이터 갱신을 원할 λ•Œ λ³΅μž‘ν•œ μƒνƒœ μ‘°μž‘μ΄ ν•„μš”ν•˜κ²Œ λ©λ‹ˆλ‹€.

How to Fix?

useEffect의 μ˜μ‘΄μ„± λ°°μ—΄μ—λŠ” ν•΄λ‹Ή 효과(effect)κ°€ μ‹€ν–‰λ˜μ–΄μ•Ό ν•˜λŠ” μ΅œμ†Œν•œμ˜ ν•„μˆ˜μ μΈ κ°’λ“€λ§Œ 포함해야 ν•©λ‹ˆλ‹€. API 호좜의 경우, μ‹€μ œ API μš”μ²­ νŒŒλΌλ―Έν„°μ— 직접적인 영ν–₯을 λ―ΈμΉ˜λŠ” propsλ‚˜ μƒνƒœ(예: userId)λ§Œμ„ μ˜μ‘΄μ„±μœΌλ‘œ ν¬ν•¨ν•˜κ³ , UI ν‘œμ‹œμš©μœΌλ‘œλ§Œ μ‚¬μš©λ˜λŠ” props(profileType)λŠ” μ œμ™Έν•΄μ•Ό ν•©λ‹ˆλ‹€.

μ‚¬μš©μž μΈν„°λž™μ…˜μ— μ˜ν•΄ νŠΈλ¦¬κ±°λ˜λŠ” 둜직(예: 'μƒˆλ‘œκ³ μΉ¨' λ²„νŠΌ 클릭)은 useEffect λ‚΄λΆ€κ°€ μ•„λ‹Œ, ν•΄λ‹Ή 이벀트 ν•Έλ“€λŸ¬ ν•¨μˆ˜ λ‚΄μ—μ„œ 직접 ν˜ΈμΆœν•˜λŠ” 것이 λ°”λžŒμ§ν•©λ‹ˆλ‹€. 이λ₯Ό μœ„ν•΄ 데이터 패칭 λ‘œμ§μ„ useCallback으둜 감싸 ν•¨μˆ˜ 자체의 μ°Έμ‘° μ•ˆμ •μ„±μ„ ν™•λ³΄ν•˜κ³ , ν•„μš”μ— 따라 useEffect (μ»΄ν¬λ„ŒνŠΈ 마운트 μ‹œ λ˜λŠ” 핡심 μ˜μ‘΄μ„± λ³€κ²½ μ‹œ)와 이벀트 ν•Έλ“€λŸ¬ λͺ¨λ‘μ—μ„œ ν•΄λ‹Ή ν•¨μˆ˜λ₯Ό μž¬μ‚¬μš©ν•  수 μžˆλ„λ‘ ν•©λ‹ˆλ‹€. useCallback은 λΆˆν•„μš”ν•œ ν•¨μˆ˜ μž¬μƒμ„±μ„ 막아 useEffect의 λΆˆν•„μš”ν•œ μž¬μ‹€ν–‰μ„ λ°©μ§€ν•˜λŠ” 데 도움을 μ€λ‹ˆλ‹€.

λ˜ν•œ, μ»΄ν¬λ„ŒνŠΈ μ–Έλ§ˆμš΄νŠΈ μ‹œ 비동기 μš”μ²­μ΄ μ™„λ£Œλ˜μ§€ μ•Šμ•„ λ°œμƒν•˜λŠ” λ©”λͺ¨λ¦¬ λˆ„μˆ˜λ‚˜ 경쟁 쑰건(Race Condition)을 λ°©μ§€ν•˜κΈ° μœ„ν•΄ AbortControllerλ₯Ό μ‚¬μš©ν•˜μ—¬ useEffect의 클린업 ν•¨μˆ˜μ—μ„œ μš”μ²­μ„ μ·¨μ†Œν•˜λŠ” νŒ¨ν„΄μ„ κ³ λ €ν•΄μ•Ό ν•©λ‹ˆλ‹€. μ΄λŠ” μ»΄ν¬λ„ŒνŠΈμ˜ μ±…μž„ 뢄리λ₯Ό λͺ…ν™•νžˆ ν•˜κ³ , λΆˆν•„μš”ν•œ λ„€νŠΈμ›Œν¬ μš”μ²­μ„ 쀄이며, μ½”λ“œμ˜ 가독성과 μœ μ§€λ³΄μˆ˜μ„±μ„ ν–₯μƒμ‹œν‚΅λ‹ˆλ‹€.

Before Code (Bad)

import React, { useState, useEffect } from 'react';

function UserProfile({ userId, profileType }) {
  const [userData, setUserData] = useState(null);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);

  // ❌ μ•ˆν‹°νŒ¨ν„΄: profileType은 API ν˜ΈμΆœμ— 직접적인 영ν–₯을 μ£Όμ§€ μ•Šμ§€λ§Œ μ˜μ‘΄μ„± 배열에 ν¬ν•¨λ˜μ–΄
  // profileType이 변경될 λ•Œλ§ˆλ‹€ λΆˆν•„μš”ν•˜κ²Œ 데이터 μž¬μš”μ²­μ΄ λ°œμƒν•  수 있음.
  // λ˜ν•œ, λͺ…μ‹œμ μΈ 'μƒˆλ‘œκ³ μΉ¨' κΈ°λŠ₯을 κ΅¬ν˜„ν•˜κΈ° 어렀움.
  useEffect(() => {
    if (!userId) return;

    const fetchUserData = async () => {
      setIsLoading(true);
      setError(null);
      try {
        // profileType이 μ‹€μ œλ‘œλŠ” API 쿼리에 μ‚¬μš©λ˜μ§€ μ•Šκ±°λ‚˜, UI ν‘œμ‹œμš©μ΄λΌλ©΄ 이 μš”μ²­μ€ λΆˆν•„μš”
        const response = await fetch(`/api/users/${userId}`);
        if (!response.ok) {
          throw new Error('Failed to fetch user data');
        }
        const data = await response.json();
        setUserData(data);
      } catch (err) {
        setError(err.message);
      } finally {
        setIsLoading(false);
      }
    };

    fetchUserData();
  }, [userId, profileType]); // profileType이 변경될 λ•Œλ§ˆλ‹€ λΆˆν•„μš”ν•œ μž¬μ‹€ν–‰ λ°œμƒ κ°€λŠ₯

  if (isLoading) return <div>λ‘œλ”© 쀑...</div>;
  if (error) return <div>μ—λŸ¬ λ°œμƒ: {error}</div>;
  if (!userData) return <div>μ‚¬μš©μž 정보λ₯Ό 뢈러올 수 μ—†μŠ΅λ‹ˆλ‹€.</div>;

  return (
    <div>
      <h2>{userData.name}</h2>
      <p>이메일: {userData.email}</p>
      <p>ν”„λ‘œν•„ νƒ€μž…: {profileType}</p> {/* profileType은 λ‹¨μˆœνžˆ UI ν‘œμ‹œμš©μœΌλ‘œ κ°€μ • */}
      <button onClick={() => console.log('μƒˆλ‘œκ³ μΉ¨ κΈ°λŠ₯ μΆ”κ°€ ν•„μš”')}>데이터 μƒˆλ‘œκ³ μΉ¨ (λ―Έκ΅¬ν˜„)</button>
    </div>
  );
}

export default UserProfile;

After Code (Good)

import React, { useState, useEffect, useCallback } from 'react';

function UserProfile({ userId, profileType }) {
  const [userData, setUserData] = useState(null);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);

  // βœ… κ°œμ„ : 데이터 패칭 λ‘œμ§μ„ λ³„λ„μ˜ ν•¨μˆ˜λ‘œ λΆ„λ¦¬ν•˜κ³  useCallback으둜 λž˜ν•‘ν•˜μ—¬ ν•¨μˆ˜ μ°Έμ‘° μ•ˆμ •μ„± 확보
  // userId μ™Έ λ‹€λ₯Έ prop/stateλŠ” API ν˜ΈμΆœμ— 직접 영ν–₯을 μ£Όμ§€ μ•ŠλŠ”λ‹€λ©΄ μ˜μ‘΄μ„±μ—μ„œ μ œμ™Έ
  const fetchDataById = useCallback(async (id) => {
    if (!id) {
      setUserData(null);
      return;
    }

    setIsLoading(true);
    setError(null);

    // AbortControllerλ₯Ό μ‚¬μš©ν•˜μ—¬ μ»΄ν¬λ„ŒνŠΈ μ–Έλ§ˆμš΄νŠΈ μ‹œ μš”μ²­ μ·¨μ†Œ (λ©”λͺ¨λ¦¬ λˆ„μˆ˜ 및 경쟁 쑰건 λ°©μ§€)
    const controller = new AbortController();
    const signal = controller.signal;

    try {
      const response = await fetch(`/api/users/${id}`, { signal });
      if (!response.ok) {
        throw new Error('Failed to fetch user data');
      }
      const data = await response.json();
      setUserData(data);
    } catch (err) {
      if (err.name === 'AbortError') {
        console.log('Fetch aborted');
      } else {
        setError(err.message);
      }
    } finally {
      setIsLoading(false);
    }
    return controller; // cleanup을 μœ„ν•΄ controller λ°˜ν™˜
  }, []); // 이 ν•¨μˆ˜λŠ” μ™ΈλΆ€ μŠ€μ½”ν”„μ˜ μƒνƒœμ— μ˜μ‘΄ν•˜μ§€ μ•ŠμœΌλ―€λ‘œ 빈 λ°°μ—΄ (userIdλŠ” 인자둜 λ°›μŒ)

  // βœ… κ°œμ„ : useEffectλŠ” 이제 userIdκ°€ 변경될 λ•Œλ§Œ 데이터λ₯Ό λ‘œλ“œν•˜κ³ , 클린업 ν•¨μˆ˜λ₯Ό 제곡
  useEffect(() => {
    const controller = fetchDataById(userId);
    return () => {
      // μ»΄ν¬λ„ŒνŠΈ μ–Έλ§ˆμš΄νŠΈ λ˜λŠ” userId λ³€κ²½ μ‹œ 이전 μš”μ²­ μ·¨μ†Œ
      if (controller) controller.abort();
    };
  }, [userId, fetchDataById]); // fetchDataByIdλŠ” useCallback으둜 μ•ˆμ •μ μ΄λ―€λ‘œ userId λ³€κ²½ μ‹œμ—λ§Œ μ‹€ν–‰

  // βœ… κ°œμ„ : μ‚¬μš©μžκ°€ λͺ…μ‹œμ μœΌλ‘œ 데이터λ₯Ό λ‹€μ‹œ λ‘œλ“œν•˜κ³  싢을 λ•Œ ν˜ΈμΆœν•  ν•¨μˆ˜ (이벀트 ν•Έλ“€λŸ¬)
  const handleRefreshClick = () => {
    fetchDataById(userId);
  };

  if (isLoading) return <div>λ‘œλ”© 쀑...</div>;
  if (error) return <div>μ—λŸ¬ λ°œμƒ: {error}</div>;
  if (!userData) return <div>μ‚¬μš©μž 정보λ₯Ό 뢈러올 수 μ—†μŠ΅λ‹ˆλ‹€.</div>;

  return (
    <div>
      <h2>{userData.name}</h2>
      <p>이메일: {userData.email}</p>
      <p>ν”„λ‘œν•„ νƒ€μž…: {profileType}</p> {/* profileType은 UI ν‘œμ‹œμš©μœΌλ‘œλ§Œ μ‚¬μš© */}
      <button onClick={handleRefreshClick} disabled={isLoading}>데이터 μƒˆλ‘œκ³ μΉ¨</button>
    </div>
  );
}

export default UserProfile;