Skip to content

Files

Latest commit

 

History

History
510 lines (386 loc) ยท 20.5 KB

File metadata and controls

510 lines (386 loc) ยท 20.5 KB

๐Ÿ’ป Migrating to TanStack Query(React) v5

๐Ÿ“„ ์ฃผ์š” ๋ณ€๋™ ์‚ฌํ•ญ (โญ๏ธ ์ค‘์š”)

1. โญ๏ธ Supports a single signature, one object

  • useQuery, useInfiniteQuery, useMutation์ด ์ด์ œ๋Š” ๊ฐ์ฒด ํ˜•์‹๋งŒ ์ง€์›ํ•˜๋„๋ก ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
  • v4์—์„œ๋Š” useQuery(key, fn, options), useQuery({ queryKey, queryFn, ...options }) ๋‘ ํ˜•ํƒœ๋ฅผ ๋ชจ๋‘ ์ง€์›ํ–ˆ๋Š”๋ฐ ์ด๋Š” ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ํž˜๋“ค๊ณ , ๋งค๊ฐœ ๋ณ€์ˆ˜ ํƒ€์ž…์„ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•œ ๋Ÿฐํƒ€์ž„ ๊ฒ€์‚ฌ๋„ ํ•„์š”ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— ์˜ค๋กœ์ง€ ๊ฐ์ฒด ํ˜•์‹๋งŒ ์ง€์›ํ•˜๋„๋ก v5์—์„œ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
- 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.ensureQueryData(key, filters)
+ queryClient.ensureQueryData({ queryKey, ...filters })
- queryClient.getQueriesData(key, filters)
+ queryClient.getQueriesData({ queryKey, ...filters })
- queryClient.setQueriesData(key, updater, filters, options)
+ queryClient.setQueriesData({ queryKey, ...filters }, updater, options)
- 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.refetchQueries(key, filters, options)
+ queryClient.refetchQueries({ queryKey, ...filters }, options)
- queryClient.fetchQuery(key, fn, options)
+ queryClient.fetchQuery({ queryKey, queryFn, ...options })
- queryClient.prefetchQuery(key, fn, options)
+ queryClient.prefetchQuery({ queryKey, queryFn, ...options })
- queryClient.fetchInfiniteQuery(key, fn, options)
+ queryClient.fetchInfiniteQuery({ queryKey, queryFn, ...options })
- queryClient.prefetchInfiniteQuery(key, fn, options)
+ queryClient.prefetchInfiniteQuery({ queryKey, queryFn, ...options })
- queryCache.find(key, filters)
+ queryCache.find({ queryKey, ...filters })
- queryCache.findAll(key, filters)
+ queryCache.findAll({ queryKey, ...filters })

2. โญ๏ธ 'queryClient.getQueryData', 'queryClient.getQueryState' now accepts queryKey only as an Argument

  • queryClient.getQueryData์˜ ์ธ์ˆ˜๊ฐ€ queryKey๋งŒ ๋ฐ›๋„๋ก v5์—์„œ ์ˆ˜์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
- queryClient.getQueryData(queryKey, filters)
+ queryClient.getQueryData(queryKey)
  • ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ queryClient.getQueryState๋„ ์ธ์ˆ˜๊ฐ€ queryKey๋งŒ ๋ฐ›๋„๋ก v5์—์„œ ์ˆ˜์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
- queryClient.getQueryState(queryKey, filters)
+ queryClient.getQueryState(queryKey)

3. โญ๏ธ Callbacks on useQuery (and QueryObserver) have been removed

  • useQuery์˜ ์˜ต์…˜์ธ onSuccess, onError, onSettled๊ฐ€ ์ œ๊ฑฐ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
    • ํ•ด๋‹น ์ฝœ๋ฐฑ ํ•จ์ˆ˜๋“ค์€ ๊ฐ„๋‹จํ•˜๊ณ , ์ง๊ด€์ ์ด๋ผ ๊ต‰์žฅํžˆ ์œ ์šฉํ•˜์ง€๋งŒ ๋ฒ„๊ทธ๋ฅผ ์œ ๋ฐœ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์ž์„ธํ•œ ๋‚ด์šฉ์€ Tanstack Query maintainer์ธ tkdodo ๋ธ”๋กœ๊ทธ ํฌ์ŠคํŒ…์„ ์ฐธ๊ณ ํ•ด์ฃผ์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค.

