전체 목록
ReactEasy#13

React에서 useState와 useEffect의 역할과 사용법을 설명해주세요.

#React##상태관리
힌트

상태 관리와 사이드 이펙트 처리의 기본 훅입니다.

정답 및 해설

React에서 useState와 useEffect의 역할과 사용법을 설명해주세요.

useStateuseEffect는 React 함수형 컴포넌트에서 가장 기본적이고 핵심적인 훅(Hook)입니다. useState는 컴포넌트 내부의 상태(state)를 관리하고, useEffect는 렌더링 이후 발생하는 사이드 이펙트(부수 효과)를 처리합니다. 두 훅을 올바르게 이해하고 조합하면 클래스형 컴포넌트의 setState, componentDidMount, componentDidUpdate, componentWillUnmount 등을 모두 대체할 수 있습니다.


useState

개념과 기본 사용법

useState는 컴포넌트의 로컬 상태를 선언합니다. 초기값을 인수로 받아 [현재 상태, 상태 변경 함수] 형태의 배열을 반환합니다.

import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>현재 카운트: {count}</p>
      <button onClick={() => setCount(count + 1)}>증가</button>
      <button onClick={() => setCount(count - 1)}>감소</button>
      <button onClick={() => setCount(0)}>초기화</button>
    </div>
  );
}

다양한 타입의 상태 관리

function UserForm() {
  // 문자열 상태
  const [name, setName] = useState('');

  // 숫자 상태
  const [age, setAge] = useState(0);

  // 불리언 상태
  const [isVisible, setIsVisible] = useState(false);

  // 객체 상태
  const [user, setUser] = useState({ name: '', email: '' });

  // 배열 상태
  const [items, setItems] = useState([]);

  return (
    <form>
      <input value={name} onChange={e => setName(e.target.value)} />
      <input
        value={user.email}
        onChange={e => setUser(prev => ({ ...prev, email: e.target.value }))}
      />
    </form>
  );
}

함수형 업데이트 (Functional Update)

이전 상태를 기반으로 새 상태를 계산할 때는 함수형 업데이트를 사용해야 합니다. 비동기 환경에서 최신 상태를 보장합니다.

function Counter() {
  const [count, setCount] = useState(0);

  // 잘못된 방법: 연속 호출 시 최신 상태를 반영하지 못할 수 있음
  const handleWrongIncrement = () => {
    setCount(count + 1);
    setCount(count + 1); // 여전히 이전 count를 참조
  };

  // 올바른 방법: 함수형 업데이트로 최신 상태 보장
  const handleCorrectIncrement = () => {
    setCount(prev => prev + 1);
    setCount(prev => prev + 1); // 항상 최신 상태에서 +1
  };

  return (
    <div>
      <p>{count}</p>
      <button onClick={handleCorrectIncrement}>+2 (정확)</button>
    </div>
  );
}

지연 초기화 (Lazy Initialization)

초기값 계산 비용이 클 때 함수를 전달하면 최초 렌더링 시에만 실행됩니다.

function ExpensiveComponent() {
  // 매 렌더링마다 실행됨 (비효율)
  // const [state, setState] = useState(computeExpensiveValue());

  // 최초 마운트 시에만 실행됨 (효율적)
  const [state, setState] = useState(() => {
    return computeExpensiveValue();
  });

  return <div>{state}</div>;
}

function loadFromStorage() {
  const [todos, setTodos] = useState(() => {
    const saved = localStorage.getItem('todos');
    return saved ? JSON.parse(saved) : [];
  });
  // ...
}

객체 상태 업데이트 시 주의사항

function ProfileEditor() {
  const [profile, setProfile] = useState({
    name: '홍길동',
    age: 30,
    address: { city: '서울', district: '강남구' }
  });

  // 중첩 객체 업데이트: 스프레드 연산자로 불변성 유지
  const updateCity = (newCity) => {
    setProfile(prev => ({
      ...prev,
      address: {
        ...prev.address,
        city: newCity
      }
    }));
  };

  // 배열 상태 업데이트
  const [items, setItems] = useState(['사과', '바나나']);

  const addItem = (item) => setItems(prev => [...prev, item]);
  const removeItem = (index) => setItems(prev => prev.filter((_, i) => i !== index));
  const updateItem = (index, newItem) =>
    setItems(prev => prev.map((item, i) => i === index ? newItem : item));
}

useEffect

개념과 기본 사용법

useEffect는 컴포넌트가 렌더링된 후 사이드 이펙트를 실행합니다. 데이터 패칭, 구독 설정, DOM 직접 조작, 타이머 등록 등에 사용됩니다.

import { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    // 렌더링 후 실행되는 사이드 이펙트
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => setUser(data));
  }, [userId]); // userId가 변경될 때마다 재실행

  if (!user) return <div>로딩 중...</div>;
  return <div>{user.name}</div>;
}

의존성 배열로 실행 시점 제어

function Examples() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');

  // 1. 의존성 배열 없음: 매 렌더링마다 실행
  useEffect(() => {
    console.log('매 렌더링마다 실행됨');
  });

  // 2. 빈 배열: 마운트 시 1회만 실행
  useEffect(() => {
    console.log('컴포넌트 마운트 시 1회 실행');
  }, []);

  // 3. 특정 의존성: 해당 값이 변경될 때마다 실행
  useEffect(() => {
    console.log('count가 변경됨:', count);
  }, [count]);

  // 4. 여러 의존성
  useEffect(() => {
    console.log('count 또는 name이 변경됨');
  }, [count, name]);
}

