드디어 TanStack Query v5가 릴리즈되었습니다! v4보다 용량은 더작게, 하지만 더 직관적이게 변화했는데요, 변경사항과 마이그레이션시의 주의사항을 살펴보도록 하겠습니다.
1. Breaking Change
가장 큰 변화에는 코드베이스로부터의 overload를 제거하여 useQuery와같은 훅을 더 직관적으로 만들었다는 것입니다. 사실 v4부터 하려고 기획했던것이지만 타입스크립트에서의 제약으로 인해 할수가없었는데요, TS 4.7에서 이부분이 보완되었고, 훅을 호출할때의 다양한 매개변수를 이제는 신경쓰지 않아도되게 변화하였습니다. v5에서는 객체만을 지원합니다. 이 부분은 DX측면에서 큰 성과라고 할수있습니다. 일일히 overload를 고려하지않아도되며, 하나의 객체만을 전달하면되기때문에 일관성이 높아졌습니다. 당연히 새로운 API를 사용할수있도록 eslint 패키지에 자동수정가능한 규칙도 배포되었습니다. 또한 v5로 마이그레이션하는데에 도움이되는 codemod도 추가되었습니다.
이외에도, 기존 cacheTime을 gcTime으로 이름을 변경하여 직관성을 높였고, keepPrevioudsData 옵션을 placeholderData 옵션과 통합하여 비슷한 기능을 하던 두가지의 옵션을 하나로 재정의하였습니다. 또한 loading으로 표시되던 상태값을 pending이라는 이름으로 변경되었고, useQuery에서의 콜백을 제거하였습니다.
2. New Features
당연히 새롭게 추가된 기능들도 있습니다.
- Simplified optimistic updates: cache를 수동적으로 업데이트하는 코드대신, useMutation에서 반환된 'variables'를 사용하여 업데이트를 수행할수있습니다.
- Sharable mutation state: useMutation 훅을 통해 컴포넌트 간에 공유되는 모든 상태에 접근할수있습니다.
- 1st class 'suspense' support: suspense를 완전히 지원하며, useSuspenseQuery나 useSuspenseInfiniteQuery와 같은 훅을 사용할수있습니다.
- Improved Infinite Queries: infinite query는 이제 한번에 여러페이지를 prefetch할수있으며, 옵션으로 최대페이지의 개수까지 지정할수있습니다.
- New Devtools: Query devtools는 모든 어댑터, 프레임워크에 사용될수있도록 다시 개발되었습니다. UI개선과함께 cache 인라인 편집이나 light mode같은 기능도 추가되었습니다.
- Fine-grained persistense: PersistQueryClient 플러그인에 기존에는 없었던 복원기능이 추가되었습니다. experimental_createPersister를 사용하면 storage를 전달할수있습니다.
3. Migration Guide
1) Supports a single signature, one object
이제 useQuery, useInfiniteQuery등 훅을 호출할때 객체만을 지원합니다. TS에 많은 오버로드를 가지는것과 매개변수 유형을 확인하기위해 runtime 검사를 해야하는 수고로움이 사라졌습니다. 타입스크립트 4.7 이전버전에서는 함수인수가 unknown으로 추론되는 이슈가 있었고, 이 때문에 useQuery등의 훅에서 매개변수를 사용할때 많은 타입스크립트 에러를 만나야만했습니다. 하지만 이 부분이 TS 4.7에서 해결이되면서 오버로드가 모두 제거되었기때문에 단일객체로 사용이 가능합니다. 그렇기때문에 최소한 TS 버전은 4.7이상으로 설정해주어야합니다.
- useQuery(key, fn, options)
+ useQuery({ queryKey, queryFn, ...options })
- useInfiniteQuery(key, fn, options)
+ useInfiniteQuery({ queryKey, queryFn, ...options })
- useMutation(fn, options)
+ useMutation({ mutationFn, ...options })
- useIsFetching(key, filters)
+ useIsFetching({ queryKey, ...filters })
- useIsMutating(key, filters)
+ useIsMutating({ mutationKey, ...filters })
2) getQueryData, getQueryState now accepts queryKey only as an Argument
getQueryData와 getQueryState는 이제부터 queryKey만을 인자로 받습니다. 기존에는 filters 인자도 optional하게 받아서, stale이나 exact 등의 필터를 설정할수있었지만, v5에서는 filters 설정기능이 사라졌습니다.
- queryClient.getQueryData(queryKey, filters)
+ queryClient.getQueryData(queryKey)
- queryClient.getQueryState(queryKey, filters)
+ queryClient.getQueryState(queryKey)
3) Callbacks on useQuery have been removed
useQuery에서의 콜백이 제거되었습니다(useMutation에서의 콜백은 사라지지않았습니다). 따라서 onSuccess, onError, onSettled는 이제 사용하지 못합니다. useQuery의 콜백이 제거된 이유는 예상대로 동작하지 않을 가능성이 높기때문입니다. 기존에는 쿼리가 성공적으로 실행되었을때 / 오류가 발생했을때 / 두 경우모두 해당할때 호출되는 3가지 콜백함수 onSuccess / onError / onSettled가 있었습니다. 하지만 이러한 콜백들이 일어나지않을수도, 중복으로 호출될수도 있습니다. (자세한 사례는 본글을 참조하세요) 따라서 이 콜백들은 직관적이지 못합니다. v5에서는 이러한 콜백을 사용하지 못하기때문에 다른 대안법을 찾아야합니다. onError대신 meta필드를 사용한다든지, onSuccess대신 value값 자체를 반환한다던지의 대안을 사용해야합니다.
//AS-IS
export function useTodos() {
return useQuery({
queryKey: ['todos', 'list'],
queryFn: fetchTodos,
onError: (error) => {
toast.error(error.message)
},
})
}
// To-BE
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: (error, query) => {
if (query.meta.errorMessage) {
toast.error(query.meta.errorMessage)
}
},
}),
})
export function useTodos() {
return useQuery({
queryKey: ['todos', 'list'],
queryFn: fetchTodos,
meta: {
errorMessage: 'Failed to fetch todos',
},
})
}
//AS-IS
export function useTodos() {
const [todoCount, setTodoCount] = React.useState(0)
const { data: todos } = useQuery({
queryKey: ['todos', 'list'],
queryFn: fetchTodos,
//😭 please don't
onSuccess: (data) => {
setTodoCount(data.length)
},
})
return { todos, todoCount }
}
//TO-BE
export function useTodos() {
const { data: todos } = useQuery({
queryKey: ['todos', 'list'],
queryFn: fetchTodos,
})
const todoCount = todos?.length ?? 0
return { todos, todoCount }
}
4) refetchInterval callback function only gets 'query' passed
refetchInterval, refechOnWindowFocus, refetchOnMount, refetchOnReconnect와 같은 훅들에서 더이상 data를 select하여 변환하지 못합니다. 기존에 변환된 데이터를 가져올때 발생하던 typing issue를 해결하기위해서 수정되었습니다. 하지만 여전히 query.state.date를 사용하여 데이터에 접근할수있기때문에 refetch한 후에 데이터변환을 시도하면됩니다.
- refetchInterval: number | false | ((data: TData | undefined, query: Query) => number | false | undefined)
+ refetchInterval: number | false | ((query: Query) => number | false | undefined)
5) 'remove' method has been removed from useQuery
remove는 기존에 옵저버에게 알리지않고 쿼리를 제거하는데 사용되는 메서드였으나, 쿼리가 아직 활성화되지 않은 상태에서 remove를 수행하는 것은 리렌더링시에 로딩상태를 불러오기때문에 합리적이지 못하다는 이유로 사라졌습니다. v5에서는 removeQueries 메서드를 통해 쿼리 삭제가 가능합니다.
const queryClient = useQueryClient();
const query = useQuery({ queryKey, queryFn });
- query.remove()
+ queryClient.removeQueries({ queryKey })
6) `isDataEqual` option has been removed from useQuery
기존에 isDataEqual 옵션은 이전데이터를 사용할지 새로운데이터를 사용할지에 대해 판단하는데 사용되었으나 structuralSharing 옵션과 비슷한 기능을 하기에 제거되었습니다.
import { replaceEqualDeep } from '@tanstack/react-query'
- isDataEqual: (oldData, newData) => customCheck(oldData, newData)
+ structuralSharing: (oldData, newData) => customCheck(oldData, newData) ? oldData : replaceEqualDeep(oldData, newData)
7) Supported Broswers
성능과 번들크기를위해 지원되는 브라우저 범위를 업데이트하였습니다.
Chrome >= 91
Firefox >= 90
Edge >= 91
Safari >= 15
iOS >= 15
opera >= 77
8) Rename `cacheTime` to `gcTime`
기존 cacheTime은 마치 "데이터가 캐시로 유지되는시간"이라는 뜻을 나타내는것처럼 보이지만 사실은 "쿼리가 사용되지않을때 캐시가 제거되기까지의 시간"입니다. 쿼리가 계속사용될때 cacheTime은 아무일도하지않다가 쿼리가 사용되지않는 순간부터 일정시간 이후에 garbage colletor가 수집해서 버려버리기 때문입니다. 따라서 gcTime으로 이름을 변경하여 가비지로 수집되기까지의 시간이라는 뜻을 더 잘 나타내게하였습니다.
const MINUTE = 1000 * 60;
const queryClient = new QueryClient({
defaultOptions: {
queries: {
- cacheTime: 10 * MINUTE, //AS-IS
+ gcTime: 10 * MINUTE, //TO-BE
},
},
})
9) `useErrorBoundary` option has been renamed to `throwOnError`
기존 리액트 훅의 use와 혼동되는것을 방지하기위해 useErrorBoundary 옵션의 이름이 throwErrors로 변경되었습니다.
10) TS: `Error` is now the default type for errors instead of `unknown`
에러의 기본타입이 unknown에서 Error로 변경되었습니다. 커스텀에러나 에러가 아닌 타입을 지정하고싶다면 다음과 같이 지정할수있습니다.
declare module '@tanstack/react-query' {
interface Register {
defaultError: AxiosError
}
}
const { error } = useQuery({ queryKey: ['groups'], queryFn: fetchGroups })
11) Removed `keepPreviousData` in favor of `placeholderData` idenify function
keepPreviousData와 isPreviousData 플래그는 placeholderData 혹은 isPlaceholderData플래그와 비슷한 기능을 하므로 제거되었습니다. 다만 몇가지 다른점은 placeholderData는 항상 success상태를 반환할것이며, dataUpdatedAt은 항상 0으로 유지될것이라는점입니다. 기존 keepPreviousData는 이전 쿼리의 상태를 나타냈지만, 에러 자체가 다음 쿼리로 공유되지는 않으므로 placeholderData의 동작(success로 반환되는것)을 그대로 사용하기로 하였습니다.
useQuery({
queryKey,
queryFn,
placeholderData: (previousData, previousQuery) => previousData, // identity function with the same behaviour as `keepPreviousData`
})
12) Window focus refetching no longer listends to the `focus` event
focus 이벤트 대신에 visibilityChange 이벤트를 전적으로 사용합니다. visibilityChange이벤트를 지원하는 브라우저만을 지원하고 있으며, 기존에 focus로 인한 이슈는 해결되었습니다.
13) Network status no longer relies on the `navigator.onLine` property
navigator.onLine은 chromium 기반 브라우저에서 제대로 작동하지않기때문에 online:true로 가정하고 온라인/오프라인 이벤트를 수신하였을때만 상태를 업데이트합니다. 이때 인터넷 연결없이도 작동이 가능한 서비스워커를 통해 로드되는 오프라인 앱에서는 오탐이 있을수 있습니다.
14) Removed custom `context` prop in favor of custom `queryClient` instance
v4에서는 모든 리액트 쿼리 훅에 custom context를 전달할수있는 기능을 도입했으나, 이는 리액트 전용 기능이므로 quert client를 직접 전달하게함으로써 모든 프레임워크에서 사용가능하도록 변경하였습니다.
import { queryClient } from './my-client'
const { data } = useQuery(
{
queryKey: ['users', id],
queryFn: () => fetch(...),
- context: customContext //AS-IS
},
+ queryClient, //To-BE
)
15) Infinite queries now need a `initialPageParam`
기존에는 pageParam으로 undefined를 지정할수도 있었지만 이는 query cache에 정의되지않은 상태로 존재한다는 허점이 있었습니다. v5에서는 명시적으로 initialPageParam을 지정하여 첫페이지의 pageParam을 정해주어야합니다.
useInfiniteQuery({
queryKey,
- queryFn: ({ pageParam = 0 }) => fetchSomething(pageParam), //AS-IS
+ queryFn: ({ pageParam }) => fetchSomething(pageParam), //TO-BE
+ initialPageParam: 0, //TO-BE
getNextPageParam: (lastPage) => lastPage.next,
})
16) No retries on the server
기존에 서버 요청 실패시 재시도 기본값은 3번이였으나 이제는 0으로 변경되었습니다. prefetch의 경우 항상 기본값이 0이였지만, suspense가 활성화된 쿼리는 직접 서버에서 실행할수있기때문에 재시도를 전혀 하지않는것으로 변경되었습니다.
4. Conclusion
이외에도 변경된 사항이 있으므로 자세한 내용은 원글을 참조하시길 바랍니다. v5로 마이그레이션 할때 codemod를 지원해주므로 같이 사용한다면 훨씬 빠르게 가능할것으로 보입니다. 1년전 v5 로드맵 발표이후 많은 우려와 논의가 이루어졌었는데요, 기존에 문제되던 이슈들이 많이해결되었고, '직관성'을 강조하는 철학이 담겨 더 탄탄한 Tanstack query가 되었다라는 생각이 들었습니다. v4 -> v5에서 엄청난 변화가 있다고 느껴지진않지만, DX가 향상된 확고한 API들은 더 매력적으로 보이는것같습니다. 긴글 읽어주셔서 감사합니다.
Reference.
https://tanstack.com/query/v5/docs/react/guides/migrating-to-v5#codemod
https://tanstack.com/blog/announcing-tanstack-query-v5
'FYI > FE' 카테고리의 다른 글
리액트에서 컴포넌트를 작성하는 기준 (Feat. SOLID원칙) (0) | 2024.01.25 |
---|---|
'unload' 이벤트는 이제 사용하지마세요 (0) | 2023.10.24 |