4. The 'remove' method has been removed from useQuery

  • useQuery์˜ remove ๋ฉ”์„œ๋“œ๊ฐ€ ์ œ๊ฑฐ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ด์ „์—๋Š” remove ๋ฉ”์„œ๋“œ๋Š” observer์—๊ฒŒ ์•Œ๋ฆฌ์ง€ ์•Š๊ณ  ์ฟผ๋ฆฌ๋ฅผ queryCache์—์„œ ์ œ๊ฑฐํ•˜๋Š”๋ฐ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

    • ์˜ˆ๋ฅผ ๋“ค์–ด, ์‚ฌ์šฉ์ž๋ฅผ ๋กœ๊ทธ์•„์›ƒ ํ•  ๋•Œ์™€ ๊ฐ™์ด ๋” ์ด์ƒ ํ•„์š”ํ•˜์ง€ ์•Š์€ ๋ฐ์ดํ„ฐ๋ฅผ ์ œ๊ฑฐํ•  ๋•Œ์™€ ๊ฐ™์€ ๊ฒฝ์šฐ์— ํ™œ์šฉ ํ•  ์ˆ˜ ์žˆ๋Š” ๋ฉ”์„œ๋“œ์˜€์Šต๋‹ˆ๋‹ค.
  • ํ•˜์ง€๋งŒ, query๊ฐ€ ์•„์ง ํ™œ์„ฑํ™”๋œ ์ƒํƒœ์—์„œ ์ด remove ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๊ฒƒ์€ ๋‹ค์Œ ๋ฒˆ ๋ฆฌ๋ Œ๋”๋ง ํ•  ๋•Œ hard loading ์ƒํƒœ๋ฅผ ํŠธ๋ฆฌ๊ฑฐํ•˜๊ธฐ ๋•Œ๋ฌธ์— ํ•ฉ๋ฆฌ์ ์ด์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค.

    • ์—ฌ๊ธฐ์„œ, hard loading ์ƒํƒœ๋ž€? ๋ฐ์ดํ„ฐ๊ฐ€ ์—†๋Š” ์ฆ‰, ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ๋•Œ ๋กœ๋”ฉ ์ƒํƒœ๋ฅผ ๋งํ•ฉ๋‹ˆ๋‹ค.
    • useQuery์—์„œ ์ž์ฃผ ์‚ฌ์šฉํ•˜๋Š” isLoading์ด ์ด๋Ÿฐ hard loading ์ƒํƒœ์ธ ๊ฒฝ์šฐ์—๋งŒ ์ฐธ(true)์ž…๋‹ˆ๋‹ค.
    • When we refetch a query, it doesn't set isLoading true.
  • ํ•˜์ง€๋งŒ!! ๊ทธ๋Ÿผ์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ  ์ฟผ๋ฆฌ๋ฅผ ์ œ๊ฑฐํ•ด์•ผ ๋œ๋‹ค๋ฉด queryClient.removeQueries๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

const queryClient = useQueryClient();
const query = useQuery({ queryKey, queryFn });
- query.remove()
+ queryClient.removeQueries({ queryKey })

5. The 'isDataEqual' option has been removed from useQuery

  • isDataEqual ํ•จ์ˆ˜๋Š” query์—์„œ resolved๋œ ๋ฐ์ดํ„ฐ๋กœ์„œ ์ด์ „ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ• ์ง€ ์•„๋‹ˆ๋ฉด ์ƒˆ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ• ์ง€ ํ™•์ธํ•˜๋Š”๋ฐ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.
  • ์ด์ œ๋Š” isDataEqual์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ , ๋™์ผํ•œ ๊ธฐ๋Šฅ์œผ๋กœ์„œ structuralSharing์œผ๋กœ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
import { replaceEqualDeep } from '@tanstack/react-query'

- isDataEqual: (oldData, newData) => customCheck(oldData, newData)
+ structuralSharing: (oldData, newData) => customCheck(oldData, newData) ? oldData : replaceEqualDeep(oldData, newData)

