[React 심화] TanStack Query 2 - 동작원리

조아영·2025년 3월 19일

📌

TanStack Query는 서버 상태 관리를 위한 라이브러리. 서버 상태란 서버와의 통신을 통해 받아오는 데이터 의미.

클라이언트 상태와 달리 다음과 같은 관리 필요.

  • Fetching(패칭) : 서버에서 데이터를 가져옴.
  • Caching(캐싱) : 가져온 데이터를 캐싱하여, 동일한 데이터를 반복해서 요청하지 않음.
  • Synchronizing(동기화) : 서버의 데이터와 캐시된 데이터를 동기화.
  • Updating(업데이트) : 서버의 데이터를 쉽게 업데이트하고, 이를 캐시에 반영.

TanStack Query는 위 과정을 자동화 및 간소화 지원.

stale-while-revalidate(swr) 전략

SWR은 최신 데이터가 도착하기 전까지 기존 캐시 데이터를 사용하는 전략.
이를 통해 사용자는 최신 데이터를 기다리는 동안에도 즉시 UI 응답이 가능.
TanStack Query는 SWR 전략을 사용하여 효율적으로 데이터를 관리.

캐시 데이터는 어디에 보관?

TanStack Query는 캐시 데이터를 전역으로 관리함.
QueryClientProvider를 사용하면 React 애플리케이션 전체에서 캐시에 접근할 수 있음.
이는 내부적으로 React Context API를 사용하기 때문임. Provider 하위의 모든 자식 컴포넌트가 캐시 데이터에 접근할 수 있음.
이 내부 Context를 앞으로 캐시 컨텍스트, 그 안의 데이터를 캐시 데이터로 정의.

Provider 설정

// src/main.jsx
// <Provider>로 감싸준 범위 내부의 자식 컴포넌트 안에서 캐시 데이터를 공유할 수 있음.

import ReactDOM from "react-dom/client";
import App from "./App.jsx";
import "./index.css";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

const queryClient = new QueryClient();

ReactDOM.createRoot(document.getElementById("root")).render(
  <QueryClientProvider client={queryClient}>
    <App />
  </QueryClientProvider>
);

◼ 데이터 흐름

A 컴포넌트

  1. useQuery 실행. 이때 todos라는 queryKey를 기준으로 캐시 컨텍스트에 데이터를 요청.
  2. 초기 상태에서는 todos라는 queryKey에 아무 값도 저장되어 있지 않으므로 const { data } = useQuery~~의 data는 undefined가 됨.
  3. 이후 getTodos(query function)를 호출하고, 실행이 완료되면 외부에서 가져온 todos 데이터를 캐시 컨텍스트에 저장. 이 과정을 ["todos"]에 대한 데이터로 캐싱 처리한다고 표현.
  4. A 컴포넌트에 값을 반영하기 전에 리렌더링이 발생.
    React 컴포넌트는 상태나 props가 변경되면 다시 렌더링됨. useQuery 역시 데이터 패칭 상태가 변경될 때 컴포넌트를 다시 렌더링함.
  5. useQuery가 재실행되며, todos라는 queryKey를 기준으로 캐시 컨텍스트에 데이터를 요청.
  6. 이제 캐싱된 데이터가 존재하므로 값을 반환하고, const { data } = useQuery~~의 data에 값이 담김.

B 컴포넌트

  1. B 컴포넌트에서 useQuery가 실행. 이때 todos라는 queryKey를 기준으로 캐시 컨텍스트에 데이터를 요청.
  2. 이미 캐싱된 데이터가 존재하므로 값을 반환하고, const { data } = useQuery~~의 data에 값이 담김.

C 컴포넌트

  1. C 컴포넌트에서 어떤 액션으로 addTodo라는 API 호출이 발생하며 데이터 갱신을 시도.
  2. 호출이 성공적으로 완료되어도, 이때 useQuery에서 사용하던 캐시 데이터가 자동으로 갱신되지는 않음. 반드시 해당 queryKey를 기준으로 invalidateQueries 처리를 해줘야 기존에 가져왔던 오래된 데이터를 새 것으로 교체할 수 있음.
  3. invalidateQueries를 처리하면 useQuery로 캐시 데이터를 활용하던 모든 곳이 새로운(fresh) 데이터를 구독.

◼ LifeCycle

캐시 데이터 LifeCycle

TanStack Query의 생명주기는 데이터가 캐시되고 사용되며 갱신되는 과정을 포함함.
아래는 주요 상태를 설명함.

