React Query, useQuery, useSuspenseQuery

이유림·2025년 9월 14일
0

React Query란?

React Query는 서버 상태를 효율적으로 관리할 수 있게 도와주는 라이브러리이다.
서버 데이터가 여러 컴포넌트에서 필요할 때 동기화, 캐싱, 갱신 처리 등이 복잡해질 수 있는데, 이 때 React Query를 사용하면 이런 문제가 해결된다. 주요 기능은 다음과 같다

  • 캐싱: 한 번 불러온 데이터는 캐시에 저장되어 재사용 가능
  • 자동 갱신: 데이터가 최신 상태를 유지하도록 주기적 갱신 지원
  • 백그라운드 업데이트: UI는 캐시 데이터를 먼저 보여주고, 서버에서 새로운 데이터를 받아 업데이트
  • 비동기 상태 관리: 로딩, 오류 상태를 쉽게 관리

useQuery란?

useQuery는 서버 데이터를 가져오는 핵심 훅이다. useState로 하던 번거로운 비동기 로직을 간결하게 만들어준다.

import { useQuery } from '@tanstack/react-query';

const fetchBooks = async ({ signal }) => {
  const res = await fetch('/api/books', { signal });
  if (!res.ok) throw new Error('Failed to fetch books');
  return res.json();
};

function BookList() {
  const { data, isLoading, isError, error, refetch } = useQuery({
    queryKey: ['books'],
    queryFn: fetchBooks,
    staleTime: 1000 * 60,
  });

  if (isLoading) return <p>로딩중...</p>;
  if (isError) return <p>에러: {error.message}</p>;

  return (
    <ul>
      {data.map(b => <li key={b.id}>{b.title}</li>)}
    </ul>
  );
}

useQuery가 반환하는 주요 값

  • data: 성공적으로 가져온 데이터

  • isLoading: 초기 로딩 중인지

  • isFetching: 백그라운드에서 새로고침 중인지 (데이터가 있어도 true가 될 수 있음)

  • isError / error: 에러 발생 여부와 에러 객체

  • isSuccess: 데이터가 성공적으로 로드된 상태

  • refetch(): 수동으로 재요청

  • Pagination: useQuery + keepPreviousData(페이지 전환시 이전 데이터 유지) 또는 useInfiniteQuery 사용.

  • 병렬 요청: 여러 독립 쿼리는 각각 useQuery로 병렬 처리. 여러 쿼리를 동적으로 만들 땐 useQueries 사용.

  • Optimistic updates: useMutation의 onMutate에서 queryClient.setQueryData로 즉시 UI 반영 → 실패 시 rollback.

  • SSR / SSG: 서버에서 queryClient.prefetchQuery로 데이터를 채우고 dehydrate로 클라이언트로 전달 → <Hydrate state={dehydratedState}>.

  • Suspense: suspense: true로 설정하면 useQuery가 로딩 관리를 Suspense가 담당하게 한다.

useQuery는 로딩 상태나 에러 상태를 훅이 반환해주는 값으로 직접 관리해야 한다. 그래서 보통 컴포넌트 안에서 if (isLoading) return ... 같은 분기문을 써야 하는데, 이 방법은 컴포넌트 내부에 로딩/에러 분기 처리가 많아지면 코드가 지저분해질 수 있다.

이런 불편함을 해결하기 위해 useSuspenseQuery라는 훅이 추가되었다. 이 훅은 리액트의 Suspense와 함께 동작하는데, 이 훅을 사용하면 isLoading 같은 상태를 직접 확인할 필요가 없다. Suspense의 fallback으로 로딩 UI를 처리하고, 에러도 ErrorBoundary에서 처리하게 된다.

useQuery로 데이터를 가져올 때는 아래와 같이 작성한다.

function BookList() {
  const { data, isLoading, isError } = useQuery({
    queryKey: ['books'],
    queryFn: fetchBooks,
  });

  if (isLoading) return <p>로딩중...</p>;
  if (isError) return <p>에러 발생!</p>;

  return (
    <ul>
      {data.map(b => <li key={b.id}>{b.title}</li>)}
    </ul>
  );
}

같은 내용으로 useSuspenseQuery로 바꾸면 아래와 같이 단순해진다.

function BookList() {
  const { data } = useSuspenseQuery({
    queryKey: ['books'],
    queryFn: fetchBooks,
  });

  return (
    <ul>
      {data.map(b => <li key={b.id}>{b.title}</li>)}
    </ul>
  );
}

// 상위 컴포넌트에서 Suspense 처리
<Suspense fallback={<p>로딩중...</p>}>
  <BookList />
</Suspense>

useQuery는 로딩 / 에러를 직접 분기해야 하지만, useSuspenseQuery는 훨씬 깔끔해진다. 대신 전체 앱 구조에서 fallback, ErrorBoundary 등을 고려해야 한다.

useSuspenseQuery란?

React Suspense와 함께 사용하는 쿼리 훅이다.
특징은 다음과 같다.

  • Suspense의 fallback UI로 처리되기 때문에, 로딩 상태(isLoading)를 따로 확인할 필요가 없다.
  • 에러도 ErrorBoundary에서 처리할 수 있어, 컴포넌트 코드가 매우 단순해진다.
  • 단, Suspense와 ErrorBoundary 환경을 갖추어야 하므로 프로젝트 구조 설계 시 조금 고려해야 한다.

0개의 댓글