6. โญ๏ธ Rename 'cacheTime' to 'gcTime'

  • cacheTime์ด gcTime์œผ๋กœ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
    • ๋„ค์ด๋ฐ์ด ๋ณ€๊ฒฝ๋œ ์ด์œ ๋Š” ๋งŽ์€ ์‚ฌ๋žŒ๋“ค์ด cacheTime์„ ๋งˆ์น˜ "๋ฐ์ดํ„ฐ๊ฐ€ ์บ์‹œ๋˜๋Š” ์‹œ๊ฐ„"์œผ๋กœ ์ฐฉ๊ฐํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.
    • ํ•˜์ง€๋งŒ, ์‹ค์ œ๋กœ query๊ฐ€ ๊ณ„์† ์‚ฌ์šฉ๋˜๋Š” ํ•œcacheTime์€ ์•„๋ฌด ์ผ๋„ ํ•˜์ง€ ์•Š๊ณ , query๊ฐ€ ๋”์ด์ƒ ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” ์‹œ์ ์— ์‹œ์ž‘๋ฉ๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  cacheTime ์‹œ๊ฐ„์ด ์ง€๋‚˜๋ฉด ์บ์‹œ๊ฐ€ ๋”์ด์ƒ ์ปค์ง€๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ๋ฐ์ดํ„ฐ๋Š” garbage collected๋ฉ๋‹ˆ๋‹ค.
  • ๋”ฐ๋ผ์„œ, ์˜๋ฏธ์ƒ์˜ ํ˜ผ๋™์„ ์ค„์ด๊ธฐ ์œ„ํ•ด cacheTime์—์„œ gcTime์œผ๋กœ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
const MINUTE = 1000 * 60;

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

7. โญ๏ธ The 'useErrorBoundary' option has been renamed to 'throwOnError'

  • ๊ธฐ์กด์— ErrorBoundary์— ์—๋Ÿฌ๋ฅผ ๋˜์ง€๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ–ˆ๋˜ ์˜ต์…˜์ธ useErrorBoundary๋ฅผ ํŠน์ • ํ”„๋ ˆ์ž„์›Œํฌ์— ์ข…์†๋˜์ง€ ์•Š์œผ๋ฉด์„œ, ๋ฆฌ์•กํŠธ ์ปค์Šคํ…€ ํ›…์˜ ์ ‘๋ฏธ์‚ฌ์ธ use์™€ ErrorBoundary ์ปดํฌ๋„ŒํŠธ๋ช…๊ณผ ํ˜ผ๋™์„ ํ”ผํ•˜๊ธฐ ์œ„ํ•ด, throwOnError๋กœ ๋ณ€๊ฒฝ๋์Šต๋‹ˆ๋‹ค.
const todos = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
-  useErrorBoundary: true,
+  throwOnError: true,
})

8. โญ๏ธ TypeScript: 'Error' is now the default type for errors instead of 'unknown'

  • v5 ๋ถ€ํ„ฐ๋Š” error์˜ ๊ธฐ๋ณธ ํƒ€์ž…์ด Error ์ž…๋‹ˆ๋‹ค. ๋ณ€๊ฒฝ๋œ ์ด์œ ๋Š” ๋งŽ์€ ์‚ฌ์šฉ์ž๋“ค์ด ๊ธฐ๋Œ€ํ•˜๋Š” ๊ฒฐ๊ณผ์ด๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.
// const error: Error
const { error } = useQuery({
  queryKey: ["groups"],
  queryFn: fetchGroups,
});
  • ๋งŒ์•ฝ ์ปค์Šคํ…€ ์—๋Ÿฌ๋ฅผ ํ™œ์šฉํ•˜๊ฑฐ๋‚˜ Error๊ฐ€ ์•„๋‹Œ ๊ฒƒ์„ ํ™œ์šฉํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด, ์•„๋ž˜ ์˜ˆ์ œ์ฒ˜๋Ÿผ ํƒ€์ž…์„ ๊ตฌ์ฒดํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
// const error: string | null
const { error } = useQuery<Group[], string>({
  queryKey: ["groups"],
  queryFn: fetchGroups,
});

