TanStack Query(React Query)

Eunwoo·2025년 1월 5일
0

React

목록 보기
17/18

TanStack Query?

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

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

대표적인 기능

  • 데이터 가져오기 및 캐싱
  • 동일 요청의 중복 제거
  • 신선한 데이터 유지
  • 무한 스크롤, 페이지네이션 등의 성능 최적화
  • 네트워크 재연결, 요청 실패 등의 자동 갱신

데이터 캐싱

TanStack Query를 활용해서 데이터를 가져올 때는 항상 쿼리 키(queryKey)를 지정하게 된다.
이 쿼리 키는 캐시된 데이터와 비교해 새로운 데이터를 가져올지, 캐시된 데이터를 사용할지 결정하는 기준이 된다.

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

export default function DelayedData() {
  const { data } = useQuery({
    queryKey: ['delay'],
    queryFn: async () => (await fetch('https://api.heropy.dev/v0/delay?t=1000')).json()
  })
  return <div>{JSON.stringify(data)}</div>
}

다음 이미지는 쿼리 키와 일치하는 캐시된 데이터가 없을 때, 서버에서 새로운 데이터를 가져오는 과정을 보여준다.
서버에서 데이터를 가져오면 그 데이터는 캐시되고 그 이후 요청부터는 캐시된 데이터를 사용할 수 있다.

(캐쉬된 data가 없을 때 - Miss)

반대로 쿼리 키와 일치하는 캐시된 데이터가 있으면, 서버에 요청하지 않고 캐시된 데이터를 사용하게 된다..
따라서 같은 데이터를 가져오는 요청이 여러 번 발생해도, 캐시된 데이터를 사용하게 되어 중복 요청을 줄일 수 있다.
그렇다면 한번 캐시된 데이터가 있으면, 서버로는 더 이상 요청을 보낼 수 없는 걸까?

(캐시된 data가 있을 때 - Hit)


데이터의 신선도

TanStack Query는 캐시한 데이터를 신선(Fresh)하거나 상한(Stale) 상태로 구분해 관리한다.
캐시된 데이터가 신선하다면 캐시된 데이터를 사용하고, 만약 데이터가 상했다면 서버에 다시 요청해 신선한(새로운) 데이터를 가져온다.
일종의 데이터 유통기한 정도로 생각하면 이해하기 쉽다.

데이터가 상하는 데까지 걸리는 시간은 staleTime 옵션으로 지정할 수 있다.
그리고 신선한지 상했는지 여부는 isStale로 확인할 수 있다.

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

export default function DelayedData() {
  const { data, isStale } = useQuery({
    queryKey: ['delay'],
    queryFn: async () => (await fetch('https://api.heropy.dev/v0/delay?t=1000')).json(),
    staleTime: 1000 * 10 // 10초 후 상함. 즉, 10초 동안 신선함.
  })
  return (
    <>
      <div>데이터가 {isStale ? '상했어요..' : '신선해요!'}</div>
      <div>{JSON.stringify(data)}</div>
    </>
  )
}

useQuery

  • 가장 기본적인 쿼리 훅으로, 컴포넌트에서 데이터를 가져올 때 사용한다.

ex)

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 fetch('https://api.heropy.dev/v0/delay?t=1000')).json(),
    staleTime: 1000 * 10 // 10초
  })
  return <div>{data?.time}</div>
}

queryKey

쿼리 키(queryKey)는 쿼리를 식별하는 고유한 값으로, 배열 형태로 지정한다.
다중 아이템 쿼리 키를 사용할 때는, 아이템의 순서가 중요하다.

// 단일 아이템 쿼리 키
useQuery({ queryKey: ['hello'] })

// 다중 아이템 쿼리 키
useQuery({ queryKey: ['hello', 'world', 123, { a: 1, b: 2 }] })

// 서로 같은 쿼리
useQuery({ queryKey: ['hello', 'world', 123, { a: 1, b: 2 }] })
useQuery({ queryKey: ['hello', 'world', 123, { b: 2, c: undefined, a: 1 }] })

// 서로 다른 쿼리
useQuery({ queryKey: ['hello', 'world', 123, { a: 1, b: 2 }] })
useQuery({ queryKey: ['hello', 'world', 123, { a: 1, b: 2, c: 3 }] })
useQuery({ queryKey: ['hello', 'world'] })
useQuery({ queryKey: [123, 'world', { a: 1, b: 2, c: 3 }, 'hello'] })

queryFn

쿼리 함수(queryFn)는 데이터를 가져오는 비동기 함수로, 꼭 데이터를 반환하거나 오류를 던져야 한다.
던져진 오류는 반환되는 error 객체로 확인할 수 있다. error는 기본적으로 null이다.

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

type ResponseValue = {
  message: string
  time: string
}

export default function DelayedData() {
  const { data, error } = useQuery<ResponseValue>({
    queryKey: ['delay'],
    queryFn: async () => {
      const res = await fetch('https://api.heropy.dev/v0/delay?t=1000')
      const data = await res.json()
      if (!data.time) {
        throw new Error('문제가 발생했습니다!')
      }
      return data
    },
    staleTime: 1000 * 10,
    retry: 1
  })
  return (
    <>
      {data && <div>{JSON.stringify(data)}</div>}
      {error && <div>{error.message}</div>}
    </>
  )
}

상태 확인

isFetching은 쿼리 함수(queryFn)가 실행 중인지의 여부로, 데이터를 가져오는 중을 나타낸다.

isPending은 캐시된 데이터가 없고 쿼리가 아직 완료되지 않은 상태의 여부로, initialData 혹은 placeholderData 옵션으로 데이터를 제공하면 출력 대기(Pending)가 필요하지 않으므로 false를 반환한다.
enabled 옵션을 false로 지정하면, 쿼리가 대기 상태로 시작하므로 isPending는 true를 반환한다.

isLoading은 isFetching && isPending와 같은 의미로, 쿼리의 첫 번째 가져오기가 진행 중인 경우를 나타낸다.

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>
        </>
      )}
    </>
  )
}

useMutation

TanStack Query는 데이터 변경 작업(생성, 수정, 삭제 등)을 위한 useMutation 훅을 제공한다.
이를 통해, 데이터 변경 작업을 처리하고 다양한 성공, 실패, 로딩 등의 상태를 얻을 수 있다.
그리고 요청 실패 시의 자동 재시도나 낙관적 업데이트 같은 고급 기능도 쉽게 처리할 수 있다.

쿼리(useQuery)는 '가져오기'에 집중하는 반면, 변이(useMutation)는 '보내기'에 집중하는 훅으로 이해하면 쉽다.

ex)

우선 다음과 같이, 사용자 목록을 가져오는 쿼리(['users'])가 있다.

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

export type Users = User[]
export interface User {
  name: string
  age: number
  isValid?: boolean
  emails?: string[]
  photo?: {
    name: string
    data: string // Base64
  }
  id?: string
}

export default function Users() {
  const { data } = useQuery<Users>({
    queryKey: ['users'],
    queryFn: async () => {
      const res = await fetch('https://api.heropy.dev/v0/users') // GET
      const json = await res.json()
      return json.users
    },
    staleTime: 1000 * 60 * 5 // 5분
  })

  // ...
}

변이는 조회(Read)를 제외한 데이터 생성(Create)이나 수정(Update), 삭제(Delete) 작업을 수행한다.
다음 예제는 입력한 사용자의 이름과 나이로 새로운 사용자를 추가하는 예제이다.

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>
  )
}
profile
KyungPook National University - Computer Science, 꾸준히 성장하는 개발자

0개의 댓글