Tanstack Query

TanStack Query 는 서버로부터 데이터 가져오기, 데이터 캐싱, 캐시 제어 등
데이터를 쉽고 효율적으로 관리할 수 있는 라이브러리 입니다.

 

React Query 라는 이름으로 시작했지만, v4 부터 Vue.js 나 Svelte 등의 다른 프레임워크에서도 활용할 수 있도록 기능이 확장되며

TanStack Query 라는 이름으로 변경되었습니다.

 

Tanstack Query

대표적인 기능은 다음과 같습니다.

 

1. 데이터 가져오기 및 캐싱

2. 동일한 요청의 중복 제거

3. 신선한 데이터 유지

4. 무한 스크롤, 페이지네이션 등의 성능 최적화

5. 네트워크 재연결, 요청 실패 등의 자동 갱신

 

Tanstack Query 는 데이터를 패치하는 기능이 있습니다.

쿼리 키와 일치하는 캐시된 데이터가 없을 때, 서버에서 새로운 데이터를 가져옵니다. 그리고 쿼리 키와 일치하는 캐시된 데이터가 있을 때, 서버에서 데이터를 가져오지 않고 캐시된 데이터를 재사용하여 서버의 부담을 줄여 줍니다.

 

TanStack Query 는 캐시한 데이터를 신선(Fresh) 하거나 상한(Stale) 상태로 구분해 관리합니다.

캐시된 데이터가 신선하다면 캐시된 데이터를 사용하고, 만약 데이터가 상했다면 서버에 다시 요청해 신선한 데이터를 가져옵니다. 

일종의 데이터 유통기한 정도로 생각하면 이해하기가 쉽습니다.

이러한 신선한 데이터를 가져오는 동작도 새로고침, refresh 라는 동작으로 가져올 수 있습니다. 

아래에서 천천히 설명하면서 자세히 알아보도록 하겠습니다.

설치

npm i @tanstack/react-query
npm i -D @tanstack/eslint-plugin-query

 

eslint plugin 은 일반적은 Tanstack Query 의 실수를 피하는데 도움을 주는 플러그인 입니다.

 

사용법

Tanstack Query 를 사용하시려면 먼저 설정을 하셔야 합니다. 

최상위 컴포넌트를 <QueryClientProvider> 로 묶어주시면 됩니다.

import { QueryClient, QueryClientProvider } from '@tanstack/react-query' 
import DelayedData from '~/components/DelayedData' 

const queryClient = new QueryClient() 

export default function App() { 
  return ( 
  <QueryClientProvider client={queryClient}> 
    <DelayedData /> 
  </QueryClientProvider> 
  ) 
}

 

useQuery

해당 컴포넌트에서 데이터를 가져올 때 사용하는 가장 기본적인 방법입니다.

const 변수명 = useQuery<데이터타입>(옵션);

 

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

type ResponseValue = {
  message: string;
  time: string;
}

export default function DelayedData() {
  const { data } = useQuery<ResponseValue>({
    queryKey: ['delay'],
    queryFn: async () => await axios.get('http://www.test.com'),
    staleTime: 1000 * 10 // 10초
  })
  
  return (
    <div>{data?.time}</div>
  )
}

 

queryKey

쿼리 키(queryKey) 는 쿼리를 식별하는 고유한 값으로, 배열 형태로 지정합니다.

useQuery({ queryKey: ['hello']});

useQUery({ queryKey: ['hello', 'world', 123, {a: 1, b: 2}]});

 

기본적으로 쿼리 함수(queryFn) 에서 사용하는 변수는 쿼리 키에 포함되어야 합니다.

그러면 변수가 변경될 때마다 자동으로 다시 가져올 수 있습니다.

 

queryFn

쿼리 함수(queryFn) 은 데이터를 가져오는 비동기 함수로, 꼭 데이터를 반환하거나 오류를 던져야 합니다.

던져진 오류는 반환되는 error 객체로 확인할 수 있습니다.

error 는 기본적으로 Null 입니다.

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

type ResponseValue = {
  message: string;
  time: string;
}

