본문으로 건너뛰기

TanStack Query v5로 마이그레이션하기

호환성을 깨는 변경 사항

v5는 메이저 버전이므로 알아두어야 할 몇 가지 호환성을 깨는 변경 사항이 있습니다:

단일 시그니처와 하나의 객체를 지원합니다

useQuery 및 관련 항목에는 TypeScript에서 함수 호출 방식이 서로 다른 여러 오버로드가 있었습니다. 이는 타입 측면에서 유지 관리하기 어려웠을 뿐만 아니라, 옵션을 올바르게 생성하기 위해 첫 번째와 두 번째 매개변수의 타입을 확인하는 런타임 검사도 필요했습니다.

이제 객체 형식만 지원합니다.

useQuery(key, fn, options) // [!code --]
useQuery({ queryKey, queryFn, ...options }) // [!code ++]
useInfiniteQuery(key, fn, options) // [!code --]
useInfiniteQuery({ queryKey, queryFn, ...options }) // [!code ++]
useMutation(fn, options) // [!code --]
useMutation({ mutationFn, ...options }) // [!code ++]
useIsFetching(key, filters) // [!code --]
useIsFetching({ queryKey, ...filters }) // [!code ++]
useIsMutating(key, filters) // [!code --]
useIsMutating({ mutationKey, ...filters }) // [!code ++]
queryClient.isFetching(key, filters) // [!code --]
queryClient.isFetching({ queryKey, ...filters }) // [!code ++]
queryClient.getQueriesData(key, filters) // [!code --]
queryClient.getQueriesData({ queryKey, ...filters }) // [!code ++]
queryClient.setQueriesData(key, updater, filters, options) // [!code --]
queryClient.setQueriesData({ queryKey, ...filters }, updater, options) // [!code ++]
queryClient.removeQueries(key, filters) // [!code --]
queryClient.removeQueries({ queryKey, ...filters }) // [!code ++]
queryClient.resetQueries(key, filters, options) // [!code --]
queryClient.resetQueries({ queryKey, ...filters }, options) // [!code ++]
queryClient.cancelQueries(key, filters, options) // [!code --]
queryClient.cancelQueries({ queryKey, ...filters }, options) // [!code ++]
queryClient.invalidateQueries(key, filters, options) // [!code --]
queryClient.invalidateQueries({ queryKey, ...filters }, options) // [!code ++]
queryClient.refetchQueries(key, filters, options) // [!code --]
queryClient.refetchQueries({ queryKey, ...filters }, options) // [!code ++]
queryCache.find(key, filters) // [!code --]
queryCache.find({ queryKey, ...filters }) // [!code ++]
queryCache.findAll(key, filters) // [!code --]
queryCache.findAll({ queryKey, ...filters }) // [!code ++]

명령형 QueryClient 메서드

이 메서드들은 queryClient.queryqueryClient.infiniteQuery의 도입으로 더 이상 사용되지 않으며 v6에서 제거됩니다.

v4 또는 이전 버전에서 마이그레이션하는 경우:

queryClient.fetchQuery(key, fn, options) // [!code --]
queryClient.query({ queryKey: key, queryFn: fn, ...options }) // [!code ++]
queryClient.fetchInfiniteQuery(key, fn, options) // [!code --]
queryClient.infiniteQuery({
queryKey: key,
queryFn: fn,
...options,
}) // [!code ++]

queryClient.prefetchQuery(key, fn, options) // [!code --]
queryClient.query({ queryKey: key, queryFn: fn, ...options }).catch(noop) // [!code ++]

queryClient.prefetchInfiniteQuery(key, fn, options) // [!code --]
queryClient
.infiniteQuery({ queryKey: key, queryFn: fn, ...options })
.catch(noop) // [!code ++]

queryClient.ensureQueryData(key, options) // [!code --]
queryClient.query({ queryKey: key, ...options, staleTime: 'static' }) // [!code ++]

queryClient.ensureInfiniteQueryData(key, options) // [!code --]
queryClient.infiniteQuery({ queryKey: key, ...options, staleTime: 'static' }) // [!code ++]

