TanStack Query 핵심 정리(내 cheatSheet)

박요셉·2024년 7월 16일

Simple note

목록 보기
4/18

개요

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

내가 모르는 것과 적고싶은 것들에 대한 정리만 해놓을 것.

대표적인 기능 :

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

데이터 캐싱

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

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

캐시된 데이터 x
캐시된 데이터가 없을 때

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

캐시된 데이터가 있을 때

데이터의 신선도

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

데이터가 상하는 데까지 걸리는 시간은 staleTime 옵션으로 지정할 수 있고 여부는 isStale을 반환받아 확인 가능하다.

핵심 기능

useQuery

옵션

속성설명기본값유형
enabled쿼리 자동 실행 여부. false인 경우, 대기 상태(pending)로 시작.trueboolean | (query: Query) => boolean
gcTime비활성 캐시 데이터(Inactive)가 메모리에 남아 있는 시간(ms).5 60 1000number | Infinity
initialData쿼리가 생성되거나 캐시되기 전에 사용하는 초기 데이터.TData | () => TData
initialDataUpdatedAt초기 데이터의 마지막 업데이트 시간 설정.number | (() => number | undefined)
meta활용할 메타 정보를 저장.Record<string, unknown>
networkMode네트워크 모드 설정.'online''online' | 'always' | 'offlineFirst'
notifyOnChangeProps변경 시 알림 받을 속성 설정.string[] | "all" | (() => string[] | "all")
placeholderData대기(Pending) 중인 상태에서 사용할 데이터.TData | (previousValue: TData | undefined, previousQuery: Query | undefined) => TData
queryClient커스텀 쿼리 클라이언트 연결.QueryClient
queryFn데이터를 가져오는 쿼리 함수로, 꼭 데이터를 반환하거나 오류를 던져야 함.기본 쿼리 함수가 지정되지 않은 경우 필수 옵션!(context: QueryFunctionContext) => Promise
queryKey고유한 쿼리 키(식별자). 필수 옵션!unknown[]
queryKeyHashFn쿼리 키를 해시하는 함수.(queryKey: QueryKey) => string
refetchInterval데이터 자동 갱신(다시 가져오기)의 시간 간격(ms).number | false | ((query: Query) => number | false | undefined)
refetchIntervalInBackground백그라운드에서 데이터 자동 갱신 여부.falseboolean
refetchOnMountuseQuery 연결 시 데이터 갱신 여부. true: 연결 시 데이터가 상한 경우만 갱신. always: 연결 시 데이터 항상 갱신.trueboolean | "always" | ((query: Query) => boolean | "always")
refetchOnReconnect네트워크 재연결 시 데이터 갱신 여부.trueboolean | "always" | ((query: Query) => boolean | "always")
refetchOnWindowFocus브라우저 화면 포커스 시 데이터 갱신 여부.trueboolean | "always" | ((query: Query) => boolean | "always")
retry쿼리 실패 시 재시도 횟수.3boolean | number | (failureCount: number, error: TError) => boolean
retryDelay재시도 시간 간격(ms).number | (retryAttempt: number, error: TError) => number
retryOnMountuseQuery 연결 시 재시도 여부.trueboolean
select가져온 데이터를 변형(선택)하는 함수.(data: TData) => unknown
staleTime데이터가 상하는데 걸리는 시간(ms).0number | ((query: Query) => number)
structuralSharing데이터 구조의 재사용을 최적화해, 불변성을 유지하고 불필요한 리렌더링 방지.trueboolean | (oldData: unknown | undefined, newData: unknown) => unknown
throwOnError쿼리 실패 시 오류를 던질지 여부.undefinedundefined | boolean | (error: TError, query: Query) => boolean

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

다음 예제에서 DelayedData 컴포넌트의 wait Prop의 값이 다르면, 각각 별개의 요청을 전송한다.

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

type ResponseValue = {
message: string
time: string
}

export default function DelayedData({ wait = 1000 }: { wait: number }) {
const { data } = useQuery<ResponseValue>({
  queryKey: ['delay', wait],
  queryFn: async () => (await fetch(`https://api.heropy.dev/v0/delay?t=${wait}`)).json(),
  staleTime: 1000 * 10
})
return <div>{data?.time}</div>
}

