본문 바로가기
Tech Notes

React useEffect 무한루프 완전 해결 가이드: 원인부터 해결책까지

by miracle-tech 2025. 8. 18.
728x90
반응형

React 개발을 하다 보면 한 번쯤은 마주치게 되는 문제가 있습니다. 바로 useEffect 무한루프입니다.

"어? 왜 API가 계속 호출되지?" "브라우저가 멈췄어요!" "개발자 도구 네트워크 탭이 빨간색으로 가득해요!"

혹시 이런 경험 있으신가요? 저도 React를 처음 배울 때 이 문제로 몇 시간을 헤맸던 기억이 납니다. 오늘은 이 골치 아픈 무한루프 문제를 완전히 해결하는 방법을 알려드리겠습니다.

🎯 무한루프란 무엇인가?

무한루프는 말 그대로 끝없이 반복되는 루프를 의미합니다. React에서는 주로 useEffect가 계속해서 실행되는 상황을 말합니다.

💥 실제 발생한 문제 사례

최근에 사용자 인증 기능을 구현하다가 이런 코드를 작성했습니다:

function Home() {
  const [isCheckingProfile, setIsCheckingProfile] = useState(false);
  const { user, loading: authLoading } = useAuth();

  useEffect(() => {
    if (user && !isCheckingProfile) {
      setIsCheckingProfile(true); // 👈 문제의 시작
      checkUserProfileAndRedirect();
    }
  }, [user, authLoading, isCheckingProfile]); // 👈 여기가 문제!

  return <div>홈 화면</div>;
}

결과는? 브라우저가 먹통이 되었습니다! 😱

 

🔍 React 컴포넌트 실행 순서 이해하기

무한루프 문제를 해결하려면 먼저 React 컴포넌트가 어떤 순서로 실행되는지 알아야 합니다.

📋 컴포넌트 생명주기

export default function Home() {
  // 1️⃣ 먼저 실행 - useState, 커스텀 훅 등
  const [showSplash, setShowSplash] = useState(true);
  const [isCheckingProfile, setIsCheckingProfile] = useState(false);
  const { user, loading: authLoading } = useAuth(); // 커스텀 훅도 여기서 실행
  const router = useRouter();
  
  // 2️⃣ 나중에 실행 - useEffect (컴포넌트 렌더링 후)
  useEffect(() => {
    // 이 코드는 컴포넌트가 화면에 그려진 후 실행됨
  }, [user, authLoading]);
  
  // 3️⃣ return 문 (렌더링)
  return <div>...</div>;
}

🎭 상세 실행 단계

순서 단계 설명

1단계 함수 실행 useState, 커스텀 훅 등 초기화
2단계 렌더링 return 문의 JSX가 DOM에 그려짐
3단계 useEffect 실행 부수 효과(side effect) 처리

핵심 포인트: useEffect는 항상 렌더링 이후에 실행됩니다!

 

🌀 무한루프가 발생하는 이유

🔄 useEffect 동작 원리

useEffect(() => {
  // 실행할 코드
}, [의존성1, 의존성2]); // 👈 이 값들 중 하나라도 바뀌면 재실행

useEffect는 의존성 배열의 값이 변경될 때마다 다시 실행됩니다. 이게 바로 무한루프의 원인이 됩니다.

💀 무한루프 발생 과정

앞서 본 문제 코드를 다시 살펴보겠습니다:

useEffect(() => {
  if (user && !isCheckingProfile) {
    setIsCheckingProfile(true); // 👈 상태 변경!
    checkUserProfileAndRedirect();
  }
}, [user, authLoading, isCheckingProfile]); // 👈 변경된 상태가 의존성에 포함됨

무한루프 단계별 분석:

1. isCheckingProfile: false → true 변경
   ↓
2. 의존성 배열에 isCheckingProfile이 있어서 useEffect 재실행 🔄
   ↓
3. 조건문 통과 못함 (이미 true니까)
   ↓
4. finally에서 다시 false로 변경
   ↓
5. 또 useEffect 재실행... 무한 반복! 🌀

🛠️ 해결 방법

✅ 1. 의존성 배열에서 문제 요소 제거

가장 간단한 해결책은 useEffect 내부에서 변경하는 state를 의존성 배열에서 제거하는 것입니다.

// ❌ 문제가 있는 코드
useEffect(() => {
  if (user && !isCheckingProfile) {
    setIsCheckingProfile(true);
    checkUserProfileAndRedirect();
  }
}, [user, authLoading, isCheckingProfile]); // isCheckingProfile 때문에 무한루프

// ✅ 해결된 코드
useEffect(() => {
  if (user && !isCheckingProfile) {
    setIsCheckingProfile(true);
    checkUserProfileAndRedirect();
  }
}, [user, authLoading]); // isCheckingProfile 제거!

✅ 2. useCallback으로 함수 최적화

함수가 의존성 배열에 있을 때도 무한루프가 발생할 수 있습니다:

// ❌ 무한루프 위험
function UserList() {
  const [users, setUsers] = useState([]);
  
  const fetchUsers = () => {
    api.getUsers().then(setUsers);
  };
  
  useEffect(() => {
    fetchUsers(); // 함수가 매번 새로 생성되어 무한루프
  }, [fetchUsers]);
  
  return <div>{/* 렌더링 */}</div>;
}