export default function DelayedData() {
  const { data, error } = useQuery<ResponseValue>({
    queryKey: ['delay'],
    queryFn: async () => {
      try {
        const response = await axios.get('http://www.test.com');
        if (!response.data) {
          throw new Error('데이터를 불러오는 데 실패했습니다.');
        }
        return response.data;
      } catch (err) {
        throw new Error('API 요청 실패: ' + err);
      }
    },
    staleTime: 1000 * 10 // 10초
  });

  return (
    <div>{data?.time}</div>
    <div>{error?.message}</div>
  );
}

 

옵션

select

선택 함수(select) 를 사용하면 가져온 데이터를 변형할 수 있습니다.

쿼리 함수가 반환하는 데이터를 인수로 받아 선택 함수에서 처리하고 반환하면 최종 데이터가 됩니다.

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

type Users = User[]
interface User {
  id: string
  name: string
  age: number
}

export default function UserNames() {
  const { data } = useQuery<Users, Error, string[]>({
    queryKey: ['users'],
    queryFn: async () => {
      const res = await fetch('https://api.heropy.dev/v0/users')
      const { users } = await res.json()
      return users
    },
    staleTime: 1000 * 10,
    select: data => data.map(user => user.name)
  })
  return (
    <>
      <h2>User Names</h2>
      <ul>{data?.map((name, i) => <li key={i}>{name}</li>)}</ul>
    </>
  )
}

 

밑에 코드처럼 함수를 따로 빼주면 선택 함수를 통한 최종 데이터의 타입을 추론할 수 있습니다.

// ...

async function queryFn(): Promise<Users> {
  const res = await fetch('https://api.heropy.dev/v0/users')
  const { users } = await res.json()
  return users
}
export default function UserNames() {
  // data는 string[] 타입으로 추론
  const { data } = useQuery({
    queryKey: ['users'],
    queryFn,
    staleTime: 1000 * 10,
    select: data => data.map(user => user.name)
  })
  // ...
}

 

staleTime

시간을 설정하면 그 시간동안은 데이터가 신선하다고 설정해 주는 기능입니다.

주로 불필요한 리패치를 방지하고 성능 최적화를 위해 존재합니다.

 

이러한 staleTime 의 주요 목적은 자동 리패치 방지 이유가 큽니다.

기본적으로 useQuery 는 컴포넌트가 다시 마운트되거나, 브라우저 탭이 다시 포커스될 때 "stale" 상태인 데이터를 자동으로 리패치 하려고 합니다. 하지만 staleTIme 이 설정된 경우, 그 시간 동안은 자동 리패치가 발생하지 않습니다.

const { data, isStale } = useQuery<ResponseValue>({
  queryKey: ['delay'],
  queryFn: async () => await axios.get("http://www.test.com"),
  staleTime: 1000 * 10
})

 

refetchInterval

refetchInterval 옵션을 설정하면 설정한 시간마다 자동으로 데이터를 리패치를 시켜 줍니다.

즉 아래의 코드와 같이 20초가 지난다면 페이지를 새로고침하지 않아도 자동으로 데이터를 가져와 반영해 줍니다.

const { data, isStale } = useQuery<ResponseValue>({
  queryKey: ['delay'],
  queryFn: async () => await axios.get("http://www.test.com"),
  refetchInterval: 20000,
  staleTime: 1000 * 10
})

 

isFetching, isPending, isLoading

isFetching

쿼리가 백그라운드에서 데이터를 갱신하고 있을 때 true 입니다.

기존 데이터를 유지하면서 새 데이터를 가져오는 중일 때 주로 사용합니다.

쿼리가 이미 한번 실행되었지만, 다시 데이터를 갱신하기 위해 새 데이터를 가져오고 있을 때 true 가 된다고 생각하시면 됩니다.

예를 들어, 페이지를 새로고침하거나, 특정 이벤트로 인해 데이터를 다시 가져오는 중일 때를 말합니다.

isPending

쿼리가 실행될 준비가 되었지만 아직 시작되지 않은 상태를 나타냅니다.

자동으로 처리되는 경우가 많아 직접 사용할 일이 적습니다.

isLoading

쿼리가 처음으로 실행되어 데이터를 가져오고 있을 때 true 가 되며, 첫 로딩상태를 처리할 때 사용합니다.

