
얼마전, TanStack Query의 버전5가 정식 릴리즈 되었다. 주요 변경 사항에는 어떠한 것들이 있었는지 한번 알아보자.
참고로 이제 v5의 Typescript 최소 요구 버전은 4.7 / React의 최소 버전은 18.0 이다.
주요변경
단일 signature, 단일 객체 지원
useQuery와 몇 가지 함수들은 호출하는데 첫 번째 혹은, 두 번째 매개변수 등을 확인해야 하고 일관성이 없어 관리하기가 어려웠다.
이제는 객체 형식만 지원하도록 변경되었다.
- 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 })
- queryClient.isFetching(key, filters)
+ queryClient.isFetching({ queryKey, ...filters })
- queryClient.removeQueries(key, filters)
+ queryClient.removeQueries({ queryKey, ...filters })
- queryClient.resetQueries(key, filters, options)
+ queryClient.resetQueries({ queryKey, ...filters }, options)
- queryClient.cancelQueries(key, filters, options)
+ queryClient.cancelQueries({ queryKey, ...filters }, options)
- queryClient.invalidateQueries(key, filters, options)
+ queryClient.invalidateQueries({ queryKey, ...filters }, options)
...
...
...
queryClient의 getQueryData, getQueryState 에서는 argument가 쿼리키만 받도록 변경되었다.
- queryClient.getQueryData(queryKey, filters)
+ queryClient.getQueryData(queryKey)
- queryClient.getQueryState(queryKey, filters)
+ queryClient.getQueryState(queryKey)
useQuery (and QueryObserver) 콜백 제거
onSuccess, onError, onSettled가 Mutations 에는 그대로 있지만 Quries에서는 제거되었다고 한다. 이러한 결정이 된 비하인드 스토리는 여기서 확인해볼 수 있다.
짧게 정리해보면, onSuccess, onError, onSettled 가 대부분의 경우 유용하지 않고 충분히 다른 곳에서 데이터 조작을 할 수 있으며 그렇게 하길 가이드하고 있다.
refetchInterval
콜백 함수는 query
만 전달 받도록 변경
아래처럼 콜백이 호출되는 방식이 간소화 되어(refetchOnWindowFocus
, refetchOnMount
, refetchOnReconnect
도 마찬가지로) 일부 타이핑 문제가 해결되었다.
- refetchInterval: number | false | ((data: TData | undefined, query: Query) => number | false | undefined)
+ refetchInterval: number | false | ((query: Query) => number | false | undefined)
remove
메소드 제거
이전에는 observer에게 알리지 않고 쿼리 캐시에서 쿼리를 제거하는데에 remove
메소드를 사용했었다. 그러나 쿼리가 아직 활성화된 상태에서 이 작업을 수행하는 것은 다음 번 다시 렌더링할 때 하드 로딩 상태를 트리거할 뿐이므로 그다지 의미가 없었다.
그래도 쿼리를 제거해야 하는 경우 queryClient.removeQueries({queryKey: key})
를 사용하면 된다. 마찬가지로 객체 형식 인자이다.
const queryClient = useQueryClient();
const query = useQuery({ queryKey, queryFn });
- query.remove()
+ queryClient.removeQueries({ queryKey })
isDataEqual
옵션 제거
isDataEqual
옵션이 useQuery에서 제거되었다. 이전에는 이것을 사용해서 이전 데이터를 사용할지 새 데이터를 사용할지를 결정했는데, 대신 structuralSharing
으로 동일한 기능을 할 수 있다.
import { replaceEqualDeep } from '@tanstack/react-query'
- isDataEqual: (oldData, newData) => customCheck(oldData, newData)
+ structuralSharing: (oldData, newData) => customCheck(oldData, newData) ? oldData : replaceEqualDeep(oldData, newData)
cacheTime
이 gcTime
으로 이름 변경
대부분 cacheTime
을 잘못 알고 있다. "데이터가 캐시되는 시간" 인 것 같지만 이는 정확하지 않다. 쿼리가 계속 사용 중인 한 cacheTime
은 아무 일도 하지 않는다. 쿼리가 사용되지 않는 즉시 시작된다. 시간이 지나면 캐시가 커지는 것을 방지하기 위해 데이터가 "가비지 컬렉터" 가 작동한다.
"gc" 약어는 가비지 컬렉터 에서 따왔다고 한다.
const MINUTE = 1000 * 60;
const queryClient = new QueryClient({
defaultOptions: {
queries: {
- cacheTime: 10 * MINUTE,
+ gcTime: 10 * MINUTE,
},
},
})
useErrorBoundary
가 throwOnError
으로 이름 변경
프레임워크에 구애받지 않고, React의 hooks 접두사 "use" 및 "ErrorBoundary" 컴포넌트 이름과의 혼동을 피하기 위해, useErrorBoundary 이름이 throwOnError
로 변경되었다.
오류의 기본 타입이 unknown
대신 Error
로 변경
JavaScript에서는 무엇이든 throw
할 수 있지만 (그래서 이런 경우 unknown 타입이 된다.), 거의 항상 오류(또는 오류의 하위 클래스)가 throw 된다. 이번 변경으로 대부분의 경우 TypeScript에서 오류 필드로 작업하기가 더 쉬워졌다.
Error가 아닌 다른 것을 던지려면 이제 제네릭을 직접 설정해야 한다.
useQuery<number, string>({
queryKey: ['some-query'],
queryFn: async () => {
if (Math.random() > 0.5) {
throw 'some error'
}
return 42
},
})
custom context
가 제거되고 queryClient
instance 사용
v4에서는 모든 리액트 쿼리 hooks에 custom context
를 전달할 수 있는 기능을 도입했었다. 이를 통해 마이크로프론트엔드를 사용할 때 적절한 격리가 가능해졌다.
하지만 context
는 리액트 전용 기능이기 때문에 다른 프레임워크에서도 프레임워크에 구애받지 않고 동일한 기능을 사용할 수 있도록 아래처럼 queryClient
인스턴스를 사용할 수 있도록 수정했다.
import { queryClient } from './my-client'
const { data } = useQuery(
{
queryKey: ['users', id],
queryFn: () => fetch(...),
- context: customContext
},
+ queryClient,
)
새로운 dehydrate
API
dehydrate
시에 전달할 수 있는 옵션이 간소화되었다. Quries/Mutations를 dehydrate 동작을 얻으려면 () => false를 전달하면 된다.
- dehydrateMutations?: boolean
- dehydrateQueries?: boolean
Infinite queries에 initialPageParam
추가
이전에는 정의되지 않은 값을 페이지 파라미터로 queryFn에 전달했고, queryFn 함수 시그니처의 페이지 파라미터에 기본값을 할당할 수 있었다. 이 경우 직렬화할 수 없는 쿼리 캐시에 정의되지 않은 상태로 저장된다는 단점이 있었다.
대신 이제 infiniteQuery 옵션에 명시적인 initialPageParam을 전달하는 것으로 변경되었다. 이 값은 첫 번째 페이지의 pageParam으로 사용된다.
useInfiniteQuery({
queryKey,
- queryFn: ({ pageParam = 0 }) => fetchSomething(pageParam),
+ queryFn: ({ pageParam }) => fetchSomething(pageParam),
+ initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.next,
})
infinite queries 위한 매뉴얼 모드 제거
이전에는 페이지 파라미터 값을 fetchNextPage 또는 fetchPreviousPage에 직접 전달하여 getNextPageParam 또는 getPreviousPageParam에서 반환되는 페이지 파라미터를 덮어쓰는 것이 허용되었다. 이 기능은 refetch에서는 전혀 작동하지 않았고 널리 알려지거나 사용되지 않았다.
이제 getNextPageParam 또는 getPreviousPageParam에서 null을 반환하면 더 이상 사용할 수 있는 페이지가 없음을 나타낼 수 있다.
loading, pending 이름 변경
status: loading이 status: pending으로, isLoading이 isPending으로, isInitialLoading이 isLoading으로 이름이 변경되었다.
mutation 경우에도 상태가 loading
에서 pending
으로 변경되었으며, isLoading
플래그가 isPending
으로 변경되었다.
queries에 isPending && isFetching으로 구현되는 새로운 파생된 isLoading 플래그가 추가되었다. 즉, isLoading과 isInitialLoading은 동일한 기능을 수행하지만, 더 이상 사용되지 않으며 다음 메이저 버전에서 제거될 예정이라고 한다.
이렇게 변경이 된 히스토리는 여기서 확인해 볼 수 있다.
정리해보면, loading
이라는 단어가 어떠한 두가지 의미가 혼재되어 있어 혼란스럽기 때문에 변경하고 싶었던 것 같다.
hashQueryKey
가 hashKey
로 이름 변경
hashQueryKey
가 hashKey
로 이름이 변경되었다. 이는 mutation키도 해시하고 mutation을 전달하는 useIsMutating 및 useMutationState의 함수 내부에서도 사용할 수 있기 때문이다.
contextSharing
제거
이전에는 contextSharing 프로퍼티를 사용하여 창 전체에서 쿼리 클라이언트 컨텍스트의 첫 번째 인스턴스를 공유할 수 있었다. 이렇게 하면 여러 번들 또는 마이크로프론트엔드에서 TanStack Query를 사용하는 경우 모듈 범위와 관계없이 모두 동일한 컨텍스트 인스턴스를 사용할 수 있었는데,
v4에서는 cutstom 컨텍스트를 QueryClientProvider에 전달할 수 있는 옵션이 추가되어 바로 이 기능을 사용할 수 있다. 애플리케이션의 여러 패키지에서 동일한 쿼리 클라이언트를 사용하려면 애플리케이션에서 QueryClient를 생성한 다음 번들에서 QueryClientProvider의 컨텍스트 속성을 통해 이를 공유하도록 할 수 있다.
Hydration API 변경
Hydrate 컴포넌트의 이름이 HydrationBoundary로 변경되었고 useHydrate 훅이 제거되었다. HydrationBoundary는 더 이상 mutations를 hydrate하지 않고 queries만 hydrate 하게 된다.
이에 대한 기술적으로 중요한 변경 사항으로 Tanstack Query 문서에는 다음 내용을 기술하고 있다.
queries가 hydrate 되는 타이밍이 약간 변경되었는데 새 쿼리는 여전히 렌더링 단계에서 hydrate되어 SSR이 정상적으로 작동하지만, 캐시에 이미 존재하는 쿼리는 이펙트에서 대신 hydrate 된다(해당 데이터가 캐시에 있는 것보다 최신 데이터인 경우). 일반적인 경우처럼 애플리케이션을 시작할 때 한 번만 하이드레이션하는 경우에는 영향을 받지 않지만, 서버 컴포넌트를 사용하고 페이지 탐색에서 하이드레이션을 위해 새로운 데이터를 전달하는 경우에는 페이지가 즉시 다시 렌더링되기 전에 이전 데이터가 깜박이는 것을 확인할 수 있다.
이는 페이지 전환이 완전히 커밋되기 전에 기존 페이지의 콘텐츠를 조기에 업데이트하지 않도록 하기 위해 적용되었다.
- import { Hydrate } from '@tanstack/react-query'
+ import { HydrationBoundary } from '@tanstack/react-query'
- <Hydrate state={dehydratedState}>
+ <HydrationBoundary state={dehydratedState}>
<App />
- </Hydrate>
+ </HydrationBoundary>
New Features
단순화 된 낙관적 업데이트 방식
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>
)
}
여기서는 데이터를 캐시에 직접 쓰는 대신 mutation이 실행 중일 때 UI가 표시되는 방식만 변경한다. 이 방법은 낙관적 업데이트를 표시해야 하는 곳이 한 곳뿐인 경우에 가장 효과적일 수 있다.
Infinite Queries 새로운 maxPages 옵션
모두 알다시피 infinite queries는 무한 스크롤이나 페이지네이션이 필요할 때 유용한데, 하지만 더 많은 페이지를 가져올수록 더 많은 메모리를 사용하게 되며, 모든 페이지를 순차적으로 다시 가져오기 때문에 쿼리 refetching 과정도 느려지게 된다.
v5에서는 infinite queries를 위한 새로운 maxPages 옵션이 추가되어 개발자가 쿼리 데이터에 저장되고 이후에 다시 불러오는 페이지의 수를 제한할 수 있다. 제공하려는 UX 및 refetching 성능에 따라 maxPages 값을 조정할 수 있는 것 이다. 이는 양방향이어야 하므로 getNextPageParam과 getPreviousPageParam을 모두 정의해야 한다는 것을 주의해야 한다!
Infinite Queries의 멀티 페이지 prefetch
infinite queries는 일반 쿼리처럼 prefetching 할 수 있다. 기본적으로 쿼리의 첫 번째 페이지만 prefetch되며 지정된 QueryKey 아래에 저장된다. 두 개 이상의 페이지를 미리 가져오려면 페이지 옵션을 사용하면 된다.
Suspense
v5에서는 데이터 fetching을 위한 Suspense가 안정화 되었다. useSuspenseQuery
, useSuspenseInfiniteQuery
, useSuspenseQueries
hooks가 새롭게 추가되었다.
const { data: post } = useSuspenseQuery({
// ^? const post: Post
queryKey: ['post', postId],
queryFn: () => fetchPost(postId),
})
참고
'Frontend' 카테고리의 다른 글
Artillery 서버 부하테스트 오픈소스 알아보기(1) (0) | 2024.03.30 |
---|---|
[Tanstack-Query] 핵심 로직 딥다이브 (0) | 2024.01.07 |
나는 웹 성능 지표를 잘 알고 있었나? (0) | 2023.06.04 |
그래서 모노레포가 뭐지? (feat. yarn workspace) (0) | 2023.04.09 |
모듈 시스템의 역사와 모듈 번들러 알아보기 (0) | 2023.03.12 |
- 주요변경
- 단일 signature, 단일 객체 지원
- useQuery (and QueryObserver) 콜백 제거
- refetchInterval 콜백 함수는 query만 전달 받도록 변경
- remove메소드 제거
- isDataEqual 옵션 제거
- cacheTime이 gcTime으로 이름 변경
- useErrorBoundary가 throwOnError으로 이름 변경
- 오류의 기본 타입이 unknown 대신 Error로 변경
- custom context 가 제거되고 queryClient instance 사용
- 새로운 dehydrate API
- Infinite queries에 initialPageParam 추가
- infinite queries 위한 매뉴얼 모드 제거
- loading, pending 이름 변경
- hashQueryKey가 hashKey로 이름 변경
- contextSharing 제거
- Hydration API 변경
- 단순화 된 낙관적 업데이트 방식
- Infinite Queries 새로운 maxPages 옵션
- Infinite Queries의 멀티 페이지 prefetch
- Suspense