9. โญ๏ธ Removed 'keepPreviousData' in favor of 'placeholderData' identity function

  • โ€‹keepPreviousData ์˜ต์…˜๊ณผ isPreviousData ํ”Œ๋ž˜๊ทธ๊ฐ€ ์ œ๊ฑฐ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
    • ์™œ๋ƒํ•˜๋ฉด ์ด๋“ค์€ ๊ฐ๊ฐ placeholderData์™€ `isPlaceholderData ํ”Œ๋ž˜๊ทธ์™€ ๊ฑฐ์˜ ์œ ์‚ฌํ•˜๊ฒŒ ๋™์ž‘ํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.
  • ์•„๋ž˜ ์˜ˆ์ œ๋Š” placeholderData๋ฅผ ํ™œ์šฉํ•˜๋ฉด์„œ ์ด์ „์— keepPreviousData ์˜ต์…˜์„ true๋กœ ์คฌ์„๋•Œ์™€ ๋™์ผํ•œ ๊ธฐ๋Šฅ์„ ์ˆ˜ํ–‰ํ•˜๊ธฐ ์œ„ํ•ด identity function์„ ํ—ˆ์šฉํ•˜๋Š” placeholderData์— Tanstack Query์— ํฌํ•จ๋œ keepPreviousData ํ•จ์ˆ˜๋ฅผ ์ถ”๊ฐ€ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
import {
   useQuery,
+  keepPreviousData
} from "@tanstack/react-query";

const {
   data,
-  isPreviousData,
+  isPlaceholderData,
} = useQuery({
  queryKey,
  queryFn,
- keepPreviousData: true,
+ placeholderData: keepPreviousData
});
  • ๋˜๋Š”, ์ง์ ‘ identity function์„ ์ œ๊ณตํ•˜๋Š” ๋ฐฉ๋ฒ•๋„ ์žˆ์Šต๋‹ˆ๋‹ค.
useQuery({
  queryKey,
  queryFn,
  placeholderData: (previousData, previousQuery) => previousData,
  // identity function with the same behaviour as `keepPreviousData`
});
  • ์—ฌ๊ธฐ์„œ ์œ„ ๋ณ€๊ฒฝ์‚ฌํ•ญ์—๋Š” ๋ช‡ ๊ฐ€์ง€ ์ฃผ์˜์‚ฌํ•ญ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

    • placeholderData๋Š” ํ•ญ์ƒ success ์ƒํƒœ๋ฅผ ์œ ์ง€ํ•˜๋ฉฐ, keepPreviousData๋Š” ์ด์ „ ์ฟผ๋ฆฌ ์ƒํƒœ๋ฅผ ์•Œ๋ ค์ค๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ๋ฅผ ์„ฑ๊ณต์ ์œผ๋กœ ๊ฐ€์ ธ์˜จ ํ›„ background refetch error๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด placeholderData์˜ success ์ƒํƒœ๋Š” ์˜ค๋ฅ˜๋ผ๊ณ  ๋Š๋‚„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์—๋Ÿฌ ์ž์ฒด๊ฐ€ ๊ณต์œ ๋˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— placeholderData์˜ ๋™์ž‘์€ ๊ทธ๋Œ€๋กœ ์œ ์ง€ํ•˜๊ธฐ๋กœ ๊ฒฐ์ •๋์Šต๋‹ˆ๋‹ค.

    • keepPreviousData๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ๋Š” ์ด์ „ ๋ฐ์ดํ„ฐ์˜ dateUpdatedAt ํƒ€์ž„ ์Šคํƒฌํ”„๊ฐ€ ์ œ๊ณต๋˜์—ˆ๋Š”๋ฐ, placeholderData๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด dateUpdatedAt์€ 0์œผ๋กœ ์œ ์ง€๋ฉ๋‹ˆ๋‹ค.

    • ๋งŒ์•ฝ, ํƒ€์ž„์Šคํƒฌํ”„๋ฅผ ํ™”๋ฉด์— ๊ณ„์† ๋ณด์—ฌ์ฃผ๊ณ  ์‹ถ๋‹ค๋ฉด ์ด๋Ÿฐ ๋™์ž‘์ด ๋ถˆ๋งŒ์กฑ์Šค๋Ÿฌ์šธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ๋ฌธ์ œ๋Š” ์•„๋ž˜์™€ ๊ฐ™์ด useEffect๋ฅผ ํ™œ์šฉํ•˜๋ฉด ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

const [updatedAt, setUpdatedAt] = useState(0);

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

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

10. โญ๏ธ Window focus refetching no longer listens to the 'focus' event

  • Tanstack Query๋Š” visibilitychange ์ด๋ฒคํŠธ๋ฅผ ์ง€์›ํ•˜๋Š” ๋ธŒ๋ผ์šฐ์ €๋งŒ ์ง€์›ํ•˜๋„๋ก ๊ฒฐ์ •๋์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ, ์ด์ œ visibilitychange ์ด๋ฒคํŠธ๋งŒ ๋…์ ์ ์œผ๋กœ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.
  • ์ด๋ฅผ ํ†ตํ•ด ๋‹ค์Œ focus ๊ด€๋ จ ๋ฌธ์ œ๊ฐ€ ํ•ด๊ฒฐ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

11. Removed custom 'context' prop in favor of custom 'queryClient' instance

  • ์ปค์Šคํ…€ queryClient ์ธ์Šคํ„ด์Šค๋ฅผ ์œ„ํ•ด ์ปค์Šคํ…€ context prop์ด ์ œ๊ฑฐ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
  • ๊ธฐ์กด v4์—์„œ๋Š” context๋ฅผ ๋ชจ๋“  react query hooks์— ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์„ ์ œ๊ณตํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” MicroFrontends๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ ์ ์ ˆํ•˜๊ฒŒ ๊ฒฉ๋ฆฌํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ–ˆ์Šต๋‹ˆ๋‹ค.
  • ํ•˜์ง€๋งŒ, ๋‹ค๋“ค ์•Œ๋‹ค์‹ถ์ด context๋Š” ๋ฆฌ์•กํŠธ์—์„œ๋งŒ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๊ธฐ๋Šฅ์ž…๋‹ˆ๋‹ค. context๋Š” que`ryClient์— ๋Œ€ํ•œ ์ ‘๊ทผ ๊ถŒํ•œ์„ ์ฃผ๋Š” ์—ญํ• ์„ ํ•  ๋ฟ์ž…๋‹ˆ๋‹ค.
  • v5์—์„œ๋Š” ์œ„์™€ ๋™์ผํ•œ ๊ธฐ๋Šฅ์„ ์•„๋ž˜ ์˜ˆ์ œ์ฒ˜๋Ÿผ ์ปค์Šคํ…€ queyClient๋ฅผ ์ง์ ‘ ์ „๋‹ฌํ•จ์œผ๋กœ์จ ํ•ด๊ฒฐํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด์ œ๋Š” ์–ด๋–ค ๋‹ค๋ฅธ ํ”„๋ ˆ์ž„์›Œํฌ์— ๊ตฌ์• ๋ฐ›์ง€ ์•Š๊ณ  ๋™์ผํ•œ ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