예를 들어, 페이지를 처음 로드할 때 데이터를 가져오고 있다면 isLoading 이 true 가 됩니다.

const { data, isFetching, isPending, isLoading } = useQuery<ResponseValue>({...})
import { useQuery } from '@tanstack/react-query'

type ResponseValue = {
  message: string
  time: string
}

export default function DelayedData() {
  const { data, isStale, isFetching, isLoading, refetch } =
    useQuery<ResponseValue>({
      queryKey: ['delay'],
      queryFn: async () => (await fetch('https://api.heropy.dev/v0/delay?t=1000')).json(),
      staleTime: 1000 * 10
    })
  return (
    <>
      {isLoading ? (
        <div>로딩 중..</div>
      ) : (
        <>
          <div>{data?.time}</div>
          <div>데이터가 상했나요?: {JSON.stringify(isStale)}</div>
          <button
            disabled={isFetching}
            onClick={() => refetch()}>
            {isFetching ? '데이터 가져오는 중..' : '데이터 다시 가져오기!'}
          </button>
        </>
      )}
    </>
  )
}

 

getQueryData

만약 새로운 데이터가 아닌 캐시된 데이터가 필요하다면, queryClient.getQueryData() 메서드를 사용할 수 있습니다.

데이터가 상해도 새로 가져오지 않고, 캐시된 데이터만 반환합니다.

useQueryClient 훅을 사용해 queryClient 객체를 가져온 후, getQueryData 메서드를 사용합니다.

import { useCallback } from 'react'
import { useQuery, useQueryClient } from '@tanstack/react-query'

// ...

export default function DelayedData() {
  const { data, isStale } = useQuery<ResponseValue>({
    queryKey: ['delay'],
    queryFn: async () => (await fetch('https://api.heropy.dev/v0/delay?t=1000')).json(),
    staleTime: 1000 * 10
  })
  const queryClient = useQueryClient()
  const queryData = useCallback(() => {
    const data = queryClient.getQueryData(['delay'])
    console.log(data) // 캐시된 데이터
  }, [queryClient])
  return (
    <>
      <div>{data?.time}</div>
      <div>데이터가 상했나요?: {JSON.stringify(isStale)}</div>
      <button onClick={queryData}>데이터 가져오기!</button>
    </>
  )
}

 

fetchQuery

만약 신선도(staleTime)를 기반으로 데이터를 가져오려면, queryClient.fetchQuery() 메서드를 사용할 수 있습니다.

주의할 부분은, queryKey 와 staleTime 을 기존 쿼리와 동일하게 제공해야 합니다.

import { useCallback } from 'react'
import { useQuery, useQueryClient, queryOptions } from '@tanstack/react-query'

// ...

const options = queryOptions<ResponseValue>({
  queryKey: ['delay'],
  queryFn: async () => (await fetch('https://api.heropy.dev/v0/delay?t=1000')).json(),
  staleTime: 1000 * 10
})

export default function DelayedData() {
  const { data, isStale } = useQuery(options)
  const queryClient = useQueryClient()
  const queryData = useCallback(async () => {
    const data = await queryClient.fetchQuery(options)
    console.log(data) // 캐시된 데이터 or 새로 가져온 데이터
  }, [queryClient])
  return (
    <>
      <div>{data?.time}</div>
      <div>데이터가 상했나요?: {JSON.stringify(isStale)}</div>
      <button onClick={queryData}>데이터 가져오기!</button>
    </>
  )
}

 

useMutation

Tanstack Query 는 데이터 변경 작업(생성, 수정, 삭제 등) 을 위한 useMutation 훅을 제공합니다.

이를 통해, 데이터 변경 작업을 처리하고 다양한 성공, 실패, 로딩 등의 상태를 얻을 수 있습니다.

그리고 요청 실패 시의 자동 재시도나 낙관적 업데이트 같은 고급 기능도 쉽게 처리할 수 있습니다.