기본적으로 쿼리 함수에서 사용하는 변수는 쿼리 키에 포함돼야 한다.
그러면 변수가 변경될 때마다 자동으로 다시 가져올 수 있다.
그런데 만약 변수와는 상관없이 항상 하나의 쿼리로 처리하고 싶다면, ESLint exhaustive-deps 규칙을 비활성화할 수 있다.

export default function DelayedData({ wait = 1000 }: { wait: number }) {
const { data } = useQuery<ResponseValue>({
  // eslint-disable-next-line @tanstack/query/exhaustive-deps
  queryKey: ['delay'], // ESLint Error - The following dependencies are missing in your queryKey: wait
  queryFn: async () => (await fetch(`https://api.heropy.dev/v0/delay?t=${wait}`)).json(),
  staleTime: 1000 * 10
})
return <div>{data?.time}</div>
}

queryFn

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

select

선택 함수(select)를 사용하면 가져온 데이터를 변형(선택)할 수 있습니다.
쿼리 함수가 반환하는 데이터를 인수로 받아 선택 함수에서 처리하고 반환하면 최종 데이터가 됩니다.
최종 데이터의 타입은 useQuery의 3번째 제네릭 타입으로 선언할 수 있습니다.

다음 예제의 사용한 API는 사용자 정보 API입니다.

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

쿼리 함수를 따로 선언해 제공하면, 선택 함수를 통한 최종 데이터의 타입을 추론할 수 있습니다.
다음 예제에서 쿼리 함수의 반환은 Users 타입이고, 최종 데이터(선택 함수의 반환)은 string[] 타입으로 추론됩니다.

// ...

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

응답

반환 속성설명타입
data성공적으로 가져온 데이터.TData
dataUpdatedAt최근에 데이터를 성공적으로 가져온 시간(유닉스 타임스탬프).number
error오류가 발생했을 때의 오류 객체. 오류가 발생하지 않았다면 null.null | TError
errorUpdateCount모든 오류의 횟수.number
errorUpdatedAt최근에 오류가 발생한 시간(유닉스 타임스탬프).number
failureCount쿼리의 실패 횟수. 쿼리가 실패할 때마다 증가하고 쿼리가 성공하면 0으로 재설정.number
failureReason쿼리의 재시도 실패 이유. 쿼리가 성공하면 null로 재설정.null | TError
fetchStatus'fetching': 쿼리 함수가 실행 중.(첫 대기 및 백그라운드 다시 가져오기 포함, isFetching)'fetching' | 'paused' | 'idle'
'paused': 쿼리 함수의 가져오기가 일시 중단됨.(isPaused)
'idle': 쿼리 함수가 동작 중이지 않음.
isError쿼리 함수에서의 오류 발생 여부.boolean
isFetched쿼리의 첫 데이터 가져오기가 완료되었는지 여부.boolean
isFetchedAfterMount컴포넌트 연결 후 가져오기가 완료되었는지 여부. 컴포넌트 연결 전에 캐시된 데이터를 표시하지 않는 용도로 사용.boolean
isFetching쿼리 함수가 실행 중.(첫 대기 및 백그라운드 다시 가져오기 포함)boolean
isLoading쿼리 함수의 첫 번째 가져오기가 진행 중. isFetching && isPending와 같음.boolean
isLoadingError쿼리 함수의 첫 번째 가져오기 중 실패 여부.boolean
isPaused쿼리 가져오기가 일시 중단됨.boolean
isPending캐시된 데이터가 없고 쿼리가 아직 완료되지 않은 상태.boolean
isPlaceholderData표시된 데이터가 대체 데이터인지 여부.boolean
isRefetchError쿼리가 다시 가져오기를 시도하는 중에 실패했는지 여부.boolean
isRefetching백그라운드에서 다시 가져오기가 진행 중인지의 여부. isFetching && !isPending와 같음.boolean
isStale캐시된 데이터가 무효화(Invalidated)되거나 staleTime이 경과된 여부.boolean
isSuccess쿼리 데이터를 성공적으로 가져왔는지 여부.boolean
refetch데이터를 새롭게 다시 가져오는 함수. throwOnError: true 옵션을 사용해야 오류가 발생.(options: { throwOnError: boolean, cancelRefetch: boolean }) => Promise
status'pending': 캐시된 데이터가 없고 아직 완료되지 않은 상태.(isPending)'pending' | 'error' | 'success'
'error': 오류가 발생한 상태.(isError)
'success': 데이터를 성공적으로 가져온 상태.(isSuccess)