// ✅ useCallback으로 해결
function UserList() {
  const [users, setUsers] = useState([]);
  
  const fetchUsers = useCallback(() => {
    api.getUsers().then(setUsers);
  }, []); // 의존성이 없으므로 함수가 재생성되지 않음
  
  useEffect(() => {
    fetchUsers();
  }, [fetchUsers]);
  
  return <div>{/* 렌더링 */}</div>;
}

✅ 3. 함수형 업데이트 사용

이전 state 값을 기반으로 업데이트할 때는 함수형 업데이트를 사용하세요:

// ❌ 무한루프 위험
useEffect(() => {
  setCount(count + 1);
}, [count]); // count가 변경될 때마다 실행 → 무한루프

// ✅ 함수형 업데이트로 해결
useEffect(() => {
  setCount(prev => prev + 1); // 이전 값 기반 업데이트
}, []); // 의존성 없음

📝 의존성 배열 규칙

🟢 포함해야 하는 것

  • useEffect 내부에서 읽기만 하는 state, props
  • useEffect 내부에서 사용하는 외부 함수, 변수
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  
  useEffect(() => {
    if (userId) { // userId는 읽기만 하므로 의존성에 포함
      fetchUser(userId).then(setUser);
    }
  }, [userId]); // ✅ 올바른 의존성
  
  return <div>{user?.name}</div>;
}

🔴 포함하지 말아야 하는 것

  • useEffect 내부에서 변경하는 state
  • setter 함수들 (useState의 반환값)
function Counter() {
  const [count, setCount] = useState(0);
  const [loading, setLoading] = useState(false);
  
  useEffect(() => {
    setLoading(true); // loading을 변경하지만 의존성에 포함하지 않음
    
    setTimeout(() => {
      setCount(prev => prev + 1); // count를 변경하지만 의존성에 포함하지 않음
      setLoading(false);
    }, 1000);
  }, []); // ✅ 변경하는 state들은 의존성에 포함하지 않음
  
  return <div>{loading ? 'Loading...' : count}</div>;
}

🎯 실전 예제

나의 경우엔 다음과 같은 로직으로 무한 루프가 발생했었다.

 

1️⃣ 로그인 하기 전 (초기 렌더링)
  showSplash, isCheckingProfile 선언

  → useAuth 실행(hook도 실행됨)

  → 렌더링 (return 이후 로직 실행)

 

2️⃣ 로그인 후 -> user 가 있음 -> useEffect 실행

  showSplash, isCheckingProfile 선언

  → useAuth 실행

  → 렌더링
  →  useEffect 실행 → setIsCheckingProfile(true) → checkUserProfileAndRedirect()
   router.push('/dashboard') 실행 → finally { setIsCheckingProfile(false) } 여기가 문제!
  → isCheckingProfile 변경 (true → false)
       useEffect 의존성 배열 [user, authLoading, isCheckingProfile] 때문에 다시 실행

 

  → 다시 처음 부터 실행. user && !isCheckingProfile (true) → setIsCheckingProfile(true) 에 의해 또 실행
  → 무한 반복...

🛡️ 무한루프 방지 체크리스트

개발할 때 이 체크리스트를 확인해보세요:

✅ 의존성 배열 점검

  • [ ] useEffect 내부에서 변경하는 state가 의존성에 포함되어 있지 않은가?
  • [ ] setter 함수가 의존성에 포함되어 있지 않은가?
  • [ ] 객체나 배열을 직접 의존성에 포함하지 않았나?

✅ 함수 최적화 점검

  • [ ] useEffect에서 사용하는 함수가 useCallback으로 최적화되어 있는가?
  • [ ] 불필요하게 매번 새로 생성되는 함수는 없는가?

✅ 로직 구조 점검

  • [ ] 한 번만 실행되어야 하는 로직이 반복 실행되고 있지 않은가?
  • [ ] 조건부 실행이 제대로 구현되어 있는가?

🔧 디버깅 팁

1. console.log 활용

useEffect(() => {
  console.log('useEffect 실행됨', { user, authLoading, isCheckingProfile });
  
  if (user && !isCheckingProfile) {
    console.log('인증 확인 시작');
    setIsCheckingProfile(true);
    checkUserProfileAndRedirect();
  }
}, [user, authLoading]); // 의존성도 콘솔에 출력

2. React Developer Tools 사용

React Developer Tools의 Profiler 탭을 사용하면 컴포넌트가 언제, 왜 리렌더링되는지 확인할 수 있습니다.

3. useEffect 분리

복잡한 useEffect는 여러 개로 분리해서 어떤 부분에서 문제가 발생하는지 파악하기 쉽게 만드세요.

💡 마무리

React useEffect 무한루프는 처음엔 어려워 보이지만, 원리를 이해하면 충분히 해결할 수 있는 문제입니다.

핵심 원칙들을 다시 정리하면:

  1. useEffect 내부에서 변경하는 state는 의존성에 포함하지 않기
  2. 함수는 useCallback으로 최적화하기
  3. 객체/배열보다는 원시값을 의존성에 사용하기
  4. 관련된 로직은 하나의 effect로 통합하기

이 원칙들만 지키셔도 대부분의 무한루프 문제를 예방할 수 있습니다.

무한루프 때문에 고생하고 계신 분들에게 이 글이 도움이 되었으면 좋겠습니다. React 개발이 더욱 즐거워지시길 바랍니다! 🚀


이 글이 도움이 되셨다면 ❤️ 공감과 댓글로 응원해주세요! 더 좋은 개발 팁으로 찾아뵙겠습니다.

728x90