이전 v5 코드를 업데이트하는 경우 단일 옵션 객체를 유지한다는 점을 제외하면 위와 동일합니다

이제 queryClient.getQueryData는 인수로 queryKey만 허용합니다

queryClient.getQueryData 인수가 queryKey만 받도록 변경되었습니다

queryClient.getQueryData(queryKey, filters) // [!code --]
queryClient.getQueryData(queryKey) // [!code ++]

이제 queryClient.getQueryState는 인수로 queryKey만 허용합니다

queryClient.getQueryState 인수가 queryKey만 받도록 변경되었습니다

queryClient.getQueryState(queryKey, filters) // [!code --]
queryClient.getQueryState(queryKey) // [!code ++]

Codemod

remove 오버로드 마이그레이션을 더 쉽게 할 수 있도록 v5에는 codemod가 포함되어 있습니다.

codemod는 호환성을 깨뜨리는 변경 사항의 마이그레이션을 돕기 위해 최선을 다합니다. 생성된 코드를 철저히 검토해 주세요! 또한 codemod로 찾을 수 없는 예외 사례도 있으므로 로그 출력을 주의 깊게 살펴보세요.

.js 또는 .jsx 파일을 대상으로 실행하려면 아래 명령을 사용하세요:

npx jscodeshift@latest ./path/to/src/ \
--extensions=js,jsx \
--transform=./node_modules/@tanstack/react-query/build/codemods/src/v5/remove-overloads/remove-overloads.cjs

.ts 또는 .tsx 파일을 대상으로 실행하려면 아래 명령을 사용하세요:

npx jscodeshift@latest ./path/to/src/ \
--extensions=ts,tsx \
--parser=tsx \
--transform=./node_modules/@tanstack/react-query/build/codemods/src/v5/remove-overloads/remove-overloads.cjs

TypeScript의 경우 tsx을 파서로 사용해야 합니다. 그렇지 않으면 codemod가 제대로 적용되지 않는다는 점에 유의하세요!

참고: codemod를 적용하면 코드 서식이 깨질 수 있으므로, codemod를 적용한 후 prettier 및/또는 eslint를 실행하는 것을 잊지 마세요!

codemod 작동 방식에 관한 몇 가지 참고 사항:

  • 일반적으로 첫 번째 매개변수가 객체 표현식이고, 변환되는 훅/메서드 호출에 따라 "queryKey" 또는 "mutationKey" 속성을 포함하는 운 좋은 경우를 찾습니다. 이 경우 코드는 이미 새 시그니처와 일치하므로 codemod가 수정하지 않습니다. 🎉
  • 위 조건이 충족되지 않으면 codemod는 첫 번째 매개변수가 배열 표현식인지 또는 배열 표현식을 참조하는 식별자인지 확인합니다. 이에 해당하면 codemod가 이를 객체 표현식에 넣으며, 그러면 해당 객체 표현식이 첫 번째 매개변수가 됩니다.
  • 객체 매개변수를 추론할 수 있으면 codemod는 이미 존재하는 속성을 새로 생성된 객체에 복사하려고 시도합니다.
  • codemod가 사용 방식을 추론할 수 없으면 콘솔에 메시지를 남깁니다. 이 메시지에는 해당 사용 위치의 파일 이름과 줄 번호가 포함됩니다. 이 경우 마이그레이션을 수동으로 수행해야 합니다.
  • 변환 결과 오류가 발생하면 콘솔에도 메시지가 표시됩니다. 이 메시지는 예기치 않은 문제가 발생했음을 알리므로 마이그레이션을 수동으로 수행해 주십시오.

useQuery 및 QueryObserver의 콜백이 제거되었습니다

Queries에서 onSuccess, onErroronSettled를 제거했습니다. Mutations에는 변경 사항이 없습니다. 이 변경의 동기와 대신 해야 할 작업은 이 RFC을 참조하세요.

refetchInterval 콜백 함수에는 query만 전달됩니다