useQuery 는 '가져오기' 에 집중하는 반면,
useMutation 는 '보내기' 에 집중하는 훅으로 이해하면 쉽습니다.
낙관적인 업데이트
서버 요청의 응답을 기다리지 않고, 먼저 UI 를 업데이트하는 기능을 말합니다.
서버 응답이 느린 상황에서도 빠른 인터페이스를 제공할 수 있어 사용자 경험을 크게 향상시킬 수 있습니다.

 

아래는 기본 사용법 예시 입니다.

import { useMutation } from "react-query";
import axios from 'axios';

const TodoForm = () => {
  const createTodoMutation = useMutation({
    mutationFn: async (newUser: User) => {
      const res = await axios.post('/api/todos', newUSer)
      return res.data;
    },
    onSuccess: () => {
      console.log("Success")
    },
    onError: (error) => {
      console.log("Error")
    },
    onSettled: () => {}
  })
  
  const handleSubmit = (e) => {
    e.preventDefault();
    const newTodo = {name: "chan", completed: false};
    createTodoMutation.mutate(newTodo);
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <button type="submit">Create Todo</button>
    </form>
  )
}

 

아래는 제가 사용했던 코드입니다.

여기서 낙관적인 업데이트를 사용하며, 변이 실패 시 3번 재시도와 0.5초 간격으로 재시도를 하는 예제 입니다.

import React, { useState } from 'react'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import type { Users, User } from './Users'

export default function AddUser() {
  const [name, setName] = useState('')
  const [age, setAge] = useState(0)
  const queryClient = useQueryClient()

  const { mutate, error, isPending, isError } = useMutation({
    mutationFn: async (newUser: User) => { // 
      const res = await fetch('https://api.heropy.dev/v0/users', {
        method: 'POST',
        body: JSON.stringify(newUser)
      })
      if (!res.ok) throw new Error('변이 중 에러 발생!') // 변이 실패!
      return res.json() // 변이 성공!
    },
    onMutate: async newUser => {
      // 낙관적 업데이트 전에 사용자 목록 쿼리를 취소해 잠재적인 충돌 방지!
      await queryClient.cancelQueries({ queryKey: ['users'] })

      // 캐시된 데이터(사용자 목록) 가져오기!
      const previousUsers = queryClient.getQueryData<Users>(['users'])

      // 낙관적 업데이트
      if (previousUsers) {
        queryClient.setQueryData<Users>(['users'], [...previousUsers, newUser])
      }

      // 각 콜백의 context로 전달할 데이터 반환!
      return { previousUsers }
    },
    onSuccess: (data, newUser, context) => {
      console.log('onSuccess', data, newUser, context)
      // 변이 성공 시 캐시 무효화로 사용자 목록 데이터 갱신!
      queryClient.invalidateQueries({ queryKey: ['users'] })
    },
    onError: (error, newUser, context) => {
      console.log('onError', error, newUser, context)
      // 변이 실패 시, 낙관적 업데이트 결과를 이전 사용자 목록으로 되돌리기!
      if (context) {
        queryClient.setQueryData(['users'], context.previousUsers)
      }
    },
    onSettled: (data, error, newUser, context) => {
      console.log('onSettled', data, error, newUser, context)
    },
    retry: 3, // 변이 실패 시 3번 재시도
    retryDelay: 500 // 0.5초 간격으로 재시도
  })

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault()
    mutate({ name, age }) // 변이!
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={name}
        onChange={e => setName(e.target.value)}
        placeholder="사용자 이름"
      />
      <input
        type="number"
        value={age || ''}
        onChange={e => setAge(Number.parseInt(e.target.value, 10))}
        placeholder="사용자 나이"
      />
      <button
        type="submit"
        disabled={isPending}>
        {isPending ? '사용자 추가 중..' : '사용자 추가하기!'}
      </button>
      {isError && <p>에러 발생: {error.message}</p>}
    </form>
  )
}

 

개발자 도구 사용

TanStack Query 전용 개발자 도구를 활용할 수 있습니다.

이를 통해 쿼리의 상태나 데이터, 오류, 캐시 등을 쉽게 확인하고 디버깅할 수 있습니다.

우선 다음과 같이 개발자 도구 라이브러리를 설치합니다.

npm i @tanstack/react-query-devtools

 

QueryClientProvider 범위에서 개발자 도구 컴포넌트를 사용하면 됩니다.

