dev.syw

서버 데이터를 불러오고 복잡한 상태를 useReducer로 관리하기.

데이터 패칭과 useReducer

대부분의 앱은 서버에서 데이터를 가져옵니다. 로딩·에러·성공이라는 여러 상태가 얽히죠. useEffect 로 데이터를 불러오고, useReducer 로 복잡한 상태를 깔끔하게 관리하는 법을 배웁니다.

학습 목표

  • useEffect 안에서 데이터를 안전하게 fetch할 수 있다.
  • 로딩·에러·데이터 상태를 함께 다룰 수 있다.
  • 빠른 입력 변화로 생기는 경쟁 상태를 클린업으로 막을 수 있다.
  • 여러 상태가 얽힐 때 useReducer 로 정리할 수 있다.

useEffect로 fetch하기

import { useState, useEffect } from 'react';

function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    setLoading(true);
    fetch('/api/users')
      .then((r) => {
        if (!r.ok) throw new Error('요청 실패');
        return r.json();
      })
      .then(setUsers)
      .catch((e) => setError(e.message))
      .finally(() => setLoading(false));
  }, []);

  if (loading) return <p>불러오는 중...</p>;
  if (error) return <p>에러: {error}</p>;
  return <ul>{users.map((u) => <li key={u.id}>{u.name}</li>)}</ul>;
}

세 가지 상태를 모두 처리하는 게 핵심입니다.

상태화면
loading"불러오는 중..."
error에러 메시지
success실제 데이터

💡 TIPfetch 는 404·500 같은 HTTP 오류에서도 reject하지 않습니다. r.ok 를 직접 확인해 에러로 던져 줘야 합니다.

경쟁 상태(race condition) 막기

검색어가 빠르게 바뀌면 여러 요청이 동시에 진행되고, 늦게 보낸 요청이 먼저 도착해 엉뚱한 결과가 표시될 수 있습니다. 클린업으로 "오래된 요청 결과 무시" 플래그를 둡니다.

useEffect(() => {
  let ignore = false; // 이 effect가 유효한지

  fetch(`/api/search?q=${query}`)
    .then((r) => r.json())
    .then((data) => {
      if (!ignore) setResults(data); // 최신 요청만 반영
    });

  return () => { ignore = true; }; // query가 바뀌면 이전 요청 무효화
}, [query]);

AbortController 로 아예 요청을 취소할 수도 있습니다.

useEffect(() => {
  const controller = new AbortController();
  fetch(`/api/search?q=${query}`, { signal: controller.signal })
    .then((r) => r.json())
    .then(setResults)
    .catch((e) => { if (e.name !== 'AbortError') setError(e.message); });
  return () => controller.abort();
}, [query]);

⚠️ 주의 — 클린업 없이 여러 요청을 던지면 "이전 결과가 최신 결과를 덮어쓰는" 버그가 간헐적으로 발생합니다. 재현이 어려워 더 위험하니 처음부터 막아 두세요.

useReducer — 복잡한 상태 정리

loading·error·data처럼 여러 state가 함께 바뀐다면, useReducer 로 한 덩어리로 관리하면 깔끔합니다.

import { useReducer, useEffect } from 'react';

const initial = { status: 'loading', data: null, error: null };

function reducer(state, action) {
  switch (action.type) {
    case 'loading': return { status: 'loading', data: null, error: null };
    case 'success': return { status: 'done', data: action.data, error: null };
    case 'error':   return { status: 'error', data: null, error: action.error };
    default: return state;
  }
}

function Users() {
  const [state, dispatch] = useReducer(reducer, initial);

  useEffect(() => {
    let ignore = false;
    dispatch({ type: 'loading' });
    fetch('/api/users')
      .then((r) => r.json())
      .then((data) => { if (!ignore) dispatch({ type: 'success', data }); })
      .catch((e) => { if (!ignore) dispatch({ type: 'error', error: e.message }); });
    return () => { ignore = true; };
  }, []);

  if (state.status === 'loading') return <p>불러오는 중...</p>;
  if (state.status === 'error') return <p>에러: {state.error}</p>;
  return <ul>{state.data.map((u) => <li key={u.id}>{u.name}</li>)}</ul>;
}
  • useReducer(reducer, 초기상태)[state, dispatch] 를 반환합니다.
  • dispatch({ type, ... }) 로 "무슨 일이 일어났는지"를 알리면 reducer 가 다음 상태를 계산합니다.
  • 상태 전이 로직이 한곳(reducer)에 모여 테스트·추적이 쉽습니다.
비교useStateuseReducer
적합한 경우단순·독립된 값서로 얽힌 여러 값
갱신 방식set 함수dispatch(action)
로직 위치컴포넌트 곳곳reducer 한 곳

💡 TIP — 불가능한 상태 조합(예: loading이면서 error)을 막으려면 status 같은 단일 필드로 표현하는 "상태 머신" 방식이 안전합니다.

요약

  • 데이터 패칭은 loading·error·success 세 상태를 함께 다룬다.
  • fetch 는 HTTP 오류를 던지지 않으니 r.ok 를 직접 확인한다.
  • 입력이 자주 바뀌면 클린업(ignore 플래그·AbortController)으로 경쟁 상태를 막는다.
  • 얽힌 여러 state는 useReducer 로 reducer 한곳에 모은다.
  • 실무에서는 React Query 같은 라이브러리가 이런 일을 대신해 주기도 한다.

연습문제

  1. 버튼 클릭 시 랜덤 사용자 한 명을 fetch해 로딩·에러·결과를 모두 표시하세요.
  2. 검색창 입력에 따라 결과를 불러오되, ignore 플래그로 경쟁 상태를 막으세요.
  3. 2번을 AbortController 방식으로 다시 구현해 보세요.
  4. 카운터 예제를 useReducer 로 작성하세요. (increment, decrement, reset 액션)
  5. Users 예제에 "새로고침" 버튼을 추가해 다시 fetch하도록 만드세요.

힌트 — 5번은 effect 의존성에 version 같은 state를 넣고 버튼이 그 값을 증가시키게 하세요.

💡 연습문제 풀이

불러오는 중…

함께 보면 좋은 자료

댓글 0

React.js” 강좌에 대한 댓글입니다.

댓글을 작성하려면 로그인이 필요합니다.