다시 가져오기

refetch 함수를 사용하면, 데이터를 새롭게 다시 가져올 수 있습니다.

// ...

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

만약 새로운 데이터가 아닌 캐시된 데이터가 필요하다면, 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>
  </>
)
}

만약 신선도(staleTime) 기반으로 데이터를 가져오려면, queryClient.fetchQuery() 메소드를 사용할 수 있습니다.
주의할 부분은, queryKey와 staleTime를 기존 쿼리와 동일하게 제공해야 합니다.(queryFn 생략 가능)

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

useInfiniteQuery

'더 보기' 버튼으로 추가 데이터를 더 가져오거나 더 나아가 무한 스크롤 기능은 쉽게 찾아볼 수 있는 일반적인 UI입니다.
이를 위해 useInfiniteQuery훅을 제공한다.

const 반환 = useInfiniteQuery<페이지타입>(옵션)

옵션

속성설명기본값유형
getNextPageParam새로운 다음 페이지를 가져오면, 다음 페이지의 정보로 호출되는 함수. 필수 옵션! 다음 페이지 번호를 반환해야 함! 다음 페이지가 없으면, undefined 또는 null을 반환해야 함!(lastPage: TPage, allPages: TPage[], lastPageParam: number, allPageParams: number[]) => TPageParam | undefined | null
getPreviousPageParam새로운 이전 페이지를 가져오면, 이전 페이지의 정보로 호출되는 함수. 이전 페이지 번호를 반환해야 함! 이전 페이지가 없으면, undefined 또는 null을 반환해야 함!(firstPage: TPage, allPages: TPage[], firstPageParam: number, allPageParams: number[]) => TPageParam | undefined | null
initialPageParam첫 번째 페이지의 번호. 필수 옵션!TPageParam
maxPages저장 및 출력할 최대 페이지의 수. 페이지가 지나치게 많은 경우에 유용.Infinitenumber

반환

그리고 역시 useQuery의 모든 반환 속성을 사용할 수 있으며, 추가로 다음의 반환 속성들을 사용할 수 있다.

반환 속성설명타입
fetchNextPage다음 페이지를 가져오는 함수.(options?: FetchNextPageOptions) => Promise
fetchPreviousPage이전 페이지를 가져오는 함수.(options?: FetchPreviousPageOptions) => Promise
hasNextPage다음 페이지가 있는지 여부.boolean
hasPreviousPage이전 페이지가 있는지 여부.boolean
isFetchingNextPage다음 페이지를 가져오는 중인지의 여부.boolean
isFetchingPreviousPage이전 페이지를 가져오는 중인지의 여부.boolean

예제

다음은 사용자가 입력한 영화 제목으로 영화 목록을 가져와 출력하고, '더 보기' 버튼을 선택해 추가 영화 목록을 가져올 수 있는 useInfiniteQuery 휵의 이해를 돕는 간단한 예제입니다.
쿼리 함수에서 반환하는 데이터는 data.pages 배열에 저장되는 것에 주의하세요.

import React, { useState, useEffect, useCallback } from 'react'
import { useInfiniteQuery } from '@tanstack/react-query'

export interface Page {
Search: Movie[] // 검색된 영화 목록
totalResults: string // 검색된 모든 결과의 수
Response: string // 'True' or 'False'
}
export interface Movie {
Title: string
Year: string
imdbID: string
Type: string
Poster: string
}