import {
  QueryClient,
  QueryClientProvider,
} from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import DelayedData from './components/DelayedData'

const queryClient = new QueryClient()

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <DelayedData />
      <ReactQueryDevtools />
    </QueryClientProvider>
  )
}

 

여기까지 설정을 하시고 개발 서버를 실행하고 브라우저를 확인하면, 다음 이미지와 같이 화면 우측 하단에 TanStack 로고 모양의 개발자 도구 버튼이 나타납니다. 버튼을 누르고 개발자 도구를 열어 사용하거나 도구 우측 상단의 버튼을 선택해 닫을 수도 있습니다.

 

 

Next.js 에서 사용

Next.js 에서 Tanstack Query 를 사용하려면 기존에 설정했단 방식과는 조금 다르게 설정을 해주셔야 합니다.

설치

npm i @tanstack/react-query-next-experimental

설정

다음과 같이 Provider 를 구성하시면 됩니다.

서버와 클라이언트 모두에서 사용해야 하므로, 'use client' 를 사용해야 합니다.

// app/providers/query.tsx

'use client'
import {
  QueryClient,
  QueryClientProvider,
  isServer,
} from '@tanstack/react-query'
import { ReactQueryStreamedHydration } from '@tanstack/react-query-next-experimental'

function makeQueryClient() {
  return new QueryClient({
    defaultOptions: {
      queries: {
        // 클라이언트의 즉시 다시 요청에 대응하도록, 기본 캐싱 시간(min)을 설정.
        staleTime: 60 * 1000
      }
    }
  })
}

let browserQueryClient: QueryClient | undefined = undefined

function getQueryClient() {
  if (isServer) {
    return makeQueryClient()
  } else {
    if (!browserQueryClient) browserQueryClient = makeQueryClient()
    return browserQueryClient
  }
}

export function QueryProvider({ children }: { children: React.ReactNode }) {
  const queryClient = getQueryClient()
  return (
    <QueryClientProvider client={queryClient}>
      <ReactQueryStreamedHydration>
        {children}
      </ReactQueryStreamedHydration>
    </QueryClientProvider>
  )
}

 

그리고 루트 레이아웃에서 구성한 <QueryProvider> 을 사용합니다.

import { QueryProvider } from '@/providers/query'

export default function RootLayout({
  children
}: Readonly<{
  children: React.ReactNode
}>) {
  return (
    <html lang="ko">
      <body>
        <QueryProvider>{children}</QueryProvider>
      </body>
    </html>
  )
}

 

useSuspenseQuery 를 사용하면, 서버 측 렌더링 단계에서 가져오기를 시도합니다.

우리는 TanStack Query 의 캐싱 전략을 사용할 것이기 때문에, Next.js fetch 함수의 캐싱 기능을 비활성화 해야 합니다.

서버 측 렌더링 단계에서 가져오기를 시도한다는 것은 useSuspenseQuery 가 SSR 단계에서
데이터를 미리 불러와서, 클라이언트가 요청할 때 즉시 해당 데이터를 렌더링할 수 있도록 한다는 뜻입니다
// components/DelayedData.tsx

'use client'
import { useSuspenseQuery } from '@tanstack/react-query'

type ResponseValue = {
  message: string
  time: string
}

export default function DelayedData() {
  const { data } = useSuspenseQuery<ResponseValue>({
    queryKey: ['delay'],
    queryFn: async () => {
      const res = await fetch('https://api.heropy.dev/v0/delay?t=1000', {
        cache: 'no-store'
      })
      return res.json()
    },
    staleTime: 1000 * 10
  })
  return <div>{data.time}</div>
}
// app/page.tsx

import { Suspense } from 'react'
import DelayedData from '@/components/DelayedData'

export default function Page() {
  return (
    <Suspense fallback={<div>loading..</div>}>
      <DelayedData />
    </Suspense>
  )
}

'라이브러리' 카테고리의 다른 글

Lombok  (0) 2025.02.04
Zustand  (0) 2024.09.25
axios Interceptors  (2) 2024.09.01
Jotai  (1) 2024.08.24
React Intersection Observer  (0) 2024.08.20