import { queryClient } from './my-client'

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

12. โญ๏ธ Removed 'refetchPage' in favor of 'maxPages'

  • maxPages๋ฅผ ์œ„ํ•ด refetchpage๋ฅผ ์ œ๊ฑฐํ•˜์˜€์Šต๋‹ˆ๋‹ค.
  • v4์—์„œ๋Š” refetchPage ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ infinite queries์— ๋Œ€ํ•ด refresh ํ•  ํŽ˜์ด์ง€๋ฅผ ์ •์˜ํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ–ˆ์Šต๋‹ˆ๋‹ค.
  • ๊ทธ๋Ÿฌ๋‚˜ ๋ชจ๋“  ํŽ˜์ด์ง€๋ฅผ refresh ํ•˜๋ฉด UI ๋ถˆ์ผ์น˜๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋˜ํ•œ ์ด ์˜ต์…˜์€ queryClient.refetchQueries์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์ง€๋งŒ nomal queries๊ฐ€ ์•„๋‹Œ infinite queries์—๋Œ€ํ•ด์„œ๋งŒ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.
  • v5์—์„œ๋Š” query data๋ฅผ ์ €์žฅํ•˜๊ณ , ๋‹ค์‹œ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋Š” ํŽ˜์ด์ง€ ์ˆ˜๋ฅผ ์ œํ•œํ•˜๋Š” infinite queries๋ฅผ ์œ„ํ•œ ์ƒˆ๋กœ์šด maxPages ์˜ต์…˜์ด ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.
useInfiniteQuery({
  queryKey: ["projects"],
  queryFn: fetchProjects,
  initialPageParam: 0,
  getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
  getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,
  maxPages: 3,
});
  • infinite queries๋Š” ๋งŽ์€ ํŽ˜์ด์ง€๋ฅผ ๊ฐ€์ ธ์˜ฌ์ˆ˜๋ก ๋” ๋งŽ์€ ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๊ฒŒ ๋˜๋ฉฐ, ๋ชจ๋“  ํŽ˜์ด์ง€๋ฅผ ์ˆœ์ฐจ์ ์œผ๋กœ ๋‹ค์‹œ ๊ฐ€์ ธ์˜ค๊ธฐ ๋•Œ๋ฌธ์— query refetching ํ”„๋กœ์„ธ์Šค๋„ ๋Š๋ ค์ง‘๋‹ˆ๋‹ค.
  • maxPages๋ฅผ ํ™œ์šฉํ•˜๋ฉด ํŽ˜์ด์ง€ ์ˆ˜๋ฅผ ์ œํ•œํ•˜๊ณ  ์ดํ›„์— ๋‹ค์‹œ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์ด๋Ÿฐ ๋‹จ์ ์„ ๋ณด์™„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ฐธ๊ณ ๋กœ infinite list๋Š” ์–‘๋ฐฉํ–ฅ์ด์—ฌ์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์œ„ ์˜ˆ์ œ์ฒ˜๋Ÿผ getNextPageParam๊ณผ getPreviousPageParam์„ ๋ชจ๋‘ ์ •์˜ํ•ด์•ผ ํ•œ๋‹ค๋Š” ์ ์„ ์ฃผ์˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

