Query는 data fetching으로 받은 비동기 데이터이다.(GET)
Mutation은 그 데이터를 업데이트하는 액션이다.(POST, PUT, DELETE)
Mutation이 끝나면 대부분 Query에도 영향을 줄 것이다.
예를 들어, `issue`데이터를 업데이트 하면 상위 개념인 `issues` 데이터에도 영향을 준다.
따라서 React Query가 Mutation과 Query를 연결하지 않는다는 사실에 놀랄 수도 있다.
그 이유는 꽤 단순하다.
React Query는 개발자에게 데이터를 맘대로 관리할 수 있도록 자유를 준 것이다.
모든 상황에서 Mutation 이후 re-fetching(invalidation)이 필수는 아니다.
예를 들어 Mutation api가 업데이트된 데이터를 반환해준다면, 그 데이터를 Query cache에 수동으로 넣어주는 경우가 있다.(추가적인 Query 요청을 피하기 위해)
invalidation을 수행하는 방식에는 여러가지가 있다.
- onSuccess나 onSettled에서 invalidation하기
- invalidation을 기다리기 (await) -> invalidation을 기다린다면 refetch가 끝날때까지 mutation을 pending 상태로 유지한다. 예를 들어 만약 폼 제출 처럼 disabled 상태를 유지해하는 경우에 사용할 수 있다. 다만 일단 빨리 이동하고 나중에 refetch된 데이터를 보여주고 싶을땐 await을 할 필요가 없다.
- ..등등 비즈니스 로직에 따라 invalidation 방식은 여러가지일 것이다.
이처럼 모든 것을 충족하는 솔루션은 없기 때문에, React Query는 정형화된 invalidation 방식을 제공하지 않는다.
다만 직접 자동 invalidation 솔루션을 만들어보고 싶다면, global cache callbacks를 사용할 수 있다.
The Global Cache Callbacks
Mutation은 callback들을 가지고 있다. - `onSuccess`, `onError`, `onSettled`. 각각의 useMutation에서 선언해서 사용한다.
또한 `MutationCache`에도 동일한 callback들이 존재한다.
QueryClient를 생성할때, MutationCache을 생성하고 callback을 정의할 수 있다.
import { QueryClient, MutationCache } from '@tanstack/react-query'
const queryClient = new QueryClient({
mutationCache: new MutationCache({
onSuccess,
onError,
onSettled,
}),
})
앱에는 MutationCache가 "only one" 이므로, 저 callback들은 "global"이며, 모든 Mutation에 대해 호출된다.
더하여 저 callback들은 useMutation과 동일한 인자를 받고, 마지막 인자로 Mutation 인스턴스도 받을 수 있다.
위 콜백은 모든 Mutation에 대해 반응하는 콜백이기 때문에, 내부에서 invalidation을 한다면?
const queryClient = new QueryClient({
mutationCache: new MutationCache({
onSuccess: () => {
queryClient.invalidateQueries()
},
}),
})
모든 Mutation이 끝난 후 모든 Query를 invalidation하게 된다.
근데 너무 과하지 않나요?
그럴 수도 있고, 아닐 수도 있다.
여기서 알아둬야할 것은 invalidation이 항상 refetch와 동일한 것은 아니라는 점이다.
invalidation은 넘겨받은 key에 해당하는 모든 active(화면에 보이는 것) Query를 refetch하고, 나머지 Query들은 stale(not fresh)로 표시하여 다음에 사용할 때 refetch할 수 있도록 한다.
이 방식은 꽤 괜찮다.
필터가 있는 issue list를 생각해보자. 각 필터 값은 Query Key의 일부가 되어야하므로, 필터 값이 바뀌면 필터링이 적용된 모든 Query를 요청해서 cache에 넣어줄 것이다. 하지만 우리가 보는(active) Query는 한 가지일 것이며, 모든 Query를 refetch하는 것은 불필요하다.
따라서 invalidation은 현재 화면에 표시되는 active Queries만 refetch하고, 그 외 모든 Query들은 필요할 때 refetch한다.
invalidation과 특정 Query들을 연결해놓기
현재 모든 Mutation 뒤에 모든 Query invalidation이 일어나고 있다. 즉 이런 불평이 있을 수 있다.
목록에 `issue`를 추가하는데 왜 `profile` 데이터를 invalidate 하나요...? 불편하네요🤨
장단점이 있다.
모든 데이터를 invalidate 해서 요청이 많아지는 문제가 있을 수 있지만,
장점은 코드가 간단하고, 직접 invalidate(이하 fine-grained revalidation) 하다가 놓치는 Query refetch가 있는 것 보다는 낫다.
fine-grained revalidation는 Mutation 후 어떤 Query를 refetch 해야하는지 정확히 알고 있고있는 경우에 유용하다.
fine-grained revalidation을 사용한다면 새로운 자원이 추가될 때 마다 모든 mutation callback을 살펴보면서 해당 자원을 invalidation 해야할지 판단해야할 것이다. 이는 매우 번거롭고 에러가 발생하기 쉽다.
위 생각을 토대로 저는 Queries에 2분 정도 중간 크기의 staleTime을 사용해서 불필요한 사용자 인터렉션으로 인한 invalidation의 영향을 무시했습니다.
제가 과거에 사용했던 몇 가지 revalidation 테크닉들을 소개합니다.
mutationKey를 사용해 어떤 Queries를 invalidation할지 지정하기
MutationKey와 QueryKey는 보통 서로 관련이 없다. 특히 MutationKey는 optional한 설정이다.
하지만 MutationKey에 invalidation할 Query들의 Key를 배열형태로 선언해두면 편하게 invalidation할 수 있다.
const queryClient = new QueryClient({
mutationCache: new MutationCache({
onSuccess: (_data, _variables, _context, mutation) => {
queryClient.invalidateQueries({
queryKey: mutation.options.mutationKey,
})
},
}),
})
전역 콜백 mutationCache에 이렇게 선언해두고 각 Mutation에서 mutationKey: ['issues']와 같이 선언해두면 mutate 후 issues Query들을 invalidation 시킬 수 있다.
만약 mutationKey에 아무것도 넘기지 않는다면 모든 Query를 invalidate 시킬 것이다.
따라서 이 테크닉을 이용하면 mutation마다 fine-grained invalidation와 전체 invalidation를 선택적으로 할 수 있다. (굿)
staleTime에 따라 Queries 제외하기
한번 fetch한 데이터 계속 사용해도 될 때, Query에 `statleTime: Infinity` 옵션을 줘서 "static" Query를 만들 수 있다.
이 Query들이 invalidation 되는 것을 원하지 않는 경우 `predicate` 옵션을 이용해 특정 조건을 만족하는 Query만 invalidation 되게 할 수 있다.
const queryClient = new QueryClient({
mutationCache: new MutationCache({
onSuccess: (_data, _variables, _context, mutation) => {
const nonStaticQueries = (query: Query) => {
const defaultStaleTime =
queryClient.getQueryDefaults(query.queryKey).staleTime ?? 0
// Query들의 모든 staleTime을 가져온다
const staleTimes = query.observers
.map((observer) => observer.options.staleTime)
.filter((staleTime) => staleTime !== undefined)
const staleTime =
query.getObserversCount() > 0
? Math.min(...staleTimes)
: defaultStaleTime
return staleTime !== Number.POSITIVE_INFINITY
}
queryClient.invalidateQueries({
queryKey: mutation.options.mutationKey,
predicate: nonStaticQueries, // nonStaticQueries 필터링을 통과한 Query들만 invalidate
})
},
}),
})
meta 옵션 사용하기
Mutation에서 meta 옵션을 이용해 임의의 정보를 저장해둘 수 있다.
meta 객체 내부에 invalidates 속성 안에 queryKey들을 저장해두고, predicate 필터에서 저 queryKey들을 이용해보자.
import { matchQuery } from '@tanstack/react-query'
const queryClient = new QueryClient({
mutationCache: new MutationCache({
onSuccess: (_data, _variables, _context, mutation) => {
queryClient.invalidateQueries({
predicate: (query) =>
// invalidate all matching tags at once
// or everything if no meta is provided
mutation.meta?.invalidates?.some((queryKey) =>
matchQuery({ queryKey }, query)
) ?? true,
})
},
}),
})
// usage:
useMutation({
mutationFn: updateLabel,
meta: {
invalidates: [['issues'], ['labels']],
},
})
predicate에서 서비스에 존재하는 모든 Query들을 받아서 meta에 명시한 queryKey와 일치하는(matchQuery) Query들만 invalidate 해주고 있다.
이는 useMutation에서 invalidate 해주는 것과 다를 바 없어 보이지만, QueryClient를 불러오기 위해 useQueryClient를 매번 import 해주지 않아도 된다는 장점이 있다.
또한 meta를 선언해두지 않은 Mutation들은 기본적으로 모든 Query들을 invalidate 하기 때문에, invalidate 방식을 선택적으로 가져갈 수 있다
meta 옵션의 타입 선언하기
meta의 기본 타입은 Record<string, unknown> 이지만, module augmentation을 이용해 타입을 덮어쓸 수 있다.
// react-query.d.ts declare module '@tanstack/react-query' { interface Register { mutationMeta: { invalidates?: Array<QueryKey> } } }
meta 타입 선언에 대한 더 자세한 내용은 공식문서 참고
모든 케이스를 유연하게 커버하면서 비대하지 않은 API를 구현하는건 쉽지 않은 일이다.
때문에 react-query에서도 invalidation 자동화를 구현해놓지 않았다.
그런 이유로, 나는 사용자 쪽에서 필요한 기능을 구현하는 방법을 제시하는 걸 선호한다.
위 글은 https://tkdodo.eu/blog/automatic-query-invalidation-after-mutations 를 보며 나름대로 정리한 글입니다.
'React Query' 카테고리의 다른 글
8. 타입 안전한 React Query (0) | 2024.08.06 |
---|---|
7. Query Options API (0) | 2024.07.31 |
6. React Query가 필요한 이유 (0) | 2024.07.21 |
5. React Query의 Status (0) | 2024.02.21 |
4. 효율적으로 React Query Key 선언하기 (5) | 2024.02.14 |