export default function MovieList() {
const [searchText, setSearchText] = useState('')
const [queryText, setQueryText] = useState('')

const {
  data, // 가져온 데이터
  isLoading, // 첫 페이지 가져오는 중
  isFetching, // 다음 페이지 가져오는 중
  isFetched, // 첫 페이지 가져오기 완료
  hasNextPage, // 다음 페이지가 있는지 여부
  fetchPreviousPage, // 이전 페이지 가져오기 함수
  fetchNextPage // 다음 페이지 가져오기 함수
} = useInfiniteQuery<Page>({
  queryKey: ['movies', queryText], // 검색어로 쿼리 키 생성!
  queryFn: async ({ pageParam }) => {
    const res = await fetch(`https://omdbapi.com/?apikey=7035c60c&s=${queryText}&page=${pageParam}`)
    return res.json()
  },
  initialPageParam: 1, // 첫 페이지 번호 초기화!
  getNextPageParam: (lastPage, pages) => {
    // 한 페이지당 최대 10개까지의 영화 정보를 가져옴!
    // 마지막 페이지 번호 계산!
    const maxPage = Math.ceil(Number.parseInt(pages[0].totalResults, 10) / 10)

    // 다음 페이지가 있으면, 다음 페이지 번호 반환!
    if (lastPage.Response === 'True' && pages.length < maxPage) {
      return pages.length + 1
    }
    // 다음 페이지가 없으면 undefined | null 반환!
    return undefined
  },
  enabled: false, // 검색어 입력 전까지 대기!
  staleTime: 1000 * 60 * 5 // 5분
})

useEffect(() => {
  // 검색어가 변경될 때마다, 캐시된 데이터가 있어서 그 데이터의 다음 페이지를 가져오지 않도록 이미 캐시된 이전 페이지를 가져옴!
  if (queryText) fetchPreviousPage()
}, [queryText, fetchPreviousPage])

const handleSubmit = useCallback(
  (e: React.FormEvent) => {
    e.preventDefault()
    // 검색!
    if (searchText.trim()) {
      setQueryText(searchText)
    }
    // 초기화!
    if (!searchText.trim()) {
      setSearchText('')
      setQueryText('')
    }
  },
  [searchText]
)

return (
  <>
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={searchText}
        placeholder="영화 제목을 입력하세요."
        onChange={e => setSearchText(e.target.value)}
      />
    </form>
    <div>
      {data?.pages.map((page, index) => (
        // 각 페이지의 출력 최적화를 위해, 페이지 단위 key 속성을 추가!
        <React.Fragment key={index}>
          {page.Search &&
            page.Search.map(movie => (
              <div key={movie.imdbID}>{movie.Title}</div>
            ))}
        </React.Fragment>
      ))}
    </div>
    {isLoading ? <div>로딩 중..</div> : null}
    {isFetched && hasNextPage && (
      <button
        disabled={isFetching}
        onClick={() => fetchNextPage()}>
        {isFetching ? '로딩 중..' : '더 보기!'}
      </button>
    )}
  </>
)
}

무한스크롤로 구현은 아래

import React, { useState, useEffect, useCallback, useRef } from 'react'
import { useInfiniteQuery } from '@tanstack/react-query'

// ...

export default function MovieList() {
const [searchText, setSearchText] = useState('')
const [queryText, setQueryText] = useState('')
const observerEl = useRef<HTMLDivElement | null>(null)

const {
  data,
  // isLoading,
  isFetching,
  // isFetched,
  hasNextPage,
  fetchPreviousPage,
  fetchNextPage
} = useInfiniteQuery<Page>({
  // ...
})

useEffect(() => {
  const currentObserverEl = observerEl.current
  const io = new IntersectionObserver(entries => {
    if (entries[0].isIntersecting && hasNextPage) {
      fetchNextPage()
    }
  })
  if (currentObserverEl) {
    io.observe(currentObserverEl)
  }
  return () => {
    if (currentObserverEl) {
      io.disconnect()
    }
  }
}, [hasNextPage, fetchNextPage])

// ...

return (
  <>
    {/* ... */}
    {isFetching ? <div>로딩 중..</div> : null}
    <div
      ref={observerEl}
      style={{ height: '20px' }}
    />
  </>
)
}

좀 더 간결한 코드를 원한다면, react-intersection-observer 라이브러리를 사용할 수 있습니다.
ref 속성을 관찰 요소와 연결하고, inView 속성을 사용해 요소가 화면에 보이는지 여부를 확인할 수 있습니다.

라이브러리를 설치하고 아래 예제와 같이 수정해 보세요.

npm i react-intersection-observer
import React, { useState, useEffec, useCallback } from 'react'
import { useInfiniteQuery } from '@tanstack/react-query'
import { useInView } from 'react-intersection-observer'

// ...

