React-Query Tanstack Query

joyoung·2025년 2월 22일

리액트 쿼리(React Query)란?

React Query는 React 애플리케이션에서 서버 상태(Server State)를 관리하기 위한 라이브러리입니다.
클라이언트에서 서버의 데이터를 요청하고 관리하는 작업을 단순화하며,
fetching, caching, 동기화, 자동 갱신 등의 기능을 제공합니다.


기존 방식과의 차이점

React의 기본 상태관리 도구(예: useState, useEffect)는 로컬 상태(Client State) 관리에 적합
하지만 서버에서 가져오는 비동기 데이터를 관리할 때는 다음과 같은 문제가 발생:

  • 중복 요청 관리 어려움
  • 로딩/에러 상태 직접 처리 필요
  • 데이터 동기화 및 갱신 처리 복잡

React Query는 이러한 서버 상태 관리를 자동화 및 최적화해줌


React Query로 얻을 수 있는 기능

  1. 자동 캐싱(Caching)
    - 요청된 데이터를 캐시에 저장해 중복 요청 방지
    - 동일한 쿼리에 대해 서버 재요청 최소화

  2. 상태 관리 자동화
    - 로딩(isLoading), 에러(isError), 성공(isSuccess) 상태 자동 제공
    - 로직 분리 및 컴포넌트 간결화

  3. 다양한 옵션 제공
    - 자동 재시도
    - 백그라운드에서 데이터 자동 갱신
    - 브라우저 포커스 시 자동 refetch
    - 의존성 기반 쿼리 호출 제어 (enabled, staleTime 등)

  4. 서버 상태 동기화
    - 최신 데이터 유지 보장
    - 실시간에 가까운 사용자 경험 제공 가능


언제 사용할까?

  • API 호출이 많은 SPA
  • 서버 데이터 중심의 대시보드, 모니터링 시스템
  • 검색, 필터링, 페이징 등 데이터 요청이 반복되는 화면
  • 사용자 활동이 잦고, 데이터 신뢰성이 중요한 서비스

리액트 훅 사용

import { useState, useEffect } from 'react';

function TodoList() {
  const [data, setData] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const [isError, setIsError] = useState(false);
  const [error, setError] = useState(null);

  const fetchData = async () => {
    setIsLoading(true);
    try {
      const response = await fetch('/api/todos');
      const result = await response.json();
      setData(result);
      setIsError(false);
      setError(null);
    } catch (err) {
      setIsError(true);
      setError(err);
    } finally {
      setIsLoading(false);
    }
  };

  useEffect(() => {
    fetchData();
  }, []);

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

  return (
    <div>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
}

리액트 쿼리 사용

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

function TodoList() {
  const { data, isLoading, isError, error } = useQuery({
    queryKey: ['todos'],
    queryFn: () => fetch('/api/todos').then(res => res.json())
  });

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

  return (
    <div>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
}

리액트 쿼리 옵션들

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

function TodoList() {
  const {
    // 데이터와 상태
    data,                    // 성공적으로 가져온 데이터
    dataUpdatedAt,          // 마지막 데이터 업데이트 시간
    error,                  // 에러 객체
    errorUpdatedAt,         // 마지막 에러 발생 시간
    failureCount,           // 실패 횟수
    failureReason,          // 실패 이유
    isError,                // 에러 상태
    isFetched,              // 한번이라도 가져왔는지
    isFetchedAfterMount,    // 마운트 후 가져왔는지
    isFetching,             // 현재 가져오는 중인지
    isLoading,              // 첫 로딩 상태
    isLoadingError,         // 로딩 중 에러
    isPaused,               // 일시 중지 상태
    isPlaceholderData,      // 임시 데이터 사용 중인지
    isPreviousData,         // 이전 데이터 사용 중인지
    isRefetchError,         // 리페칭 중 에러
    isRefetching,           // 리페칭 중인지
    isStale,                // 데이터가 오래됐는지
    isSuccess,              // 성공 상태
    status,                 // 상태 문자열

    // 함수들
    refetch,                // 수동 리페칭
    remove,                 // 쿼리 캐시에서 제거
    fetchStatus             // 현재 fetch 상태
  } = useQuery({
    queryKey: ['todos'],    // 쿼리 키 (필수)
    queryFn: fetchTodos,    // 데이터 페칭 함수 (필수)
    
    // 일반 옵션
    cacheTime: 1000 * 60 * 5,             // 캐시 유지 시간 (5분)
    enabled: true,                         // 자동 실행 여부
    networkMode: 'online',                 // 네트워크 모드
    staleTime: 1000 * 60,                 // 데이터 신선도 유지 시간 (1분)
    suspense: false,                       // React Suspense 사용 여부
    
    // 리페칭 옵션
    refetchInterval: false,                // 주기적 리페칭 간격
    refetchIntervalInBackground: false,    // 백그라운드 리페칭 
    refetchOnMount: true,                  // 마운트 시 리페칭
    refetchOnReconnect: true,              // 재연결 시 리페칭
    refetchOnWindowFocus: true,            // 윈도우 포커스 시 리페칭
    
    // 재시도 옵션
    retry: 3,                              // 재시도 횟수
    retryDelay: attemptIndex =>            // 재시도 간격
      Math.min(1000 * 2 ** attemptIndex, 30000),
    retryOnMount: true,                    // 마운트 시 재시도
    
    // 콜백 함수들
    onError: (error) => {
      console.error('요청 실패:', error);
    },
    onSuccess: (data) => {
      console.log('데이터 수신 성공:', data);
    },
    onSettled: (data, error) => {
      console.log('요청 완료', { data, error });
    },
    
    // 데이터 변환
    select: (data) => {
      return data.map(item => ({
        ...item,
        title: item.title.toUpperCase()
      }));
    },
    
    // 초기 데이터
    initialData: () => [],                 // 초기 데이터
    placeholderData: [],                   // 임시 데이터
    
    // 기타 옵션
    keepPreviousData: true,               // 이전 데이터 유지
    structuralSharing: true               // 구조적 공유 사용
  });

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

  return (
    <div>
      <pre>{JSON.stringify(data, null, 2)}</pre>
      <button onClick={() => refetch()}>새로고침</button>
    </div>
  );
}
profile
꾸준히

0개의 댓글