이를 통해 콜백 호출 방식이 간소화되며(refetchOnWindowFocus, refetchOnMountrefetchOnReconnect 콜백에도 모두 쿼리만 전달됩니다), 콜백이 select에 의해 변환된 데이터를 받을 때 발생하는 일부 타입 지정 문제도 해결됩니다.

- refetchInterval: number | false | ((data: TData | undefined, query: Query) => number | false | undefined) // [!code --]
+ refetchInterval: number | false | ((query: Query) => number | false | undefined) // [!code ++]

여전히 query.state.data로 데이터에 접근할 수 있지만, 이는 select에 의해 변환된 데이터가 아닙니다. 변환된 데이터에 접근해야 한다면 query.state.data에서 변환을 다시 호출할 수 있습니다.

remove 메서드가 useQuery에서 제거되었습니다

이전에는 remove 메서드를 사용해 관찰자에게 알리지 않고 queryCache에서 쿼리를 제거했습니다. 이 메서드는 더 이상 필요하지 않은 데이터를 명령형으로 제거할 때, 예를 들어 사용자가 로그아웃할 때 사용하는 것이 가장 적합했습니다.

하지만 쿼리가 아직 활성 상태일 때 이렇게 하는 것은 별로 의미가 없습니다. 다음 재렌더링 시 하드 로딩 상태만 발생시키기 때문입니다.

그래도 쿼리를 제거해야 한다면 queryClient.removeQueries({queryKey: key})를 사용할 수 있습니다

const queryClient = useQueryClient()
const query = useQuery({ queryKey, queryFn })

query.remove() // [!code --]
queryClient.removeQueries({ queryKey }) // [!code ++]

필요한 최소 TypeScript 버전은 이제 4.7입니다

주된 이유는 타입 추론과 관련된 중요한 수정 사항이 배포되었기 때문입니다. 자세한 내용은 이 TypeScript 이슈를 참조합니다.

isDataEqual 옵션이 useQuery에서 제거되었습니다

이전에는 이 함수가 쿼리에서 이행된 데이터로 이전 data(true)를 사용할지 새 데이터(false)를 사용할지 나타내는 데 사용되었습니다.

대신 structuralSharing에 함수를 전달하여 동일한 기능을 구현할 수 있습니다:

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

- isDataEqual: (oldData, newData) => customCheck(oldData, newData) // [!code --]
+ structuralSharing: (oldData, newData) => customCheck(oldData, newData) ? oldData : replaceEqualDeep(oldData, newData) // [!code ++]

사용 중단된 사용자 지정 로거가 제거되었습니다

사용자 지정 로거는 이미 4에서 사용 중단되었으며 이 버전에서 제거되었습니다. 로깅은 개발 모드에서만 영향을 미쳤으며, 이 모드에서는 사용자 지정 로거를 전달할 필요가 없습니다.

지원되는 브라우저

더 현대적이고 성능이 우수하며 작은 번들을 생성하도록 browserslist를 업데이트했습니다. 요구 사항은 여기에서 확인할 수 있습니다.

비공개 클래스 필드 및 메서드

TanStack Query의 클래스에는 항상 비공개 필드와 메서드가 있었지만, 실제로 비공개인 것은 아니었으며 TypeScript에서만 비공개였습니다. 이제 ECMAScript 비공개 클래스 기능을 사용하므로 해당 필드는 이제 실제로 비공개이며 런타임에 외부에서 접근할 수 없습니다.

cacheTime의 이름을 gcTime으로 변경하기

거의 모든 사람이 cacheTime을 잘못 이해합니다. "데이터가 캐시되는 시간"처럼 들리지만, 이는 정확하지 않습니다.

쿼리가 아직 사용 중인 동안에는 cacheTime이 아무 작업도 하지 않습니다. 쿼리가 사용되지 않는 상태가 되는 즉시 작동하기 시작합니다. 지정된 시간이 지나면 캐시가 커지는 것을 방지하기 위해 데이터가 "가비지 컬렉션"됩니다.