export default function MovieList() {
const [searchText, setSearchText] = useState('')
const [queryText, setQueryText] = useState('')
const { ref, inView } = useInView()

// ...

// useEffect(() => {
//   const currentObserverEl = observerEl.current
//   const io = new IntersectionObserver(entries => {
//     if (entries[0].isIntersecting && hasNextPage) {
//       fetchNextPage()
//     }
//   })
//   if (currentObserverEl) {
//     io.observe(currentObserverEl)
//   }
//   return () => {
//     if (currentObserverEl) {
//       io.disconnect()
//     }
//   }
// }, [hasNextPage, fetchNextPage])

useEffect(() => {
  if (inView && hasNextPage) {
    fetchNextPage()
  }
}, [inView, hasNextPage, fetchNextPage])

// ...

return (
  <>
    {/* ... */}
    {isFetching ? <div>로딩 중..</div> : null}
    <div
      ref={ref}
      style={{ height: '20px' }}
    />
  </>
)
}

useMutation

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

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

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

낙관적 업데이트(Optimistic Update)는 서버 요청의 응답을 기다리지 않고, 먼저 UI를 업데이트하는 기능을 말합니다.
서버 응답이 느린 상황에서도 빠른 인터페이스를 제공할 수 있어 사용자 경험을 크게 향상시킬 수 있습니다.

옵션

옵션설명기본값타입
gcTime비활성 캐시 데이터(Inactive)가 메모리에 남아 있는 시간(ms).number | Infinity
meta활용할 메타 정보를 저장.Record<string, unknown>
mutationFn실행할 비동기 변이 함수. 필수 옵션!(variables: TVariables) => Promise
mutationKeyqueryClient.setMutationDefaults의 기본값 상속을 위한 키unknown[]
networkMode네트워크 모드 설정.'online''online' | 'always' | 'offlineFirst'
onError변이 중 오류가 발생할 때 호출되는 함수.(err: TError, variables: TVariables, context?: TContext) => Promise | unknown
onMutate변이 함수가 실행되기 전에 호출되는 함수.(variables: TVariables) => Promise<TContext | void> | TContext | void
onSettled변이가 성공하거나 실패해도 항상 호출되는 함수.(data: TData, error: TError, variables: TVariables, context?: TContext) => Promise | unknown
onSuccess변이가 성공할 때 호출되는 함수.(data: TData, variables: TVariables, context: TContext) => Promise | unknown
queryClient커스텀 쿼리 클라이언트 연결.QueryClient
retry변이 실패 시 재시도 횟수.0boolean | number | (failureCount: number, error: TError) => boolean
retryDelay재시도 시간 간격(ms).number | (retryAttempt: number, error: TError) => number
scope동시 실행 범위 설정. 같은 범위 ID를 가진 변이는 병렬이 아닌 직렬로 실행.{ id: string }
throwOnError변이 실패 시 오류를 던질지 여부.undefinedundefined | boolean | (error: TError) => boolean

반환

반환 속성설명타입
data성공적으로 가져온 데이터.undefined | unknown
error오류가 발생했을 때의 오류 객체. 오류가 발생하지 않았다면 null.null | TError
failureCount변이의 실패 횟수. 변이가 실패할 때마다 증가하고 변이가 성공하면 0으로 재설정.number
failureReason변이의 재시도 실패 이유. 쿼리가 성공하면 null로 재설정.null | TError
isError변이 함수에서의 오류 발생 여부.boolean
isIdle변이 함수가 실행되기 전의 초기 상태인지 여부boolean
isPaused변이 함수가 일시 중단되었는지 여부boolean
isPending변이 함수가 실행 중인지 여부boolean
isSuccess데이터를 성공적으로 가져왔는지 여부.boolean
mutate변이 실행 함수(variables: TVariables, { onSuccess, onSettled, onError }) => void
mutateAsync비동기 변이 실행 함수(variables: TVariables, { onSuccess, onSettled, onError }) => Promise
reset변이 내부 상태를 초기 상태로 재설정하는 함수() => void
status변이의 현재 상태. idle: 초기 상태, pending: 실행 중, error: 오류 발생, success: 성공string
submittedAt변이가 제출된 시간(유닉스 타임스탬프).number
variables변이 실행 함수(mutate)에 전달된 데이터.undefined | TVariables
변이는 조회(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
개발자 지망생

0개의 댓글