상태설명
freshstaleTime 경과 전 상태. 재패칭 불필요. 캐시 그대로 사용 가능.
(데이터를 새로 패칭할 필요가 없는 상태. staleTime이 지나지 않은 상태로, 캐시 데이터를 그대로 사용할 수 있음.)
stalestaleTime 경과 상태. 재패칭 대상.
(데이터를 새로 패칭해야 하는 상태. staleTime이 지난 후로, 새로운 데이터를 가져오기 위해 쿼리가 실행됨.)
active현재 컴포넌트에서 사용 중인 쿼리.
(현재 컴포넌트에서 사용 중인 쿼리 상태. 컴포넌트가 마운트되어 쿼리를 사용하고 있을 때를 말함.)
inactive더 이상 구독되지 않는 쿼리.
(더 이상 사용되지 않는 쿼리 상태. 컴포넌트가 언마운트되거나 쿼리가 더 이상 필요하지 않을 때를 말함.)
deletedgcTime 경과 후 캐시에서 제거된 상태.
(캐시에서 제거된 쿼리 상태. gcTime 이 지나면 쿼리가 캐시에서 삭제되어 이 상태가 됨.)
fetching서버 요청 진행 중 상태. isFetching = true
(데이터를 서버에서 가져오고 있는 상태. 이 상태에서는 isFetching이 true로 설정됨.)

default config(기본 설정)

기본 설정의미
staleTime: 0기본적으로 모든 데이터 stale 취급.
(useQuery 또는 useInfiniteQuery에 등록된 queryFn 을 통해 fetch 받아온 데이터는 항상 stale data 취급.)
refetchOnMount: true마운트 시 stale 데이터 자동 재요청.
(useQuery 또는 useInfiniteQuery 가 있는 컴포넌트가 마운트 시 stale data 를 refetch 자동 실행.)
refetchOnWindowFocus: true브라우저 포커스 시 stale 데이터 재요청.
(실행중인 브라우저 화면을 focus 할 때 마다 stale data를 refetch 자동 실행.)
refetchOnReconnect: true네트워크 재연결 시 stale 데이터 재요청.
(Network 가 끊겼다가 재연결 되었을 때 stale data를 refetch 자동 실행.)

| gcTime(cacheTime): 5분
(1000 60 5 ms) | inactive 상태 5분 후 캐시 삭제.
(useQuery 또는 useInfiniteQuery가 있는 컴포넌트가 언마운트 되었을 때 inactive query라 부르며, inactive 상태가 5분 경과 후 GC(가비지콜렉터)에 의해 cache data 삭제 처리.) |
| retry: 3 | 실패 시 3회 자동 재시도.
(useQuery 또는 useInfiniteQuery에 등록된 queryFn 이 API 서버에 요청을 보내서 실패하더라도 바로 에러를 띄우지 않고 총 3번까지 재요청을 자동으로 시도.) |

헷갈리는 개념 정리

  • staleTime : 데이터를 fresh로 유지할 시간 설정(default: 0)
  • gcTime : inactive 이후 캐시 유지 시간(default: 5분, gcTime 0되면 삭제처리)
  • staleTime과 stale/fresh 관계 : staleTime > 0 → 일정 시간 fresh 유지, staleTime = 0 → 즉시 stale 취급.
  • isPending : 새로운 캐시 데이터를 서버에서 받고 있는 지 여부. 캐시 데이터가 있는 경우 isPending은 false, isFetching은 true.
  • isFetching : 서버에서 데이터를 받고 있는 지 여부.

◼ Must-know options

enabled

enabled 옵션은 쿼리(queryFn) 실행 여부를 제어함. 기본값은 true이며, false로 설정하면 쿼리가 자동으로 실행되지 않음. 이 옵션을 사용하면 특정 이벤트가 발생했을 때만 쿼리를 실행할 수 있음.

useQuery({
	queryKey: ["todos"],
	queryFn: getTodos,
	enabled: true
})

예제

Disabling/Pausing Queries (이벤트 발생 시에만 수동 실행하고 싶을 때)

const { data, refetch } = useQuery({
	queryKey: ["todos"],
	queryFn: getTodos,
	enabled: false
});

return (
	<div>
    <button onClick={() => refetch()}>데이터 불러오기</button>
  </div>
);

Dependent Queries(useQuery 2개 이상이며 실행순서 설정 필요할 때)

// Dependent Query 예제 (순차적 query 실행)
// Get the user
const { data: user } = useQuery({
  queryKey: ['user', email],
  queryFn: getUserByEmail,
})

const userId = user?.id

// Then get the user's projects
const {
  status,
  fetchStatus,
  data: projects,
} = useQuery({
  queryKey: ['projects', userId],
  queryFn: getProjectsByUser,
  // userId 존재할 때만 두 번째 쿼리 실행 가능.
  enabled: !!userId
})
// userId는 Boolean(userId)와 같음.

select

select 옵션은 queryFn이 반환한 데이터를 가공해 사용할 때 활용 가능. UI에서 필요한 데이터만 추출하거나 변환하는 용도로 사용 가능. 단, 캐시에는 항상 원본 데이터 유지. 가공된 데이터는 해당 useQuery를 사용하는 컴포넌트에만 적용됨.

예제

import { useQuery } from 'react-query'

function User() {
  const { data } = useQuery({
	  queryKey: ["user"],
	  queryFn: fetchUser,
	  select: user => user.username
  });
  
  return <div>Username: {data}</div>
}

0개의 댓글