gc는 "garbage collect" 시간을 의미합니다. 다소 기술적인 용어이지만 컴퓨터 과학에서 잘 알려진 약어이기도 합니다.

const MINUTE = 1000 * 60;

const queryClient = new QueryClient({
defaultOptions: {
queries: {
- cacheTime: 10 * MINUTE, // [!code --]
+ gcTime: 10 * MINUTE, // [!code ++]
},
},
})

useErrorBoundary 옵션의 이름이 throwOnError로 변경되었습니다

useErrorBoundary 옵션을 프레임워크에 더욱 독립적으로 만들고, 훅에 사용되는 확립된 React 함수 접두사 "use" 및 "ErrorBoundary" 컴포넌트 이름과의 혼동을 피하기 위해, 기능을 더 정확하게 반영하도록 이름이 throwOnError로 변경되었습니다.

TypeScript: 이제 오류의 기본 타입은 unknown 대신 Error입니다

JavaScript에서는 무엇이든 throw할 수 있으므로 unknown이 가장 정확한 타입이지만, 거의 항상 Errors(또는 Error의 서브클래스)가 발생합니다. 이 변경으로 대부분의 경우 TypeScript의 error 필드를 더 쉽게 다룰 수 있습니다.

Error가 아닌 항목을 발생시키려면 이제 제네릭을 직접 설정해야 합니다:

useQuery<number, string>({
queryKey: ['some-query'],
queryFn: async () => {
if (Math.random() > 0.5) {
throw 'some error'
}
return 42
},
})

전역적으로 다른 종류의 Error를 설정하는 방법은 TypeScript 가이드를 참조하세요.

eslint prefer-query-object-syntax 규칙이 제거되었습니다

이제 지원되는 유일한 구문이 객체 구문이므로 이 규칙은 더 이상 필요하지 않습니다

placeholderData 항등 함수를 사용하도록 keepPreviousData 제거

keepPreviousData 옵션과 isPreviousData 플래그는 placeholderDataisPlaceholderData 플래그와 대부분 동일한 기능을 했기 때문에 제거했습니다.

keepPreviousData와 동일한 기능을 구현하기 위해 항등 함수를 받는 placeholderData의 인수로 이전 쿼리 data를 추가했습니다. 따라서 placeholderData에 항등 함수를 제공하거나 TanStack Query에 포함된 keepPreviousData 함수를 사용하기만 하면 됩니다.

여기서 유의할 점은 배열로 전달된 쿼리의 동적 특성으로 인해 placeholder와 queryFn의 결과 형태가 달라질 수 있으므로, useQueriesplaceholderData 함수에서 previousData를 인수로 받지 않는다는 것입니다.

import {
useQuery,
+ keepPreviousData // [!code ++]
} from "@tanstack/react-query";

const {
data,
- isPreviousData, // [!code --]
+ isPlaceholderData, // [!code ++]
} = useQuery({
queryKey,
queryFn,
- keepPreviousData: true, // [!code --]
+ placeholderData: keepPreviousData // [!code ++]
});

TanStack Query 컨텍스트에서 항등 함수란 제공된 인수(즉, 데이터)를 항상 변경하지 않고 그대로 반환하는 함수를 의미합니다.

useQuery({
queryKey,
queryFn,
placeholderData: (previousData, previousQuery) => previousData, // identity function with the same behaviour as `keepPreviousData`
})

하지만 이 변경 사항에는 반드시 알아야 할 몇 가지 주의점이 있습니다:

  • placeholderData는 항상 success 상태로 전환하지만, keepPreviousData는 이전 쿼리의 상태를 제공했습니다. 데이터를 성공적으로 가져온 후 백그라운드 다시 가져오기 오류가 발생한 경우 해당 상태는 error일 수 있습니다. 하지만 오류 자체는 공유되지 않았으므로 placeholderData의 동작을 유지하기로 결정했습니다.

  • keepPreviousData는 이전 데이터의 dataUpdatedAt 타임스탬프를 제공했지만, placeholderData를 사용하면 dataUpdatedAt0로 유지됩니다. 해당 타임스탬프를 화면에 계속 표시하려는 경우 불편할 수 있습니다. 하지만 useEffect를 사용하면 이 문제를 우회할 수 있습니다.

    const [updatedAt, setUpdatedAt] = useState(0)

    const { data, dataUpdatedAt } = useQuery({
    queryKey: ['projects', page],
    queryFn: () => fetchProjects(page),
    })

    useEffect(() => {
    if (dataUpdatedAt > updatedAt) {
    setUpdatedAt(dataUpdatedAt)
    }
    }, [dataUpdatedAt])