13. โญ๏ธ infinite queries now need a 'initialPageParam'

  • ์ด์ „์—๋Š” undefined ๊ฐ’์„ ๊ฐ€์ง„ pageParam์„ queryFn์— ์ „๋‹ฌํ–ˆ๊ณ , queryFn์—์„œ pageParam์— ๋Œ€ํ•œ ๊ธฐ๋ณธ ๊ฐ’์„ ์ •์˜ํ–ˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ด๋Ÿฐ ๊ฒฝ์šฐ ์ง๋ ฌํ™” ํ•  ์ˆ˜ ์—†๋Š” ์ฟผ๋ฆฌ ์บ์‹œ์— undefined์ธ ์ƒํƒœ๋กœ ์ €์žฅ๋œ๋‹ค๋Š” ๋‹จ์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค.
  • v5๋ถ€ํ„ฐ๋Š” ์•„๋ž˜ ์˜ˆ์ œ์ฒ˜๋Ÿผ infinite Query ์˜ต์…˜์— ๋ช…์‹œ์ ์ธ initialPageParam์„ ์ „๋‹ฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
useInfiniteQuery({
   queryKey,
-  queryFn: ({ pageParam = 0 }) => fetchSomething(pageParam),
+  queryFn: ({ pageParam }) => fetchSomething(pageParam),
+  initialPageParam: 0,
   getNextPageParam: (lastPage) => lastPage.next,
})

14. Manual mode for infinite queries has been removed

  • ์ด์ „์—๋Š” ์•„๋ž˜ ์˜ˆ์ œ์ฒ˜๋Ÿผ pageParams ๊ฐ’์„ ์ˆ˜๋™์ ์œผ๋กœ fetchNextPage ๋˜๋Š” fetchPreviousPage์— ์ง์ ‘ ์ „๋‹ฌํ•˜์—ฌ getNextPageParam ๋˜๋Š” getPreviousPageParam์—์„œ ๋ฐ˜ํ™˜๋˜๋Š” pageParam๋ฅผ ๋ฎ์–ด์“ฐ๋Š” ๊ฒƒ์ด ํ—ˆ์šฉ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
// v4
function Projects() {
  const fetchProjects = ({ pageParam = 0 }) =>
    fetch("/api/projects?cursor=" + pageParam);

  const {
    status,
    data,
    isFetching,
    isFetchingNextPage,
    fetchNextPage,
    hasNextPage,
  } = useInfiniteQuery({
    queryKey: ["projects"],
    queryFn: fetchProjects,
    getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
  });

  // Pass your own page param
  const skipToCursor50 = () => fetchNextPage({ pageParam: 50 });
}
  • ํ•˜์ง€๋งŒ ์ด pageParam์„ ๋ฎ์–ด์“ฐ๋Š” ๊ธฐ๋Šฅ์€ refetch์—์„œ๋Š” ์ „ํ˜€ ์ž‘๋™ํ•˜์ง€ ์•Š์•˜๊ณ , ๋งŽ์€ ์‚ฌ๋žŒ๋“ค์ด ์‚ฌ์šฉํ•˜๋Š” ๊ธฐ๋Šฅ์ด ์•„๋‹ˆ์˜€์Šต๋‹ˆ๋‹ค. ์ฆ‰, infinite queries์—์„œ getNextPageParam์ด ํ•„์ˆ˜์ ์ž„์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.

15. โญ๏ธ Returning 'null' from 'getNextPageParam' or 'getPreviousPageParam' now indicates that there is no further page available

  • v4์—์„œ๋Š” ๋” ์ด์ƒ ํŽ˜์ด์ง€ ์—†์Œ์„ ๋‚˜ํƒ€๋‚ด๊ธฐ ์œ„ํ•ด ๋ช…์‹œ์ ์œผ๋กœ undefined๋ฅผ ๋ฐ˜ํ™˜ํ•ด์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค. v5๋ถ€ํ„ฐ๋Š” undefined ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ null๊นŒ์ง€ ํฌํ•จํ•˜๋„๋ก ํ™•์žฅ๋์Šต๋‹ˆ๋‹ค.
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) =>
  TPageParam | undefined | null;

