💻데브노트소개
🎨

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

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

React로 개발하다 보면 가장 먼저, 그리고 가장 자주 쓰는 훅이 바로 useStateuseEffect입니다. 하지만 동시에 가장 많은 버그가 발생하는 곳이기도 합니다. "왜 화면이 안 바뀌지?", "왜 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