창 포커스 시 다시 가져오기는 더 이상 focus 이벤트를 수신하지 않습니다

이제 visibilitychange 이벤트만 사용됩니다. visibilitychange 이벤트를 지원하는 브라우저만 지원하기 때문에 가능합니다. 이를 통해 여기에 나열된 여러 문제가 해결됩니다.

네트워크 상태는 더 이상 navigator.onLine 속성에 의존하지 않습니다

navigator.onLine은 Chromium 기반 브라우저에서 제대로 작동하지 않습니다. 거짓 음성과 관련하여 많은 문제가 있으며, 이로 인해 Query가 잘못 offline으로 표시됩니다.

이를 피하기 위해 이제 항상 online: true로 시작하고, 상태를 업데이트하기 위해 onlineoffline 이벤트만 수신합니다.

이렇게 하면 거짓 음성이 발생할 가능성이 줄어들지만, 인터넷 연결 없이도 작동할 수 있는 serviceWorkers를 통해 로드되는 오프라인 앱에서는 거짓 양성이 발생할 수 있습니다.

사용자 지정 context prop을 제거하고 사용자 지정 queryClient 인스턴스로 대체했습니다

v4에서는 모든 react-query 훅에 사용자 지정 context를 전달할 수 있는 기능을 도입했습니다. 이를 통해 MicroFrontends를 사용할 때 올바르게 격리할 수 있었습니다.

그러나 context는 React 전용 기능입니다. context가 하는 일은 queryClient에 접근할 수 있게 해주는 것뿐입니다. 사용자 지정 queryClient를 직접 전달하도록 허용해도 동일한 격리를 구현할 수 있습니다. 이는 결과적으로 다른 프레임워크에서도 프레임워크에 구애받지 않는 방식으로 동일한 기능을 사용할 수 있게 합니다.

import { queryClient } from './my-client'

const { data } = useQuery(
{
queryKey: ['users', id],
queryFn: () => fetch(...),
- context: customContext // [!code --]
},
+ queryClient, // [!code ++]
)

maxPages를 위해 refetchPage가 제거되었습니다

v4에서는 refetchPage 함수를 사용해 무한 쿼리에서 다시 가져올 페이지를 정의할 수 있는 기능을 도입했습니다.

그러나 모든 페이지를 다시 가져오면 UI 불일치가 발생할 수 있습니다. 또한 이 옵션은 예를 들어 queryClient.refetchQueries에서 사용할 수 있지만, "일반" 쿼리가 아닌 무한 쿼리에만 효과가 있습니다.

v5에는 쿼리 데이터에 저장하고 다시 가져올 페이지 수를 제한하는 무한 쿼리용 새 maxPages 옵션이 포함됩니다. 이 새로운 기능은 관련 문제 없이 refetchPage 페이지 기능을 위해 처음 식별된 사용 사례를 처리합니다.

dehydrate API

dehydrate에 전달할 수 있는 옵션이 단순화되었습니다. 쿼리와 뮤테이션은 항상 디하이드레이션됩니다(기본 함수 구현에 따름). 이 동작을 변경하려면 제거된 불리언 옵션 dehydrateMutationsdehydrateQueries를 사용하는 대신 이에 상응하는 함수인 shouldDehydrateQuery 또는 shouldDehydrateMutation을 구현할 수 있습니다. 쿼리/뮤테이션을 전혀 하이드레이션하지 않던 이전 동작을 사용하려면 () => false를 전달합니다.