getPreviousPageParam: (firstPage, allPages, firstPageParam, allPageParams) =>
  TPageParam | undefined | null;

16. No retries on the server

  • ์„œ๋ฒ„์—์„œ retry์˜ ๊ธฐ๋ณธ ๊ฐ’์€ 3์ด ์•„๋‹Œ 0์ž…๋‹ˆ๋‹ค.
  • prefetching์˜ ๊ฒฝ์šฐ ํ•ญ์ƒ ๊ธฐ๋ณธ๊ฐ’์ด 0์ด์˜€์ง€๋งŒ, suspense๊ฐ€ ํ™œ์„ฑํ™”๋œ ์ฟผ๋ฆฌ๋Š” ์ด์ œ ์„œ๋ฒ„์—์„œ๋„ ์ง์ ‘ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์—(React v18 ์ดํ›„) ์„œ๋ฒ„์—์„œ ์žฌ์‹œ๋„๋ฅผ ์ „ํ˜€ ํ•˜์ง€ ์•Š๋„๋ก ํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.

17. โญ๏ธ 'status: loading' has been changed to 'status: pending' and 'isLoading' has been changed to 'isPending' and 'isInitialLoading' has now been renamed to 'isLoading'

  • loading ์˜ต์…˜์ด pending์œผ๋กœ ๋ณ€๊ฒฝ๋˜์—ˆ์œผ๋ฉฐ, ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ isLoading ํ”Œ๋ž˜๊ทธ๊ฐ€ isPending์œผ๋กœ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
isPending: boolean;
// A derived boolean from the status variable above, provided for convenience.
isSuccess: boolean;
// A derived boolean from the status variable above, provided for convenience.
isError: boolean;
// A derived boolean from the status variable above, provided for convenience.
  • mutation์˜ ๊ฒฝ์šฐ์—๋„ isLoading ํ”Œ๋ž˜๊ทธ๊ฐ€ isPending์œผ๋กœ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
status: string;
/*
  Will be:
    - 'idle' initial status prior to the mutation function executing.
    - 'pending' if the mutation is currently executing.
    - 'error' if the last mutation attempt resulted in an error.
    - 'success' if the last mutation attempt was successful.
  'isIdle', 'isPending', 'isSuccess', 'isError': boolean variables derived from 'status'
 */
  • ๊ทธ๋ฆฌ๊ณ  isPending && isFetching์œผ๋กœ ๊ตฌํ˜„๋˜๋Š” ์ƒˆ๋กœ์šด isLoading ํ”Œ๋ž˜๊ทธ๊ฐ€ ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
  • ์ด๋Š” ๊ธฐ์กด์˜ isInitialLoading๊ณผ ๋™์ผํ•œ ๊ธฐ๋Šฅ์„ ํ•˜๋Š”๋ฐ, isInitialLoading์€ ๋” ์ด์ƒ ์‚ฌ์šฉ๋˜์ง€ ์•Š์œผ๋ฉฐ ๋‹ค์Œ ๋ฉ”์ด์ € ๋ฒ„์ „ ์—…๋ฐ์ดํŠธ์—์„œ ์ œ๊ฑฐ๋  ์˜ˆ์ •์ž…๋‹ˆ๋‹ค.

18. Simplified optimistic updates

  • v5๋ถ€ํ„ฐ๋Š” ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ๋ฅผ ์ˆ˜ํ–‰ํ•˜๋Š” ๋‹จ์ˆœํ•œ ๋ฐฉ๋ฒ•์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
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๊ฐ€ ํ‘œ์‹œ๋˜๋Š” ๋ฐฉ์‹๋งŒ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค. ํ•ด๋‹น ๋ฐฉ๋ฒ•์€ ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ๋ฅผ ํ‘œ์‹œํ•ด์•ผ ํ•˜๋Š” ์œ„์น˜๊ฐ€ ํ•œ ๊ณณ๋งŒ ์žˆ๋Š” ๊ฒฝ์šฐ์— ํšจ๊ณผ์ ์ž…๋‹ˆ๋‹ค.
  • ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ์™€ ๊ด€๋ จ๋œ ์ž์„ธํ•œ ๋‚ด์šฉ์€ optimistic-updates๋ฅผ ์ฐธ๊ณ ํ•ด์ฃผ์‹œ๊ธธ ๋ฐ”๋ž๋‹ˆ๋‹ค.