클린업 함수 (Cleanup Function)

클린업 함수는 컴포넌트가 언마운트되거나 효과가 재실행되기 전에 호출됩니다.

function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setSeconds(prev => prev + 1);
    }, 1000);

    // 클린업: 언마운트 시 또는 재실행 전에 인터벌 정리
    return () => {
      clearInterval(interval);
    };
  }, []); // 마운트/언마운트 시에만

  return <div>경과 시간: {seconds}초</div>;
}
function ChatRoom({ roomId }) {
  useEffect(() => {
    // 웹소켓 구독
    const socket = createWebSocket(roomId);
    socket.connect();
    socket.on('message', handleMessage);

    // 클린업: roomId 변경 시 이전 구독 해제
    return () => {
      socket.off('message', handleMessage);
      socket.disconnect();
    };
  }, [roomId]); // roomId가 바뀌면 재연결
}

데이터 패칭과 경쟁 조건(Race Condition) 방지

function SearchResults({ query }) {
  const [results, setResults] = useState([]);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    if (!query) return;

    let isCancelled = false; // 클린업 플래그
    setLoading(true);

    fetch(`/api/search?q=${query}`)
      .then(res => res.json())
      .then(data => {
        if (!isCancelled) {
          // 컴포넌트가 여전히 마운트 상태일 때만 상태 업데이트
          setResults(data);
          setLoading(false);
        }
      });

    return () => {
      isCancelled = true; // 이전 요청 무시
    };
  }, [query]);

  return (
    <div>
      {loading ? <span>검색 중...</span> : results.map(r => <div key={r.id}>{r.title}</div>)}
    </div>
  );
}

AbortController를 활용한 패칭 취소

function ProductDetail({ productId }) {
  const [product, setProduct] = useState(null);

  useEffect(() => {
    const controller = new AbortController();

    fetch(`/api/products/${productId}`, {
      signal: controller.signal
    })
      .then(res => res.json())
      .then(data => setProduct(data))
      .catch(err => {
        if (err.name !== 'AbortError') {
          console.error('패칭 오류:', err);
        }
      });

    return () => controller.abort(); // 언마운트 시 요청 취소
  }, [productId]);

  return product ? <div>{product.name}</div> : <div>로딩 중...</div>;
}

이벤트 리스너 등록/해제

function ScrollTracker() {
  const [scrollY, setScrollY] = useState(0);

  useEffect(() => {
    const handleScroll = () => setScrollY(window.scrollY);

    window.addEventListener('scroll', handleScroll);

    // 클린업: 이벤트 리스너 제거
    return () => window.removeEventListener('scroll', handleScroll);
  }, []); // 마운트 시 1회 등록

  return <div>스크롤 위치: {scrollY}px</div>;
}

useState와 useEffect 조합 패턴

데이터 패칭 패턴

function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const controller = new AbortController();
    setLoading(true);
    setError(null);

    fetch(url, { signal: controller.signal })
      .then(res => {
        if (!res.ok) throw new Error(`HTTP error: ${res.status}`);
        return res.json();
      })
      .then(data => {
        setData(data);
        setLoading(false);
      })
      .catch(err => {
        if (err.name !== 'AbortError') {
          setError(err.message);
          setLoading(false);
        }
      });

    return () => controller.abort();
  }, [url]);

  return { data, loading, error };
}

// 사용 예시
function UserList() {
  const { data: users, loading, error } = useFetch('/api/users');

  if (loading) return <div>로딩 중...</div>;
  if (error) return <div>오류: {error}</div>;
  return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

클래스형 생명주기 메서드와의 비교

클래스형 생명주기useEffect 대응
componentDidMountuseEffect(() => {}, [])
componentDidUpdateuseEffect(() => {}, [dep])
componentWillUnmountuseEffect(() => { return () => {} }, [])
componentDidMount + componentDidUpdateuseEffect(() => {}) (의존성 배열 없음)

주요 주의사항 및 Best Practices

항목올바른 사용잘못된 사용
함수형 업데이트setCount(prev => prev + 1)비동기 로직에서 setCount(count + 1)
객체 상태스프레드로 불변성 유지직접 객체 프로퍼티 수정
useEffect 의존성사용하는 모든 외부 값 포함의존성 누락으로 stale closure
클린업구독, 타이머, 이벤트 리스너 해제클린업 없이 반복 등록
패칭 취소AbortController 또는 플래그 사용경쟁 조건 방치

핵심 정리

  • useState는 상태 변경 시 리렌더링을 트리거하며, 불변성을 지켜 새 상태를 설정해야 합니다.
  • useEffect는 렌더링 이후 실행되므로 초기 렌더링에는 이전 상태가 표시될 수 있습니다.
  • 클린업 함수는 메모리 누수와 경쟁 조건을 방지하는 핵심 메커니즘입니다.
  • 의존성 배열에 모든 의존 값을 포함하지 않으면 오래된 클로저(stale closure) 문제가 발생합니다.