💻데브노트소개
🎨

React useEffect, useState 완벽 정리: 의존성 배열부터 클린업까지

데브노트 편집팀·2026.06.14·6분 읽기
X(트위터)
ADVERTISEMENT

React로 개발하다 보면 가장 먼저, 그리고 가장 자주 쓰는 훅이 바로 useState와 useEffect입니다. 하지만 동시에 가장 많은 버그가 발생하는 곳이기도 합니다. "왜 화면이 안 바뀌지?", "왜 API가 무한히 호출되지?" 같은 문제는 대부분 이 두 훅의 동작 원리를 정확히 모를 때 생깁니다. 이 글에서 핵심 원리와 흔한 실수를 정리합니다.

useState: 상태는 직접 바꾸지 않는다

가장 흔한 실수는 상태를 직접 변경하는 것입니다. React는 **불변성(immutability)**을 기준으로 리렌더링을 결정하므로, 객체나 배열은 새로 만들어 교체해야 합니다.

import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  const [user, setUser] = useState({ name: '철수', age: 20 });

  // 잘못된 예: user.age = 21; (리렌더링 안 됨)
  // 올바른 예: 새 객체로 교체
  const birthday = () => setUser(prev => ({ ...prev, age: prev.age + 1 }));

  // 연속 업데이트는 함수형 갱신 사용
  const addTwice = () => {
    setCount(prev => prev + 1);
    setCount(prev => prev + 1); // 정상적으로 +2
  };

  return <button onClick={addTwice}>{count}</button>;
}

팁: 이전 상태를 기반으로 갱신할 때는 항상 setCount(prev => ...) 형태의 함수형 업데이트를 쓰세요. 클로저로 인한 오래된(stale) 값 문제를 피할 수 있습니다.

useEffect: 의존성 배열이 전부다

useEffect는 렌더링 이후 실행되는 사이드 이펙트입니다. 두 번째 인자인 의존성 배열이 실행 시점을 결정합니다.

의존성 배열실행 시점
없음매 렌더링마다 실행
[] (빈 배열)마운트 시 1회만 실행
[a, b]a 또는 b가 바뀔 때 실행
import { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  const [data, setData] = useState(null);

  useEffect(() => {
    let ignore = false; // 경쟁 상태(race condition) 방지
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(json => {
        if (!ignore) setData(json);
      });
    return () => { ignore = true; }; // 클린업
  }, [userId]); // userId가 바뀔 때마다 다시 호출

  return <div>{data?.name ?? '로딩 중...'}</div>;
}

무한 루프와 클린업 함수

의존성 배열에 매 렌더링마다 새로 만들어지는 객체/함수를 넣으면 무한 루프가 발생합니다. 객체는 참조값이 매번 달라지기 때문입니다.

  • 무한 루프 원인: effect 안에서 state를 바꾸는데, 그 state가 의존성에 들어있는 경우
  • 해결: 함수는 useCallback, 객체는 useMemo로 메모이제이션하거나 원시값만 의존성에 넣기
  • 클린업: setInterval, 이벤트 리스너, 구독은 반드시 return () => {...}로 해제
useEffect(() => {
  const timer = setInterval(() => console.log('tick'), 1000);
  return () => clearInterval(timer); // 언마운트 시 정리
}, []);

마무리 체크리스트

  • 상태 변경은 항상 새 값/새 객체로 교체 (불변성 유지)
  • 이전 값 기반 갱신은 함수형 업데이트 setX(prev => ...)
  • useEffect 의존성 배열에 effect에서 쓰는 모든 값을 정확히 명시
  • 타이머·리스너·구독은 반드시 클린업 함수로 해제
  • 비동기 fetch에는 ignore 플래그로 경쟁 상태 방어

이 원칙만 지켜도 React 훅 관련 버그의 90%는 사라집니다.

#React#useEffect#useState#훅
X(트위터)
ADVERTISEMENT