- dehydrateMutations?: boolean // [!code --]
- dehydrateQueries?: boolean // [!code --]

무한 쿼리에는 이제 initialPageParam이 필요합니다

이전에는 undefinedpageParam으로 queryFn에 전달했으며, queryFn 함수 시그니처의 pageParam 매개변수에 기본값을 할당할 수 있었습니다. 이 방식에는 직렬화할 수 없는 undefinedqueryCache에 저장한다는 단점이 있었습니다.

대신 이제 무한 쿼리 옵션에 명시적인 initialPageParam을 전달해야 합니다. 이는 첫 번째 페이지의 pageParam으로 사용됩니다:

useInfiniteQuery({
queryKey,
- queryFn: ({ pageParam = 0 }) => fetchSomething(pageParam), // [!code --]
+ queryFn: ({ pageParam }) => fetchSomething(pageParam), // [!code ++]
+ initialPageParam: 0, // [!code ++]
getNextPageParam: (lastPage) => lastPage.next,
})

무한 쿼리의 수동 모드가 제거되었습니다

이전에는 pageParam 값을 fetchNextPage 또는 fetchPreviousPage에 직접 전달하여 getNextPageParam 또는 getPreviousPageParam에서 반환될 pageParams를 덮어쓸 수 있었습니다. 이 기능은 다시 가져오기에서 전혀 작동하지 않았으며 널리 알려지거나 사용되지도 않았습니다. 이는 또한 이제 무한 쿼리에 getNextPageParam이 필수임을 의미합니다.

이제 getNextPageParam 또는 getPreviousPageParam에서 null을 반환하면 사용 가능한 다음 페이지가 없음을 나타냅니다

v4에서는 더 이상 사용할 수 있는 페이지가 없음을 나타내기 위해 undefined를 명시적으로 반환해야 했습니다. 이 검사를 null도 포함하도록 확장했습니다.

서버에서는 재시도하지 않음

서버에서 retry의 기본값은 이제 3 대신 0입니다. 프리페치의 재시도 기본값은 항상 0회였지만, 이제 suspense가 활성화된 쿼리도 서버에서 직접 실행될 수 있으므로(React18부터) 서버에서는 전혀 재시도하지 않도록 해야 합니다.

status: loadingstatus: pending으로 변경되었고, isLoadingisPending으로 변경되었으며, 이제 isInitialLoading의 이름이 isLoading으로 변경되었습니다

loading 상태의 이름이 pending으로 변경되었으며, 마찬가지로 파생된 isLoading 플래그의 이름도 isPending으로 변경되었습니다.

뮤테이션에서도 statusloading에서 pending으로 변경되었으며 isLoading 플래그가 isPending으로 변경되었습니다.

마지막으로 isPending && isFetching으로 구현된 새로운 파생 isLoading 플래그가 쿼리에 추가되었습니다. 이는 isLoadingisInitialLoading이 동일한 값을 가진다는 의미이지만, 이제 isInitialLoading은 더 이상 사용되지 않으며 다음 메이저 버전에서 제거됩니다.

이 변경의 배경을 이해하려면 v5 로드맵 논의를 확인하세요.

hashQueryKey의 이름이 hashKey로 변경되었습니다.

이는 뮤테이션 키도 해시하며 뮤테이션이 전달되는 useIsMutatinguseMutationStatepredicate 함수 내에서 사용할 수 있기 때문입니다.

이제 필요한 최소 React 버전은 18.0입니다

React Query v5에는 React 18.0 이상이 필요합니다. 이는 React 18.0 이상에서만 사용할 수 있는 새로운 useSyncExternalStore 훅을 사용하기 때문입니다. 이전에는 React에서 제공하는 shim을 사용했습니다.

contextSharing prop이 QueryClientProvider에서 제거되었습니다

이전에는 contextSharing 속성을 사용하여 쿼리 클라이언트 컨텍스트의 첫 번째(이자 최소 하나의) 인스턴스를 window 전역에서 공유할 수 있었습니다. 이를 통해 TanStack Query를 서로 다른 번들이나 마이크로 프런트엔드에서 사용하더라도 모듈 범위와 관계없이 모두 동일한 컨텍스트 인스턴스를 사용하도록 보장했습니다.