19. Infinite Queries can prefetch multiple Pages

  • ์ด์ œ infinite queries๋„ normal queries์ฒ˜๋Ÿผ prefetch ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ๊ธฐ๋ณธ์ ์œผ๋กœ query์˜ ์ฒซ ๋ฒˆ์งธ ํŽ˜์ด์ง€๋งŒ prefetch๋˜๋ฉฐ ์ง€์ •๋œ queryKey ์•„๋ž˜์— ์ €์žฅ๋ฉ๋‹ˆ๋‹ค. ๋‘ ๊ฐœ ์ด์ƒ์˜ ํŽ˜์ด์ง€๋ฅผ ๋ฏธ๋ฆฌ ๊ฐ€์ ธ์˜ค๋ ค๋ฉด pages ์˜ต์…˜์„ ์‚ฌ์šฉํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.
  • prefetch์™€ ๊ด€๋ จ๋œ ์ž์„ธํ•œ ๋‚ด์šฉ์€ prefetching๋ฅผ ์ฐธ๊ณ ํ•ด์ฃผ์‹œ๊ธธ ๋ฐ”๋ž๋‹ˆ๋‹ค.
const prefetchTodos = async () => {
  // The results of this query will be cached like a normal query
  await queryClient.prefetchInfiniteQuery({
    queryKey: ["projects"],
    queryFn: fetchProjects,
    initialPageParam: 0,
    getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
    pages: 3, // prefetch the first 3 pages
  });
};

20. new 'combine' option for 'useQueries'

  • useQueries ๊ฒฐ๊ณผ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹จ์ผ ๊ฐ’์œผ๋กœ ๊ฒฐํ•ฉํ•˜๋ ค๋ฉด combine ์˜ต์…˜์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
const ids = [1,2,3]
const combinedQueries = useQueries({
  queries: ids.map(id => (
    { queryKey: ['post', id], queryFn: () => fetchPost(id) },
  )),
  combine: (results) => {
    return ({
      data: results.map(result => result.data),
      pending: results.some(result => result.isPending),
    })
  }
})
  • ์œ„ ์˜ˆ์ œ์—์„œ๋Š” combinedQueries๋Š” data์™€ pending ์†์„ฑ์ด ์žˆ๋Š” ๊ฐ์ฒด๊ฐ€ ๋ฉ๋‹ˆ๋‹ค. ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ์˜ ๋‹ค๋ฅธ ๋ชจ๋“  ์†์„ฑ์€ ์†์‹ค๋œ๋‹ค๋Š” ์ ์„ ์ฃผ์˜ํ•ด์•ผ ๋ฉ๋‹ˆ๋‹ค.

โ€‹21. โญ๏ธ new hooks for suspense

  • v5์—์„œ๋Š” data fetching์— ๋Œ€ํ•œ suspense๊ฐ€ ๋งˆ์นจ๋‚ด ์•ˆ์ •ํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
  • useSuspenseQuery, useSuspenseInfiniteQuery, useSuspenseQueries 3๊ฐ€์ง€ ํ›…์ด ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
  • ์œ„ 3๊ฐ€์ง€ ํ›…์„ ์‚ฌ์šฉํ•˜๊ฒŒ ๋˜๋ฉด ํƒ€์ž… ๋ ˆ๋ฒจ์—์„œ data๊ฐ€ undefined ์ƒํƒœ๊ฐ€ ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
import { useSuspenseQuery } from "@tanstack/react-query";

const { data } = useSuspenseQuery({ queryKey, queryFn });
  • suspense์™€ ๊ด€๋ จ๋œ ์ž์„ธํ•œ ๋‚ด์šฉ์€ suspense๋ฅผ ์ฐธ๊ณ ํ•ด์ฃผ์‹œ๊ธธ ๋ฐ”๋ž๋‹ˆ๋‹ค.

22. โญ๏ธ The minimum required TypeScript version is now 4.7

  • Tanstack Query v5๋Š” ํ•„์š”ํ•œ TypeScript ์ตœ์†Œ ๋ฒ„์ „์ด v4.7์ž…๋‹ˆ๋‹ค.

23. โญ๏ธ The minimum required React version is now 18.0

  • Tanstack Query v5๋Š” ํ•„์š”ํ•œ React ์ตœ์†Œ ๋ฒ„์ „์ด v18.0์ž…๋‹ˆ๋‹ค. ์ด๋Š” React v18 ์ด์ƒ์—์„œ๋งŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” useSyncExternalStore ํ›…์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

24. โญ๏ธ Supported Browsers

Chrome >= 91
Firefox >= 90
Edge >= 91
Safari >= 15
iOS >= 15
opera >= 77