Query
TanStack Query는 비동기 상태(주로 외부 데이터)를 관리하기 위한 함수들을 제공합니다.
개요 문서에서 설명하듯이:
React Query는 종종 React에 필요한 데이터 페칭 라이브러리로 설명되지만, 더 기술적으로 말하면 React 애플리케이션에서 데이터 페칭, 캐싱, 동기화, 서버 상태 업데이트를 쉽게 만들어 줍니다.
jotai-tanstack-query는 TanStack Query를 위한 Jotai 확장 라이브러리입니다. 이 라이브러리는 TanStack Query의 모든 기능을 제공하며, 기존 Jotai 상태와 함께 이러한 기능을 사용할 수 있는 훌륭한 인터페이스를 제공합니다.
지원
jotai-tanstack-query는 현재 TanStack Query v5를 지원합니다.
설치
jotai
외에도 jotai-tanstack-query
와 @tanstack/query-core
를 설치해야 이 확장 기능을 사용할 수 있습니다.
npm install jotai-tanstack-query @tanstack/query-core
점진적 도입
jotai-tanstack-query
를 여러분의 앱에 점진적으로 도입할 수 있습니다. 이는 전부 아니면 전무(all or nothing) 방식이 아닙니다. 단지 동일한 QueryClient 인스턴스를 사용하고 있는지 확인하면 됩니다. QueryClient 설정을 참고하세요.
// 기존 useQueryHookconst { data, isPending, isError } = useQuery({queryKey: ['todos'],queryFn: fetchTodoList,})// jotai-tanstack-queryconst todosAtom = atomWithQuery(() => ({queryKey: ['todos'],}))const [{ data, isPending, isError }] = useAtom(todosAtom)
내보낸 함수들
atomWithQuery
는 useQuery를 위한 함수입니다.atomWithInfiniteQuery
는 useInfiniteQuery를 위한 함수입니다.atomWithMutation
는 useMutation를 위한 함수입니다.atomWithSuspenseQuery
는 useSuspenseQuery를 위한 함수입니다.atomWithSuspenseInfiniteQuery
는 useSuspenseInfiniteQuery를 위한 함수입니다.atomWithMutationState
는 useMutationState를 위한 함수입니다.
모든 함수는 동일한 시그니처를 따릅니다.
const dataAtom = atomWithSomething(getOptions, getQueryClient)
첫 번째 getOptions
매개변수는 옵저버에 입력값을 반환하는 함수입니다.
두 번째 선택적 getQueryClient
매개변수는 QueryClient를 반환하는 함수입니다.
atomWithQuery 사용법
atomWithQuery
는 TanStack Query의 표준 Query
를 구현하는 새로운 아톰을 생성합니다.
import { atom, useAtom } from 'jotai'import { atomWithQuery } from 'jotai-tanstack-query'const idAtom = atom(1)const userAtom = atomWithQuery((get) => ({queryKey: ['users', get(idAtom)],queryFn: async ({ queryKey: [, id] }) => {const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`)return res.json()},}))const UserData = () => {const [{ data, isPending, isError }] = useAtom(userAtom)if (isPending) return <div>Loading...</div>if (isError) return <div>Error</div>return <div>{JSON.stringify(data)}</div>}
atomWithInfiniteQuery 사용법
atomWithInfiniteQuery
는 atomWithQuery
와 매우 유사하지만, 페이지네이션을 위한 InfiniteQuery
를 사용합니다. Infinite Queries에 대해 더 알아보기.
기존 데이터에 추가적으로 데이터를 "더 불러오기" 또는 "무한 스크롤"을 구현하는 리스트 렌더링은 매우 일반적인 UI 패턴입니다. React Query는 이러한 유형의 리스트를 쿼리하기 위해
useInfiniteQuery
라는 유용한 버전의useQuery
를 지원합니다.
일반적인 쿼리 아톰과의 주요 차이점은 추가 옵션인 getNextPageParam
과 getPreviousPageParam
입니다. 이 옵션들은 추가 페이지를 어떻게 가져올지 쿼리에 지시하는 데 사용됩니다.
import { atom, useAtom } from 'jotai'import { atomWithInfiniteQuery } from 'jotai-tanstack-query'const postsAtom = atomWithInfiniteQuery(() => ({queryKey: ['posts'],queryFn: async ({ pageParam }) => {const res = await fetch(`https://jsonplaceholder.typicode.com/posts?_page=${pageParam}`)return res.json()},getNextPageParam: (lastPage, allPages, lastPageParam) => lastPageParam + 1,initialPageParam: 1,}))const Posts = () => {const [{ data, fetchNextPage, isPending, isError, isFetching }] =useAtom(postsAtom)if (isPending) return <div>Loading...</div>if (isError) return <div>Error</div>return (<>{data.pages.map((page, index) => (<div key={index}>{page.map((post: any) => (<div key={post.id}>{post.title}</div>))}</div>))}<button onClick={() => fetchNextPage()}>Next</button></>)}
atomWithMutation 사용법
atomWithMutation
은 TanStack Query의 표준 Mutation
을 구현하는 새로운 아톰을 생성합니다.
쿼리와 달리, 뮤테이션은 일반적으로 데이터를 생성/수정/삭제하거나 서버 사이드 이펙트를 수행하는 데 사용됩니다.
const postAtom = atomWithMutation(() => ({mutationKey: ['posts'],mutationFn: async ({ title }: { title: string }) => {const res = await fetch(`https://jsonplaceholder.typicode.com/posts`, {method: 'POST',body: JSON.stringify({title,body: 'body',userId: 1,}),headers: {'Content-type': 'application/json; charset=UTF-8',},})const data = await res.json()return data},}))const Posts = () => {const [{ mutate, status }] = useAtom(postAtom)return (<div><button onClick={() => mutate({ title: 'foo' })}>Click me</button><pre>{JSON.stringify(status, null, 2)}</pre></div>)}
atomWithMutationState 사용법
atomWithMutationState
는 MutationCache
에 있는 모든 뮤테이션에 접근할 수 있는 새로운 아톰을 생성합니다.
const mutationStateAtom = atomWithMutationState((get) => ({filters: {mutationKey: ['posts'],},}))
Suspense
jotai-tanstack-query는 React의 Suspense와 함께 사용할 수도 있습니다.
atomWithSuspenseQuery 사용법
import { atom, useAtom } from 'jotai'import { atomWithSuspenseQuery } from 'jotai-tanstack-query'const idAtom = atom(1)const userAtom = atomWithSuspenseQuery((get) => ({queryKey: ['users', get(idAtom)],queryFn: async ({ queryKey: [, id] }) => {const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`)return res.json()},}))const UserData = () => {const [{ data }] = useAtom(userAtom)return <div>{JSON.stringify(data)}</div>}
atomWithSuspenseInfiniteQuery 사용법
import { atom, useAtom } from 'jotai'import { atomWithSuspenseInfiniteQuery } from 'jotai-tanstack-query'const postsAtom = atomWithSuspenseInfiniteQuery(() => ({queryKey: ['posts'],queryFn: async ({ pageParam }) => {const res = await fetch(`https://jsonplaceholder.typicode.com/posts?_page=${pageParam}`)return res.json()},getNextPageParam: (lastPage, allPages, lastPageParam) => lastPageParam + 1,initialPageParam: 1,}))const Posts = () => {const [{ data, fetchNextPage, isPending, isError, isFetching }] =useAtom(postsAtom)return (<>{data.pages.map((page, index) => (<div key={index}>{page.map((post: any) => (<div key={post.id}>{post.title}</div>))}</div>))}<button onClick={() => fetchNextPage()}>Next</button></>)}
프로젝트에서 동일한 Query Client 인스턴스 참조하기
프로젝트에서 useQueryClient()
훅을 사용하여 QueryClient
객체를 가져오고 해당 메서드를 호출하는 커스텀 훅이 있을 수 있습니다.
동일한 QueryClient
객체를 참조하려면 프로젝트의 루트를 <Provider>
로 감싸고, QueryClientProvider
에 제공한 것과 동일한 queryClient
값으로 queryClientAtom
을 초기화해야 합니다.
이 단계를 생략하면 useQueryAtom
은 useQueryClient()
훅을 사용하여 queryClient
를 가져오는 다른 훅들과 별개의 QueryClient
를 참조하게 됩니다.
또는 getQueryClient
매개변수를 사용하여 queryClient
를 지정할 수도 있습니다.
예제
아래 예제에서는 useTodoMutation
뮤 테이션 훅과 todosAtom
쿼리를 사용합니다.
루트 <App>
노드에서 초기화 단계를 포함했습니다.
동일한 쿼리 키('todos'
)를 참조하지만, Provider
초기화 단계가 완료되지 않았다면 useTodoMutation
의 onSuccess
무효화가 트리거되지 않습니다.
이로 인해 todosAtom
은 리페치를 요청받지 않아 오래된 데이터를 표시하게 됩니다.
⚠️ 참고: Typescript를 사용할 때는 queryClient
값을 useHydrateAtoms
에 전달할 때 Map을 사용하는 것이 좋습니다. Initializing State on Render 문서에서 작동 예제를 확인할 수 있습니다.
import { Provider } from 'jotai/react'import { useHydrateAtoms } from 'jotai/react/utils'import {useMutation,useQueryClient,QueryClient,QueryClientProvider,} from '@tanstack/react-query'import { atomWithQuery, queryClientAtom } from 'jotai-tanstack-query'const queryClient = new QueryClient()const HydrateAtoms = ({ children }) => {useHydrateAtoms([[queryClientAtom, queryClient]])return children}export const App = () => {return (<QueryClientProvider client={queryClient}><Provider>{/*이 Provider 초기화 단계는 atomWithQuery와 앱의 다른 부분에서 동일한 queryClient를 참조하기 위해 필요합니다.이 단계가 없으면 useQueryClient() 훅이 다른 QueryClient 객체를 반환합니다.*/}<HydrateAtoms><App /></HydrateAtoms></Provider></QueryClientProvider>)}export const todosAtom = atomWithQuery((get) => {return {queryKey: ['todos'],queryFn: () => fetch('/todos'),}})export const useTodoMutation = () => {const queryClient = useQueryClient()return useMutation(async (body: todo) => {await fetch('/todo', { Method: 'POST', Body: body })},{onSuccess: () => {void queryClient.invalidateQueries(['todos'])},onError,})}
SSR 지원
모든 아톰은 Next.js 앱이나 Gatsby 앱과 같은 서버 사이드 렌더링(SSR) 앱의 컨텍스트 내에서 사용할 수 있습니다. React Query가 SSR 앱 내에서 사용하기 위해 지원하는 두 가지 옵션, hydration 또는 initialData
를 모두 사용할 수 있습니다.
에러 처리
Fetch 에러가 발생하면 ErrorBoundary로 잡을 수 있습니다. 일시적인 오류는 재요청을 통해 복구할 수 있습니다.
자세한 내용은 실행 가능한 예제를 참고하세요.
Devtools
Devtools를 사용하려면 추가로 설치해야 합니다.
npm install @tanstack/react-query-devtools
<QueryClientProvider />
안에 <ReactQueryDevtools />
를 넣기만 하면 됩니다.
import {QueryClientProvider,QueryClient,QueryCache,} from '@tanstack/react-query'import { ReactQueryDevtools } from '@tanstack/react-query-devtools'import { queryClientAtom } from 'jotai-tanstack-query'const queryClient = new QueryClient({defaultOptions: {queries: {staleTime: Infinity,},},})const HydrateAtoms = ({ children }) => {useHydrateAtoms([[queryClientAtom, queryClient]])return children}export const App = () => {return (<QueryClientProvider client={queryClient}><Provider><HydrateAtoms><App /></HydrateAtoms></Provider><ReactQueryDevtools /></QueryClientProvider>)}
Migrate to v0.8.0
atom 시그니처 변경
모든 atom 시그니처가 TanStack Query와 더 일관되도록 변경되었습니다.
v0.8.0에서는 튜플 형태의 atom 대신 단일 atom만 반환하므로, atomsWithSomething
에서 atomWithSomething
으로 이름이 변경되었습니다.
- const [dataAtom, statusAtom] = atomsWithSomething(getOptions, getQueryClient)+ const dataAtom = atomWithSomething(getOptions, getQueryClient)
단순화된 반환 구조
이전 버전의 jotai-tanstack-query
에서는 쿼리 아톰인 atomsWithQuery
와 atomsWithInfiniteQuery
가 [dataAtom, statusAtom]
형태의 튜플을 반환했습니다. 이 설계는 데이터와 그 상태를 두 개의 다른 아톰으로 분리했습니다.
atomWithQuery와 atomWithInfiniteQuery
dataAtom
은 실제 데이터(TData
)에 접근하는 데 사용되었습니다.statusAtom
은isPending
,isError
등의 추가 속성을 포함한 상태 객체(QueryObserverResult<TData, TError>
)를 제공했습니다.
v0.8.0에서는 이들이 atomWithQuery
와 atomWithInfiniteQuery
로 대체되어 단일 dataAtom
만 반환하도록 변경되었습니다. 이제 dataAtom
은 QueryObserverResult<TData, TError>
를 직접 제공하며, 이는 Tanstack Query의 바인딩과 더욱 일치하도록 조정되었습니다.
새 버전으로 마이그레이션하려면, 기존의 dataAtom
과 statusAtom
을 분리해서 사용하던 방식을 통합된 dataAtom
으로 대체해야 합니다. 이제 dataAtom
은 데이터와 상태 정보를 모두 포함합니다.
- const [dataAtom, statusAtom] = atomsWithQuery(/* ... */);- const [data] = useAtom(dataAtom);- const [status] = useAtom(statusAtom);+ const dataAtom = atomWithQuery(/* ... */);+ const [{ data, isPending, isError }] = useAtom(dataAtom);
atomWithMutation
atomsWithQuery
와 atomsWithInfiniteQuery
와 유사하게, atomWithMutation
도 튜플 대신 단일 아톰을 반환합니다. 아톰 값의 반환 타입은 MutationObserverResult<TData, TError, TVariables, TContext>
입니다.
- const [, postAtom] = atomsWithMutation(/* ... */);- const [post, mutate] = useAtom(postAtom); // post에서 뮤테이션 상태에 접근하고, mutate()로 뮤테이션 실행+ const postAtom = atomWithMutation(/* ... */);+ const [{ data, error, mutate }] = useAtom(postAtom); // 동일한 아톰에서 뮤테이션 결과와 mutate 메서드에 접근