v5에서 사용자 지정 context prop이 제거되었으므로, 사용자 지정 queryClient 인스턴스를 위해 제거된 사용자 지정 context prop 섹션을 참조하세요. 애플리케이션의 여러 패키지에서 동일한 쿼리 클라이언트를 공유하려면 공유된 사용자 지정 queryClient 인스턴스를 직접 전달할 수 있습니다.

React 및 React Native에서 더 이상 unstable_batchedUpdates를 배치 함수로 사용하지 않습니다

React 18에서 unstable_batchedUpdates 함수는 아무 작업도 하지 않으므로, 더 이상 react-query에서 일괄 처리 함수로 자동 설정되지 않습니다.

프레임워크가 사용자 정의 일괄 처리 함수를 지원하는 경우 notifyManager.setBatchNotifyFunction을 호출하여 TanStack Query에 이를 알릴 수 있습니다.

예를 들어 solid-query에서는 batch 함수가 다음과 같이 설정됩니다:

import { notifyManager } from '@tanstack/query-core'
import { batch } from 'solid-js'

notifyManager.setBatchNotifyFunction(batch)

하이드레이션 API 변경 사항

동시성 기능과 전환을 더 잘 지원하기 위해 하이드레이션 API를 일부 변경했습니다. Hydrate 컴포넌트의 이름을 HydrationBoundary로 변경했으며 useHydrate 훅을 제거했습니다.

HydrationBoundary는 더 이상 뮤테이션을 하이드레이션하지 않고 쿼리만 하이드레이션합니다. 뮤테이션을 하이드레이션하려면 저수준 hydrate API 또는 persistQueryClient 플러그인을 사용합니다.

마지막으로 기술적인 세부 사항으로, 쿼리가 하이드레이션되는 시점이 약간 변경되었습니다. 새 쿼리는 SSR이 평소처럼 작동하도록 여전히 렌더링 단계에서 하이드레이션되지만, 캐시에 이미 존재하는 쿼리는 이제 그 데이터가 캐시에 있는 데이터보다 최신인 경우 effect에서 대신 하이드레이션됩니다. 일반적인 방식대로 애플리케이션 시작 시 한 번만 하이드레이션한다면 이 변경 사항의 영향을 받지 않지만, Server Components를 사용하고 페이지 탐색 시 하이드레이션을 위한 최신 데이터를 전달한다면 페이지가 즉시 다시 렌더링되기 전에 이전 데이터가 잠깐 표시되는 현상을 볼 수 있습니다.

이 마지막 변경 사항은 엄밀히 말하면 호환성을 깨는 변경이며, 페이지 전환이 완전히 확정되기 전에 기존 페이지의 콘텐츠를 섣불리 업데이트하지 않도록 적용되었습니다. 사용자가 취해야 할 조치는 없습니다.

- import { Hydrate } from '@tanstack/react-query' // [!code --]
+ import { HydrationBoundary } from '@tanstack/react-query' // [!code ++]


- <Hydrate state={dehydratedState}> // [!code --]
+ <HydrationBoundary state={dehydratedState}> // [!code ++]
<App />
- </Hydrate> // [!code --]
+ </HydrationBoundary> // [!code ++]

쿼리 기본값 변경 사항

queryClient.getQueryDefaults는 이제 일치하는 첫 번째 등록만 반환하지 않고 일치하는 모든 등록을 병합합니다.

그 결과 이제 queryClient.setQueryDefaults 호출은 특이성이 증가하는 순서로 배치해야 합니다. 즉, 등록은 가장 일반적인 키부터 가장 덜 일반적인 키 순으로 이루어져야 합니다.

예를 들면:

+ queryClient.setQueryDefaults(['todo'], {   // [!code ++]
+ retry: false, // [!code ++]
+ staleTime: 60_000, // [!code ++]
+ }) // [!code ++]
queryClient.setQueryDefaults(['todo', 'detail'], {
+ retry: true, // [!code --]
retryDelay: 1_000,
staleTime: 10_000,
})
- queryClient.setQueryDefaults(['todo'], { // [!code --]
- retry: false, // [!code --]
- staleTime: 60_000, // [!code --]
- }) // [!code --]

이 특정 예시에서는 이제 더 일반적인 등록에서 retry: false를 상속받는 것을 상쇄하기 위해 ['todo', 'detail'] 등록에 retry: true가 추가되었다는 점에 유의하세요. 정확한 동작을 유지하는 데 필요한 구체적인 변경 사항은 기본값에 따라 달라집니다.

새로운 기능 🚀

v5에는 다음과 같은 새로운 기능도 포함됩니다:

간소화된 낙관적 업데이트

useMutation에서 반환된 variables를 활용하여 낙관적 업데이트를 수행하는 새롭고 간소화된 방법이 제공됩니다:

const queryInfo = useTodos()
const addTodoMutation = useMutation({
mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }),
onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
})

if (queryInfo.data) {
return (
<ul>
{queryInfo.data.items.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
{addTodoMutation.isPending && (
<li key={String(addTodoMutation.submittedAt)} style={{ opacity: 0.5 }}>
{addTodoMutation.variables}
</li>
)}
</ul>
)
}

여기서는 캐시에 데이터를 직접 쓰는 대신 뮤테이션이 실행 중일 때 UI가 표시되는 방식만 변경합니다. 낙관적 업데이트를 표시해야 하는 곳이 하나뿐일 때 가장 효과적입니다. 자세한 내용은 낙관적 업데이트 문서를 살펴보세요.

새로운 maxPages 옵션을 사용하는 제한된 무한 쿼리

무한 스크롤이나 페이지네이션이 필요할 때 무한 쿼리가 매우 유용합니다. 하지만 가져오는 페이지가 많을수록 더 많은 메모리를 소비하며, 모든 페이지를 순차적으로 다시 가져오므로 쿼리를 다시 가져오는 과정도 느려집니다.

Version 5에는 무한 쿼리를 위한 새로운 maxPages 옵션이 있으며, 이를 통해 개발자는 쿼리 데이터에 저장되고 이후 다시 가져오는 페이지 수를 제한할 수 있습니다. 제공하려는 UX 및 다시 가져오기 성능에 따라 maxPages 값을 조정할 수 있습니다.

무한 목록은 양방향이어야 하므로 getNextPageParamgetPreviousPageParam을 모두 정의해야 합니다.

무한 쿼리는 여러 페이지를 프리페치할 수 있습니다

Infinite Queries는 일반 Queries처럼 프리페치할 수 있습니다. 기본적으로 Query의 첫 페이지만 프리페치되어 지정된 QueryKey 아래에 저장됩니다. 둘 이상의 페이지를 프리페치하려면 pages 옵션을 사용할 수 있습니다. 자세한 내용은 프리페치 가이드를 읽어보세요.

useQueries를 위한 새로운 combine 옵션

자세한 내용은 useQueries 문서를 참조하세요.

실험적 fine grained storage persister

자세한 내용은 experimental_createPersister 문서를 참조하세요.

Query Options를 생성하는 타입 안전 방식

자세한 내용은 TypeScript 문서를 참조하세요.

Suspense를 위한 새로운 훅

v5를 사용하면 데이터 가져오기를 위한 suspense가 마침내 "안정적"이 됩니다. 전용 useSuspenseQuery, useSuspenseInfiniteQueryuseSuspenseQueries 훅을 추가했습니다. 이러한 훅을 사용하면 타입 수준에서 data는 절대로 잠재적인 undefined가 되지 않습니다:

const { data: post } = useSuspenseQuery({
// ^? const post: Post
queryKey: ['post', postId],
queryFn: () => fetchPost(postId),
})

쿼리 훅의 실험적 suspense: boolean 플래그가 제거되었습니다.

자세한 내용은 suspense 문서에서 확인할 수 있습니다.