Skip to content

ssi02014/react-query-tutorial

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

๐Ÿ’ป TanStack Query(React)

  • ํ•ด๋‹น ์ €์žฅ์†Œ๋Š” TanStack Query(React)์—์„œ ์ž์ฃผ ์‚ฌ์šฉํ•˜๋Š” ๊ฐœ๋…๋“ค์„ ์ •๋ฆฌํ•œ ์ €์žฅ์†Œ์ž…๋‹ˆ๋‹ค. TanStack Query(React)์˜ ๋ชจ๋“  ํ™œ์šฉ ๋ฐฉ๋ฒ•์ด ์ž‘์„ฑ๋œ ์ƒํƒœ๋Š” ์•„๋‹ˆ๋ฉฐ, ํ•„์š”ํ•œ ๋‚ด์šฉ์€ ์ถ”๊ฐ€, ๋ณด์™„ํ•  ์˜ˆ์ •์ž…๋‹ˆ๋‹ค.

Contributors

  • ๊ธฐ์—ฌํ•ด์ฃผ์‹  ๋ชจ๋“  ๋ถ„๊ป˜ ๊ฐ์‚ฌ๋“œ๋ฆฝ๋‹ˆ๋‹ค.
  • ์˜คํƒˆ์ž, ๊ฐ€๋…์„ฑ์ด ์ข‹์ง€ ์•Š์€ ๋ถ€๋ถ„ ๋˜๋Š” ์ถ”๊ฐ€ ๋‚ด์šฉ์€ Pull Request, Issue ๋“ฑ์„ ์ž์œ ๋กญ๊ฒŒ ๋‚จ๊ฒจ์ฃผ์‹œ๋ฉด ๊ฒ€ํ†  ํ›„์— ๋ฐ˜์˜ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

contributors


TanStack Query(React) v5

  • โญ๏ธ TanStack Query(React) v5๊ฐ€ 23.10.17์— ๋ฆด๋ฆฌ์ฆˆ๋์Šต๋‹ˆ๋‹ค. ํ•ด๋‹น ๋ฌธ์„œ๋Š” v5 ๊ธฐ์ค€์œผ๋กœ ์ž‘์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.
  • โญ๏ธ ๊ธฐ์กด v4 ๋ฌธ์„œ๋Š” react query tutorial v4 ๋ฌธ์„œ๋ฅผ ํ™•์ธํ•ด ์ฃผ์„ธ์š”.

แ„‰แ…ณแ„แ…ณแ„…แ…ตแ†ซแ„‰แ…ฃแ†บ 2023-10-18 แ„‹แ…ฉแ„Œแ…ฅแ†ซ 2 09 09


์ฃผ์š” ์ปจ์…‰ ๋ฐ ๊ฐ€์ด๋“œ ๋ชฉ์ฐจ

  1. React Query ๊ฐœ์š” ๋ฐ ๊ธฐ๋Šฅ
  2. ๊ธฐ๋ณธ ์„ค์ •(QueryClientProvider, QueryClient)
  3. React Query Devtools
  4. React Query ์บ์‹ฑ ๋ผ์ดํ”„ ์‚ฌ์ดํด
  5. useQuery
  6. useQuery ์ฃผ์š” ๋ฆฌํ„ด ๋ฐ์ดํ„ฐ
  7. staleTime๊ณผ gcTime
  8. ๋งˆ์šดํŠธ ๋  ๋•Œ๋งˆ๋‹ค ์žฌ์š”์ฒญํ•˜๋Š” refetchOnMount
  9. ์œˆ๋„์šฐ๊ฐ€ ํฌ์ปค์‹ฑ ๋  ๋•Œ๋งˆ๋‹ค ์žฌ์š”์ฒญํ•˜๋Š” refetchOnWindowFocus
  10. Polling ๋ฐฉ์‹์„ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•œ refetchInterval์™€ refetchIntervalInBackground)
  11. ์ž๋™ ์‹คํ–‰์˜ enabled์™€ ์ˆ˜๋™์œผ๋กœ ์ฟผ๋ฆฌ๋ฅผ ๋‹ค์‹œ ์š”์ฒญํ•˜๋Š” refetch
  12. ์‹คํŒจํ•œ ์ฟผ๋ฆฌ์— ๋Œ€ํ•ด ์žฌ์š”์ฒญํ•˜๋Š” retry
  13. onSuccess, onError, onSettled - ๐Ÿ’ก v5 @Deprecated
  14. select๋ฅผ ์ด์šฉํ•œ ๋ฐ์ดํ„ฐ ๋ณ€ํ™˜
  15. ์ฟผ๋ฆฌ๊ฐ€ pending ์ƒํƒœ์ธ ๋™์•ˆ ๋ณด์—ฌ ์ค„ ์ˆ˜ ์žˆ๋Š” placeholderData
  16. Paginated ๊ตฌํ˜„์— ์œ ์šฉํ•œ keepPreviousData - ๐Ÿ’ก v5 @Deprecated
  17. ํŠน์ • ์ฟผ๋ฆฌ ํ”„๋กœํผํ‹ฐ ๋ณ€๊ฒฝ ์‹œ์—๋งŒ ๋ฆฌ๋ Œ๋”๋ง์„ ํŠธ๋ฆฌ๊ฑฐ ํ•  ์ˆ˜ ์žˆ๋Š” notifyOnChangeProps
  18. ์ฟผ๋ฆฌ๋ฅผ ๋ณ‘๋ ฌ(Parallel) ์š”์ฒญํ•  ์ˆ˜ ์žˆ๋Š” useQueries
  19. ์ข…์† ์ฟผ๋ฆฌ(Dependent Queries)
  20. QueryClient ์ธ์Šคํ„ด์Šค๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” useQueryClient
  21. ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ๋ฅผ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋Š” initialData
  22. ๋ฐ์ดํ„ฐ๋ฅผ ๋ฏธ๋ฆฌ ๋ถˆ๋Ÿฌ์˜ค๋Š” PreFetching
  23. Infinite Queries(๋ฌดํ•œ ์ฟผ๋ฆฌ) + useInfiniteQuery
  24. ์„œ๋ฒ„์™€ HTTP CUD๊ด€๋ จ ์ž‘์—…์„ ์œ„ํ•œ useMutation
  25. ์ฟผ๋ฆฌ ์ˆ˜๋™ ์ทจ์†Œ cancelQueries
  26. ์ฟผ๋ฆฌ๋ฅผ ๋ฌดํšจํ™”ํ•  ์ˆ˜ ์žˆ๋Š” queryClient.invalidateQueries
  27. ์บ์‹œ ๋ฐ์ดํ„ฐ ์ฆ‰์‹œ ์—…๋ฐ์ดํŠธ๋ฅผ ์œ„ํ•œ queryClient.setQueryData
  28. ์‚ฌ์šฉ์ž ๊ฒฝํ—˜(UX)์„ ์˜ฌ๋ ค์ฃผ๋Š” Optimistic Updates(๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ)
  29. ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ๋•Œ Fallback UI๋ฅผ ์„ ์–ธ์ ์œผ๋กœ ๋ณด์—ฌ์ฃผ๊ธฐ ์œ„ํ•œ ErrorBoundary + useQueryErrorResetBoundary
  30. ์„œ๋ฒ„ ๋กœ๋”ฉ ์ค‘์ผ ๋•Œ Fallback UI๋ฅผ ์„ ์–ธ์ ์œผ๋กœ ๋ณด์—ฌ์ฃผ๊ธฐ ์œ„ํ•œ Suspense
  31. ์•ฑ ์ „์ฒด์— ๋™์ผํ•œ ์ฟผ๋ฆฌ ํ•จ์ˆ˜๋ฅผ ๊ณต์œ ํ•˜๋Š” Default Query Function
  32. ๋ฆฌ์•กํŠธ ์ฟผ๋ฆฌ์— ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ ์ ์šฉ
  33. ๋ฆฌ์•กํŠธ ์ฟผ๋ฆฌ ESLint ์ ์šฉ
  34. ๋ฆฌ์•กํŠธ ์ฟผ๋ฆฌ ์ง€์› ๋ฒ„์ „

๐Ÿ“ƒ ๊ธฐํƒ€ ์ฐธ๊ณ  ๋ฌธ์„œ

  1. QueryClient ์ฃผ์š” ๋‚ด์šฉ ์ •๋ฆฌ ๋ฌธ์„œ
  2. ๊ธฐ๋ณธ์ ์ธ React Query ์•„ํ‚คํ…์ฒ˜ ์‚ดํŽด๋ณด๊ธฐ: inside React Query

๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป ์ฃผ์š” ์ฐธ๊ณ  ํŽ˜์ด์ง€


๐Ÿ“ƒ React Query ๊ฐœ์š” ๋ฐ ๊ธฐ๋Šฅ

๋ชฉ์ฐจ ์ด๋™

๊ฐœ์š”

  • react-query๋Š” ๋ฆฌ์•กํŠธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ์„œ๋ฒ„ ์ƒํƒœ ๊ฐ€์ ธ์˜ค๊ธฐ, ์บ์‹ฑ, ๋™๊ธฐํ™” ๋ฐ ์—…๋ฐ์ดํŠธ๋ฅผ ๋ณด๋‹ค ์‰ฝ๊ฒŒ ๋‹ค๋ฃฐ ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ฃผ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ด๋‹ค. ํด๋ผ์ด์–ธํŠธ ์ƒํƒœ์™€ ์„œ๋ฒ„ ์ƒํƒœ๋ฅผ ๋ช…ํ™•ํžˆ ๊ตฌ๋ถ„ํ•˜๊ธฐ ์œ„ํ•ด ๋งŒ๋“ค์–ด์กŒ๋‹ค.
  • react-query์—์„œ๋Š” ๊ธฐ์กด ์ƒํƒœ ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ธ redux, mobX๊ฐ€ ํด๋ผ์ด์–ธํŠธ ์ƒํƒœ ์ž‘์—…์— ์ ํ•ฉํ•˜์ง€๋งŒ, ๋น„๋™๊ธฐ ๋˜๋Š” ์„œ๋ฒ„ ์ƒํƒœ ์ž‘์—…์—๋Š” ๊ทธ๋‹ค์ง€ ์ข‹์ง€ ์•Š๋‹ค๊ณ  ์–ธ๊ธ‰ํ•œ๋‹ค.
  • ํด๋ผ์ด์–ธํŠธ ์ƒํƒœ(Client State)์™€ ์„œ๋ฒ„ ์ƒํƒœ(Server State)๋Š” ์™„์ „ํžˆ ๋‹ค๋ฅธ ๊ฐœ๋…์ด๋ฉฐ, ํด๋ผ์ด์–ธํŠธ ์ƒํƒœ๋Š” ๊ฐ๊ฐ์˜ input ๊ฐ’์œผ๋กœ ์˜ˆ๋ฅผ ๋“ค ์ˆ˜ ์žˆ๊ณ , ์„œ๋ฒ„ ์ƒํƒœ๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅ๋˜์–ด ์žˆ๋Š” ๋ฐ์ดํ„ฐ๋กœ ์˜ˆ๋ฅผ ๋“ค ์ˆ˜ ์žˆ๋‹ค.

๊ธฐ๋Šฅ

  • ์บ์‹ฑ
  • ๋™์ผํ•œ ๋ฐ์ดํ„ฐ์— ๋Œ€ํ•œ ์ค‘๋ณต ์š”์ฒญ์„ ๋‹จ์ผ ์š”์ฒญ์œผ๋กœ ํ†ตํ•ฉ
  • ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์˜ค๋ž˜๋œ ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ
  • ๋ฐ์ดํ„ฐ๊ฐ€ ์–ผ๋งˆ๋‚˜ ์˜ค๋ž˜๋˜์—ˆ๋Š”์ง€ ์•Œ ์ˆ˜ ์žˆ๋‹ค.
  • ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ๋ฅผ ๊ฐ€๋Šฅํ•œ ๋น ๋ฅด๊ฒŒ ๋ฐ˜์˜
  • ํŽ˜์ด์ง€๋„ค์ด์…˜ ๋ฐ ๋ฐ์ดํ„ฐ ์ง€์—ฐ ๋กœ๋“œ์™€ ๊ฐ™์€ ์„ฑ๋Šฅ ์ตœ์ ํ™”
  • ์„œ๋ฒ„ ์ƒํƒœ์˜ ๋ฉ”๋ชจ๋ฆฌ ๋ฐ ๊ฐ€๋น„์ง€ ์ˆ˜์ง‘ ๊ด€๋ฆฌ
  • ๊ตฌ์กฐ ๊ณต์œ ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ๋ฅผ ๋ฉ”๋ชจํ™”

React Query ๊ธฐ๋ณธ ์„ค์ •

๋ชฉ์ฐจ ์ด๋™

import { QueryClient } from "@tanstack/react-query";

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: Infinity,
      // ...
    },
  },
});
  • QueryClient๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์บ์‹œ์™€ ์ƒํ˜ธ ์ž‘์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.
  • QueryClient์—์„œ ๋ชจ๋“  query ๋˜๋Š” mutation์— ๊ธฐ๋ณธ ์˜ต์…˜์„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ข…๋ฅ˜๊ฐ€ ์ƒ๋‹นํ•˜๋ฏ€๋กœ ๊ณต์‹ ๋ฌธ์„œ๋ฅผ ์ฐธ๊ณ ํ•ด ๋ณด์ž.
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

const queryClient = new QueryClient({ /* options */});

function App() {
  return (
   <QueryClientProvider client={queryClient}>
      <div>๋ธ”๋ผ๋ธ”๋ผ</div>
   </QueryClientProvider>;
  );
}
  • react-query๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” QueryClientProvider๋ฅผ ์ตœ์ƒ๋‹จ์—์„œ ๊ฐ์‹ธ์ฃผ๊ณ  QueryClient ์ธ์Šคํ„ด์Šค๋ฅผ client props๋กœ ๋„ฃ์–ด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ์—ฐ๊ฒฐํ•ด์•ผ ํ•œ๋‹ค.
  • ์œ„ ์˜ˆ์‹œ์—์„œ App.js์— QueryClientProvider๋กœ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ฐ์‹ธ๊ณ , client props์—๋‹ค queryClient๋ฅผ ์—ฐ๊ฒฐํ•จ์œผ๋กœ์จ, ์ด context๋Š” ์•ฑ์—์„œ ๋น„๋™๊ธฐ ์š”์ฒญ์„ ์•Œ์•„์„œ ์ฒ˜๋ฆฌํ•˜๋Š” background ๊ณ„์ธต์ด ๋œ๋‹ค.

Devtools

แ„‰แ…ณแ„แ…ณแ„…แ…ตแ†ซแ„‰แ…ฃแ†บ 2022-04-07 แ„‹แ…ฉแ„’แ…ฎ 11 53 32

๋ชฉ์ฐจ ์ด๋™

  • React Query Devtools ๊ณต์‹ ๋ฌธ์„œ
  • react-query๋Š” ์ „์šฉ devtools๋ฅผ ์ œ๊ณตํ•˜๋ฉฐ ๋ณ„๋„์˜ ํŒจํ‚ค์ง€ ์„ค์น˜๊ฐ€ ํ•„์š”ํ•˜๋‹ค.
  • devtools๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด React Query์˜ ๋ชจ๋“  ๋‚ด๋ถ€ ๋™์ž‘์„ ์‹œ๊ฐํ™”ํ•˜๋Š” ๋ฐ ๋„์›€์ด ๋˜๋ฉฐ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ๋””๋ฒ„๊น… ์‹œ๊ฐ„์„ ์ ˆ์•ฝํ•  ์ˆ˜ ์žˆ๋‹ค.
  • devtools๋Š” ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ process.env.NODE_ENV === "development" ์ธ ๊ฒฝ์šฐ์—๋งŒ ์‹คํ–‰๋œ๋‹ค, ์ฆ‰ ์ผ๋ฐ˜์ ์œผ๋กœ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ๋งŒ ์ž‘๋™ํ•˜๋„๋ก ์„ค์ •๋˜์–ด ์žˆ์œผ๋ฏ€๋กœ, ํ”„๋กœ์ ํŠธ ๋ฐฐํฌ ์‹œ์— Devtools ์‚ฝ์ž… ์ฝ”๋“œ๋ฅผ ์ œ๊ฑฐํ•ด ์ค„ ํ•„์š”๊ฐ€ ์—†๋‹ค.
  • Next 13+์˜ App Dir์—์„  dev dependency๋กœ ์„ค์น˜ํ•ด์•ผ ๋™์ž‘ํ•œ๋‹ค.
$ npm i @tanstack/react-query-devtools
# or
$ pnpm add @tanstack/react-query-devtools
# or
$ yarn add @tanstack/react-query-devtools
# or
$ bun add @tanstack/react-query-devtools
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      {/* The rest of your application */}
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}

options

  • initialIsOpen (Boolean)
    • true์ด๋ฉด ๊ฐœ๋ฐœ ๋„๊ตฌ๊ฐ€ ๊ธฐ๋ณธ์ ์œผ๋กœ ์—ด๋ ค ์žˆ๋„๋ก ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • buttonPosition?: ("top-left" | "top-right" | "bottom-left" | "bottom-right" | "relative")
    • ๊ธฐ๋ณธ๊ฐ’: bottom-right
    • devtools ํŒจ๋„์„ ์—ฌ๋‹ซ๊ธฐ ์œ„ํ•œ ๋กœ๊ณ  ์œ„์น˜
    • relative์ผ ๋•Œ ๋ฒ„ํŠผ์€ devtools๋ฅผ ๋ Œ๋”๋งํ•˜๋Š” ์œ„์น˜์— ๋ฐฐ์น˜๋œ๋‹ค.
  • ์ผ๋ฐ˜์ ์œผ๋กœ initialIsOpen, buttonPosition์„ ์ž์ฃผ ์‚ฌ์šฉํ•˜๋ฉฐ ๊ทธ ์™ธ์— position, client์™€ ๊ฐ™์€ ์˜ต์…˜๋“ค๋„ ์กด์žฌํ•œ๋‹ค.

์บ์‹ฑ ๋ผ์ดํ”„ ์‚ฌ์ดํด

๋ชฉ์ฐจ ์ด๋™

* Query Instances with and without cache data(์บ์‹œ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ๊ฑฐ๋‚˜ ์—†๋Š” ์ฟผ๋ฆฌ ์ธ์Šคํ„ด์Šค)
* Background Refetching(๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋ฆฌํŒจ์นญ)
* Inactive Queries(๋น„ํ™œ์„ฑ ์ฟผ๋ฆฌ)
* Garbage Collection(๊ฐ€๋น„์ง€ ์ปฌ๋ ‰์…˜)
  • gcTime์˜ ๊ธฐ๋ณธ๊ฐ’ 5๋ถ„, staleTime ๊ธฐ๋ณธ๊ฐ’ 0์ดˆ๋ฅผ ๊ฐ€์ •
  1. A๋ผ๋Š” queryKey๋ฅผ ๊ฐ€์ง„ A ์ฟผ๋ฆฌ ์ธ์Šคํ„ด์Šค๊ฐ€ mount๋จ
  2. ๋„คํŠธ์›Œํฌ์—์„œ ๋ฐ์ดํ„ฐ fetchํ•˜๊ณ , ๋ถˆ๋Ÿฌ์˜จ ๋ฐ์ดํ„ฐ๋Š” A๋ผ๋Š” queryKey๋กœ ์บ์‹ฑํ•จ
  3. ์ด ๋ฐ์ดํ„ฐ๋Š” fresh์ƒํƒœ์—์„œ staleTime(๊ธฐ๋ณธ๊ฐ’ 0) ์ดํ›„ stale ์ƒํƒœ๋กœ ๋ณ€๊ฒฝ๋จ
  4. A ์ฟผ๋ฆฌ ์ธ์Šคํ„ด์Šค๊ฐ€ unmount๋จ
  5. ์บ์‹œ๋Š” gcTime(๊ธฐ๋ณธ๊ฐ’ 5min) ๋งŒํผ ์œ ์ง€๋˜๋‹ค๊ฐ€ ๊ฐ€๋น„์ง€ ์ฝœ๋ ‰ํ„ฐ(GC)๋กœ ์ˆ˜์ง‘๋จ
  6. ๋งŒ์ผ, gcTime ์ง€๋‚˜๊ธฐ ์ „์ด๊ณ , A ์ฟผ๋ฆฌ ์ธ์Šคํ„ด์Šค freshํ•œ ์ƒํƒœ๋ผ๋ฉด ์ƒˆ๋กญ๊ฒŒ mount๋˜๋ฉด ์บ์‹œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด์—ฌ์ค€๋‹ค.

useQuery

๋ชฉ์ฐจ ์ด๋™

useQuery ๊ธฐ๋ณธ ๋ฌธ๋ฒ•

  • useQuery ๊ณต์‹ ๋ฌธ์„œ
  • useQuery๋Š” v5๋ถ€ํ„ฐ ์ธ์ž๋กœ ๋‹จ ํ•˜๋‚˜์˜ ๊ฐ์ฒด๋งŒ ๋ฐ›๋Š”๋‹ค. ๊ทธ์ค‘์— ์ฒซ ๋ฒˆ์งธ ์ธ์ž๊ฐ€ queryKey, queryFn๊ฐ€ ํ•„์ˆ˜ ๊ฐ’์ด๋‹ค.
const result = useQuery({
  queryKey, // required
  queryFn, // required
  // ...options ex) gcTime, staleTime, select, ...
});

result.data;
result.isLoading;
result.refetch;
// ...
// ์‹ค์ œ ์˜ˆ์ œ
// ๐Ÿ’ก queryFn์˜ ๋ฐ˜ํ™˜ ํƒ€์ž…์„ ์ง€์ •ํ•ด์ฃผ๋ฉด useQuery์˜ ํƒ€์ž… ์ถ”๋ก ์ด ์›ํ™œํ•ฉ๋‹ˆ๋‹ค.
const getAllSuperHero = async (): Promise<AxiosResponse<Hero[]>> => {
  return await axios.get("http://localhost:4000/superheroes");
};

const { data, isLoading } = useQuery({
  queryKey: ["super-heroes"],
  queryFn: getAllSuperHero,
});

1. queryKey

// (1) queryKey๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ๊ณ ์œ ํ•˜๊ฒŒ ์‹๋ณ„์— ๋”ํ•ด ์ฟผ๋ฆฌ ํ•จ์ˆ˜์— ์•„๋ž˜์™€ ๊ฐ™์ด ํŽธ๋ฆฌํ•˜๊ฒŒ ์ „๋‹ฌํ•  ์ˆ˜๋„ ์žˆ๋‹ค.
const getSuperHero = async ({
  queryKey,
}: {
  queryKey: ["super-hero", number];
}): Promise<AxiosResponse<Hero>> => {
  const heroId = queryKey[1]; // ex) queryKey: ["super-hero", "3"]

  return await axios.get(`http://localhost:4000/superheroes/${heroId}`);
};

const useSuperHeroData = (heroId: string) => {
  return useQuery({
    queryKey: ["super-hero", heroId],
    queryFn: getSuperHero, // (*)
  });
};
  • useQuery์˜ queryKey๋Š” ๋ฐฐ์—ด๋กœ ์ง€์ •ํ•ด ์ค˜์•ผ ํ•œ๋‹ค.
    • ์ด๋Š” ๋‹จ์ผ ๋ฌธ์ž์—ด๋งŒ ํฌํ•จ๋œ ๋ฐฐ์—ด์ด ๋  ์ˆ˜๋„ ์žˆ๊ณ , ์—ฌ๋Ÿฌ ๋ฌธ์ž์—ด๊ณผ ์ค‘์ฒฉ๋œ ๊ฐ์ฒด๋กœ ๊ตฌ์„ฑ๋œ ๋ณต์žกํ•œ ํ˜•ํƒœ์ผ ์ˆ˜๋„ ์žˆ๋‹ค.
// An individual todo
useQuery({ queryKey: ["todo", 5], ... })

// An individual todo in a "preview" format
useQuery({ queryKey: ["todo", 5, { preview: true }], ...})
  • useQuery๋Š” queryKey๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ฟผ๋ฆฌ ์บ์‹ฑ์„ ๊ด€๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ํ•ต์‹ฌ์ด๋‹ค.
    • ๋งŒ์•ฝ, ์ฟผ๋ฆฌ๊ฐ€ ํŠน์ • ๋ณ€์ˆ˜์— ์˜์กดํ•œ๋‹ค๋ฉด ๋ฐฐ์—ด์—๋‹ค ์ด์–ด์„œ ์ค˜์•ผ ํ•œ๋‹ค. ex: ["super-hero", heroId, ...]
    • ์ด๋Š” ์‚ฌ์‹ค ๊ต‰์žฅํžˆ ์ค‘์š”ํ•˜๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, queryClient.setQueryData ๋“ฑ๊ณผ ๊ฐ™์ด ํŠน์ • ์ฟผ๋ฆฌ์— ์ ‘๊ทผ์ด ํ•„์š” ํ•  ๋•Œ ์ดˆ๊ธฐ์— ์„ค์ •ํ•ด๋‘” ํฌ๋งท์„ ์ง€์ผœ์ค˜์•ผ ์ œ๋Œ€๋กœ ์ฟผ๋ฆฌ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋‹ค.
    • ์•„๋ž˜ options ์˜ˆ์ œ๋ฅผ ์‚ดํŽด๋ณด๋ฉด useSuperHeroData์˜ queryKey๋Š” ["super-hero", heroId]์ด๋‹ค. ๊ทธ๋ ‡๋‹ค๋ฉด queryClient.setQueryData๋ฅผ ์ด์šฉํ•  ๋•Œ ๋˜‘๊ฐ™์ด ["super-hero", heroId] ํฌ๋งท์„ ๊ฐ€์ ธ์•ผ ํ•œ๋‹ค. ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด ์›ํ•˜๋Š” ์ฟผ๋ฆฌ์— ์ ‘๊ทผํ•  ์ˆ˜ ์—†๋‹ค.

2. queryFn

  • useQuery์˜ queryFn๋Š” Promise๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ํ•จ์ˆ˜๋ฅผ ๋„ฃ์–ด์•ผ ํ•œ๋‹ค.
// (2) ์ƒ๋‹จ์˜ queryKey ์˜ˆ์ œ์™€ ๋ฐ˜๋Œ€๋กœ queryFn ์ž์ฒด์ ์œผ๋กœ ์ธ์ž๋ฅผ ๋ฐ›๋Š” ํ˜•ํƒœ
const getSuperHero = async (heroId: string): Promise<AxiosResponse<Hero>> => {
  return await axios.get(`http://localhost:4000/superheroes/${heroId}`);
};

const useSuperHeroData = (heroId: string) => {
  return useQuery({
    queryKey: ["super-hero", heroId],
    queryFn: () => getSuperHero(heroId), // (*)
  });
};

3. options

  • useQuery ๊ณต์‹ ๋ฌธ์„œ
  • useQuery์˜ options์— ๋งŽ์ด ์“ฐ์ด๋Š” ์˜ต์…˜๋“ค์€ ์ฐจ๊ทผ์ฐจ๊ทผ ์‚ดํŽด๋ณผ ์˜ˆ์ •์ด๋‹ค. ๋ฌธ์„œ ์™ธ์— ๋”์šฑ ์ž์„ธํžˆ ์•Œ๊ณ  ์‹ถ๋‹ค๋ฉด ์œ„ ๊ณต์‹ ๋ฌธ์„œ๋ฅผ ์ฐธ๊ณ ํ•˜์ž.

const useSuperHeroData = (heroId: string) => {
  return useQuery({
    queryKey: ["super-hero", heroId],
    queryFn: () => getSuperHero(heroId),
    gcTime: 5 * 60 * 1000, // 5๋ถ„
    staleTime: 1 * 60 * 1000, // 1๋ถ„
    retry: 1,
    // ... options
  });
};

useQuery ์ฃผ์š” ๋ฆฌํ„ด ๋ฐ์ดํ„ฐ

const {
  data,
  error,
  status,
  fetchStatus,
  isLoading,
  isFetching,
  isError,
  refetch,
  // ...
} = useQuery({
  queryKey: ["super-heroes"],
  queryFn: getAllSuperHero,
});
  • data: ์ฟผ๋ฆฌ ํ•จ์ˆ˜๊ฐ€ ๋ฆฌํ„ดํ•œ Promise์—์„œ resolved๋œ ๋ฐ์ดํ„ฐ
  • error: ์ฟผ๋ฆฌ ํ•จ์ˆ˜์— ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•œ ๊ฒฝ์šฐ, ์ฟผ๋ฆฌ์— ๋Œ€ํ•œ ์˜ค๋ฅ˜ ๊ฐ์ฒด
  • status: data, ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ๊ฐ’์— ๋Œ€ํ•œ ์ƒํƒœ๋ฅผ ํ‘œํ˜„ํ•˜๋Š” status๋Š” ๋ฌธ์ž์—ด ํ˜•ํƒœ๋กœ 3๊ฐ€์ง€์˜ ๊ฐ’์ด ์กด์žฌํ•œ๋‹ค.
    • pending: ์ฟผ๋ฆฌ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†๊ณ , ์ฟผ๋ฆฌ ์‹œ๋„๊ฐ€ ์•„์ง ์™„๋ฃŒ๋˜์ง€ ์•Š์€ ์ƒํƒœ.
    • error: ์—๋Ÿฌ ๋ฐœ์ƒํ–ˆ์„ ๋•Œ ์ƒํƒœ
    • success: ์ฟผ๋ฆฌ ํ•จ์ˆ˜๊ฐ€ ์˜ค๋ฅ˜ ์—†์ด ์š”์ฒญ ์„ฑ๊ณตํ•˜๊ณ  ๋ฐ์ดํ„ฐ๋ฅผ ํ‘œ์‹œํ•  ์ค€๋น„๊ฐ€ ๋œ ์ƒํƒœ.
  • fetchStatus: queryFn์— ๋Œ€ํ•œ ์ •๋ณด๋ฅผ ๋‚˜ํƒ€๋ƒ„
    • fetching: ์ฟผ๋ฆฌ๊ฐ€ ํ˜„์žฌ ์‹คํ–‰ ์ค‘์ธ ์ƒํƒœ
    • paused: ์ฟผ๋ฆฌ๋ฅผ ์š”์ฒญํ–ˆ์ง€๋งŒ, ์ž ์‹œ ์ค‘๋‹จ๋œ ์ƒํƒœ (network mode์™€ ์—ฐ๊ด€)
    • idle: ์ฟผ๋ฆฌ๊ฐ€ ํ˜„์žฌ ์•„๋ฌด ์ž‘์—…๋„ ์ˆ˜ํ–‰ํ•˜์ง€ ์•Š๋Š” ์ƒํƒœ
  • isLoading: ์บ์‹ฑ ๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์„ ๋•Œ ์ฆ‰, ์ฒ˜์Œ ์‹คํ–‰๋œ ์ฟผ๋ฆฌ์ผ ๋•Œ ๋กœ๋”ฉ ์—ฌ๋ถ€์— ๋”ฐ๋ผ true/false๋กœ ๋ฐ˜ํ™˜๋œ๋‹ค.
    • ์ด๋Š” ์บ์‹ฑ ๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ๋‹ค๋ฉด ๋กœ๋”ฉ ์—ฌ๋ถ€์— ์ƒ๊ด€์—†์ด false๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
    • isFetching && isPending ์™€ ๋™์ผํ•˜๋‹ค.
  • isFetching: ์บ์‹ฑ ๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ๋”๋ผ๋„ ์ฟผ๋ฆฌ๊ฐ€ ์‹คํ–‰๋˜๋ฉด ๋กœ๋”ฉ ์—ฌ๋ถ€์— ๋”ฐ๋ผ true/false๋กœ ๋ฐ˜ํ™˜๋œ๋‹ค.
  • isSuccess: ์ฟผ๋ฆฌ ์š”์ฒญ์ด ์„ฑ๊ณตํ•˜๋ฉด true
  • isError: ์ฟผ๋ฆฌ ์š”์ฒญ ์ค‘์— ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•œ ๊ฒฝ์šฐ true
  • refetch: ์ฟผ๋ฆฌ๋ฅผ ์ˆ˜๋™์œผ๋กœ ๋‹ค์‹œ ๊ฐ€์ ธ์˜ค๋Š” ํ•จ์ˆ˜.
  • ๊ทธ ์™ธ ๋ฐ˜ํ™˜ ๋ฐ์ดํ„ฐ๋“ค์„ ์ž์„ธํžˆ ์•Œ๊ณ  ์‹ถ์œผ๋ฉด useQuery ๊ณต์‹ ๋ฌธ์„œ ์ฐธ๊ณ 

๐Ÿ’ก status, fetchStatus ๋‚˜๋ˆ ์„œ ๋‹ค๋ฃจ๋Š” ๊ฑธ๊นŒ?

  • Why Two Different States ๊ณต์‹ ๋ฌธ์„œ
  • fetchStatus๋Š” HTTP ๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ ์ƒํƒœ์™€ ์ข€ ๋” ๊ด€๋ จ๋œ ์ƒํƒœ ๋ฐ์ดํ„ฐ์ด๋‹ค.
    • ์˜ˆ๋ฅผ ๋“ค์–ด, status๊ฐ€ success ์ƒํƒœ๋ผ๋ฉด ์ฃผ๋กœ fetchStatus๋Š” idle ์ƒํƒœ์ง€๋งŒ, ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ re-fetch๊ฐ€ ๋ฐœ์ƒํ•  ๋•Œ fetching ์ƒํƒœ์ผ ์ˆ˜ ์žˆ๋‹ค.
    • status๊ฐ€ ๋ณดํ†ต loading ์ƒํƒœ์ผ ๋•Œ fetchStatus๋Š” ์ฃผ๋กœ fetching๋ฅผ ๊ฐ–์ง€๋งŒ, ๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ์ด ๋˜์–ด ์žˆ์ง€ ์•Š์€ ๊ฒฝ์šฐ paused ์ƒํƒœ๋ฅผ ๊ฐ€์งˆ ์ˆ˜ ์žˆ๋‹ค.
  • ์ •๋ฆฌํ•˜์ž๋ฉด ์•„๋ž˜์™€ ๊ฐ™๋‹ค.
    • status๋Š” data๊ฐ€ ์žˆ๋Š”์ง€ ์—†๋Š”์ง€์— ๋Œ€ํ•œ ์ƒํƒœ๋ฅผ ์˜๋ฏธํ•œ๋‹ค.
    • fetchStatus๋Š” ์ฟผ๋ฆฌ ์ฆ‰, queryFn ์š”์ฒญ์ด ์ง„ํ–‰ ์ค‘์ธ์ง€ ์•„๋‹Œ์ง€์— ๋Œ€ํ•œ ์ƒํƒœ๋ฅผ ์˜๋ฏธํ•œ๋‹ค.

useQuery ์ฃผ์š” ์˜ต์…˜

๋ชฉ์ฐจ ์ด๋™


staleTime๊ณผ gcTime

  • stale์€ ์šฉ์–ด ๋œป๋Œ€๋กœ ์ฉ์€์ด๋ผ๋Š” ์˜๋ฏธ์ด๋‹ค. ์ฆ‰, ์ตœ์‹  ์ƒํƒœ๊ฐ€ ์•„๋‹ˆ๋ผ๋Š” ์˜๋ฏธ์ด๋‹ค.
  • fresh๋Š” ๋œป ๊ทธ๋Œ€๋กœ ์‹ ์„ ํ•œ์ด๋ผ๋Š” ์˜๋ฏธ์ด๋‹ค. ์ฆ‰, ์ตœ์‹  ์ƒํƒœ๋ผ๋Š” ์˜๋ฏธ์ด๋‹ค.
const {
  data,
  // ...
} = useQuery({
  queryKey: ["super-heroes"],
  queryFn: getAllSuperHero,
  gcTime: 5 * 60 * 1000, // 5๋ถ„
  staleTime: 1 * 60 * 1000, // 1๋ถ„
});

  1. staleTime: (number | Infinity)
    • staleTime์€ ๋ฐ์ดํ„ฐ๊ฐ€ fresh์—์„œ stale ์ƒํƒœ๋กœ ๋ณ€๊ฒฝ๋˜๋Š” ๋ฐ ๊ฑธ๋ฆฌ๋Š” ์‹œ๊ฐ„, ๋งŒ์•ฝ staleTime์ด 3000์ด๋ฉด fresh ์ƒํƒœ์—์„œ 3์ดˆ ๋’ค์— stale๋กœ ๋ณ€ํ™˜
    • fresh ์ƒํƒœ์ผ ๋•Œ๋Š” ์ฟผ๋ฆฌ ์ธ์Šคํ„ด์Šค๊ฐ€ ์ƒˆ๋กญ๊ฒŒ mount ๋˜์–ด๋„ ๋„คํŠธ์›Œํฌ ์š”์ฒญ(fetch)์ด ์ผ์–ด๋‚˜์ง€ ์•Š๋Š”๋‹ค.
    • ์ฐธ๊ณ ๋กœ, staleTime์˜ ๊ธฐ๋ณธ๊ฐ’์€ 0์ด๊ธฐ ๋•Œ๋ฌธ์— ์ผ๋ฐ˜์ ์œผ๋กœ fetch ํ›„์— ๋ฐ”๋กœ stale์ด ๋œ๋‹ค.
  2. gcTime: (number | Infinity)
    • ๋ฐ์ดํ„ฐ๊ฐ€ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ฑฐ๋‚˜, inactive ์ƒํƒœ์ผ ๋•Œ ์บ์‹ฑ ๋œ ์ƒํƒœ๋กœ ๋‚จ์•„์žˆ๋Š” ์‹œ๊ฐ„(๋ฐ€๋ฆฌ์ดˆ)์ด๋‹ค.
    • ์ฟผ๋ฆฌ ์ธ์Šคํ„ด์Šค๊ฐ€ unmount ๋˜๋ฉด ๋ฐ์ดํ„ฐ๋Š” inactive ์ƒํƒœ๋กœ ๋ณ€๊ฒฝ๋˜๋ฉฐ, ์บ์‹œ๋Š” gcTime๋งŒํผ ์œ ์ง€๋œ๋‹ค.
    • gcTime์ด ์ง€๋‚˜๋ฉด ๊ฐ€๋น„์ง€ ์ฝœ๋ ‰ํ„ฐ๋กœ ์ˆ˜์ง‘๋œ๋‹ค.
    • gcTime์ด ์ง€๋‚˜๊ธฐ ์ „์— ์ฟผ๋ฆฌ ์ธ์Šคํ„ด์Šค๊ฐ€ ๋‹ค์‹œ mount ๋˜๋ฉด, ๋ฐ์ดํ„ฐ๋ฅผ fetch ํ•˜๋Š” ๋™์•ˆ ์บ์‹œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด์—ฌ์ค€๋‹ค.
    • gcTime์€ staleTime๊ณผ ๊ด€๊ณ„์—†์ด, ๋ฌด์กฐ๊ฑด inactive ๋œ ์‹œ์ ์„ ๊ธฐ์ค€์œผ๋กœ ์บ์‹œ ๋ฐ์ดํ„ฐ ์‚ญ์ œ๋ฅผ ๊ฒฐ์ •ํ•œ๋‹ค.
    • gcTime์˜ ๊ธฐ๋ณธ๊ฐ’์€ 5๋ถ„์ด๋‹ค. SSR ํ™˜๊ฒฝ์—์„œ๋Š” Infinity์ด๋‹ค.
  • ์—ฌ๊ธฐ์„œ ์ฃผ์˜ํ•  ์ ์€ staleTime๊ณผ gcTime์˜ ๊ธฐ๋ณธ๊ฐ’์€ ๊ฐ๊ฐ 0๋ถ„๊ณผ 5๋ถ„์ด๋‹ค. ๋”ฐ๋ผ์„œ staleTime์— ์–ด๋– ํ•œ ์„ค์ •๋„ ํ•˜์ง€ ์•Š์œผ๋ฉด ํ•ด๋‹น ์ฟผ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ(Observer)๊ฐ€ mount ๋์„ ๋•Œ ๋งค๋ฒˆ ๋‹ค์‹œ API๋ฅผ ์š”์ฒญํ•  ๊ฒƒ์ด๋‹ค.
  • staleTime์„ gcTime๋ณด๋‹ค ๊ธธ๊ฒŒ ์„ค์ •ํ–ˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•˜๋ฉด, staleTime๋งŒํผ์˜ ์บ์‹ฑ์„ ๊ธฐ๋Œ€ํ–ˆ์„ ๋•Œ ์›ํ•˜๋Š” ๊ฒฐ๊ณผ๋ฅผ ์–ป์ง€ ๋ชปํ•  ๊ฒƒ์ด๋‹ค. ์ฆ‰, ๋‘ ๊ฐœ์˜ ์˜ต์…˜์„ ์ ์ ˆํ•˜๊ฒŒ ์„ค์ •ํ•ด ์ค˜์•ผ ํ•œ๋‹ค.
    • ์ฐธ๊ณ ๋กœ, TkDodo์˜ reply์— ๋”ฐ๋ฅด๋ฉด TkDodo๋Š” staleTime์„ gcTime๋ณด๋‹ค ์ž‘๊ฒŒ ์„ค์ •ํ•˜๋Š” ๊ฒƒ์ด ์ข‹๋‹ค.๋Š” ์˜๊ฒฌ์— ๋™์˜ํ•˜์ง€ ์•Š๋Š”๋‹ค๊ณ  ํ•œ๋‹ค.
    • ์˜ˆ์ปจ๋Œ€, staleTime์ด 60๋ถ„์ผ์ง€๋ผ๋„ ์œ ์ €๊ฐ€ ์ž์ฃผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๋ฐ์ดํ„ฐ๋ผ๋ฉด ๊ตณ์ด gcTime์„ 60๋ถ„ ์ด์ƒ์œผ๋กœ ์„ค์ •ํ•˜์—ฌ ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ๋‚ญ๋น„ํ•  ํ•„์š”๊ฐ€ ์—†๋‹ค.

refetchOnMount

const {
  data,
  // ...
} = useQuery({
  queryKey: ["super-heroes"],
  queryFn: getAllSuperHero,
  refetchOnMount: true,
});
  • refetchOnMount: boolean | "always" | ((query: Query) => boolean | "always")
  • refetchOnMount๋Š” ๋ฐ์ดํ„ฐ๊ฐ€ stale ์ƒํƒœ์ผ ๊ฒฝ์šฐ, mount๋งˆ๋‹ค refetch๋ฅผ ์‹คํ–‰ํ•˜๋Š” ์˜ต์…˜์ด๋‹ค.
  • refetchOnMount์˜ ๊ธฐ๋ณธ๊ฐ’์€ true์ด๋‹ค.
  • always๋กœ ์„ค์ •ํ•˜๋ฉด ๋งˆ์šดํŠธ ์‹œ๋งˆ๋‹ค ๋งค๋ฒˆ refetch๋ฅผ ์‹คํ–‰ํ•œ๋‹ค.
  • false๋กœ ์„ค์ •ํ•˜๋ฉด ์ตœ์ดˆ fetch ์ดํ›„์—๋Š” refetch ํ•˜์ง€ ์•Š๋Š”๋‹ค.

refetchOnWindowFocus

const {
  data,
  // ...
} = useQuery({
  queryKey: ["super-heroes"],
  queryFn: getAllSuperHero,
  refetchOnWindowFocus: true,
});
  • refetchOnWindowFocus: boolean | "always" | ((query: Query) => boolean | "always")
  • refetchOnWindowFocus๋Š” ๋ฐ์ดํ„ฐ๊ฐ€ stale ์ƒํƒœ์ผ ๊ฒฝ์šฐ ์œˆ๋„์šฐ ํฌ์ปค์‹ฑ ๋  ๋•Œ๋งˆ๋‹ค refetch๋ฅผ ์‹คํ–‰ํ•˜๋Š” ์˜ต์…˜์ด๋‹ค.
  • refetchOnWindowFocus์˜ ๊ธฐ๋ณธ๊ฐ’์€ true์ด๋‹ค.
  • ์˜ˆ๋ฅผ ๋“ค์–ด, ํฌ๋กฌ์—์„œ ๋‹ค๋ฅธ ํƒญ์„ ๋ˆŒ๋ €๋‹ค๊ฐ€ ๋‹ค์‹œ ์›๋ž˜ ๋ณด๋˜ ์ค‘์ธ ํƒญ์„ ๋ˆŒ๋ €์„ ๋•Œ๋„ ์ด ๊ฒฝ์šฐ์— ํ•ด๋‹นํ•œ๋‹ค. ์‹ฌ์ง€์–ด F12๋กœ ๊ฐœ๋ฐœ์ž ๋„๊ตฌ ์ฐฝ์„ ์ผœ์„œ ๋„คํŠธ์›Œํฌ ํƒญ์ด๋“ , ์ฝ˜์†” ํƒญ์ด๋“  ๊ฐœ๋ฐœ์ž ๋„๊ตฌ ์ฐฝ์—์„œ ๋†€๋‹ค๊ฐ€ ํŽ˜์ด์ง€ ๋‚ด๋ถ€๋ฅผ ๋‹ค์‹œ ํด๋ฆญํ–ˆ์„ ๋•Œ๋„ ์ด ๊ฒฝ์šฐ์— ํ•ด๋‹นํ•œ๋‹ค.
  • always๋กœ ์„ค์ •ํ•˜๋ฉด ํ•ญ์ƒ ์œˆ๋„์šฐ ํฌ์ปค์‹ฑ ๋  ๋•Œ๋งˆ๋‹ค refetch๋ฅผ ์‹คํ–‰ํ•œ๋‹ค๋Š” ์˜๋ฏธ์ด๋‹ค.

Polling

const {
  data,
  // ...
} = useQuery({
  queryKey: ["super-heroes"],
  queryFn: getAllSuperHero,
  refetchInterval: 2000,
  refetchIntervalInBackground: true,
});
Polling(ํด๋ง)์ด๋ž€?
์‹ค์‹œ๊ฐ„ ์›น์„ ์œ„ํ•œ ๊ธฐ๋ฒ•์œผ๋กœ "์ผ์ •ํ•œ ์ฃผ๊ธฐ(ํŠน์ •ํ•œ ์‹œ๊ฐ„)"๋ฅผ ๊ฐ€์ง€๊ณ  ์„œ๋ฒ„์™€ ์‘๋‹ต์„ ์ฃผ๊ณ ๋ฐ›๋Š” ๋ฐฉ์‹์ด ํด๋ง ๋ฐฉ์‹์ด๋‹ค.
react-query์—์„œ๋Š” "refetchInterval", "refetchIntervalInBackground"์„ ์ด์šฉํ•ด์„œ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค.
  1. refetchInterval: number | false | ((data: TData | undefined, query: Query) => number | false)
  • refetchInterval์€ ์‹œ๊ฐ„(ms)๋ฅผ ๊ฐ’์œผ๋กœ ๋„ฃ์–ด์ฃผ๋ฉด ์ผ์ • ์‹œ๊ฐ„๋งˆ๋‹ค ์ž๋™์œผ๋กœ refetch๋ฅผ ์‹œ์ผœ์ค€๋‹ค.
  1. refetchIntervalInBackground: boolean
  • refetchIntervalInBackground๋Š” refetchInterval๊ณผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋Š” ์˜ต์…˜์ด๋‹ค.
  • ํƒญ/์ฐฝ์ด ๋ฐฑ๊ทธ๋ผ์šด๋“œ์— ์žˆ๋Š” ๋™์•ˆ refetch ์‹œ์ผœ์ค€๋‹ค. ์ฆ‰, ๋ธŒ๋ผ์šฐ์ €์— focus ๋˜์–ด ์žˆ์ง€ ์•Š์•„๋„ refetch๋ฅผ ์‹œ์ผœ์ฃผ๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•œ๋‹ค.

enabled refetch

const {
  data,
  refetch,
  // ...
} = useQuery({
  queryKey: ["super-heroes"],
  queryFn: getAllSuperHero,
  enabled: false,
});

const handleClickRefetch = useCallback(() => {
  refetch();
}, [refetch]);

return (
  <div>
    {data?.data.map((hero: Data) => (
      <div key={hero.id}>{hero.name}</div>
    ))}
    <button onClick={handleClickRefetch}>Fetch Heroes</button>
  </div>
);
  • enabled: boolean
  • enabled๋Š” ์ฟผ๋ฆฌ๊ฐ€ ์ž๋™์œผ๋กœ ์‹คํ–‰๋˜์ง€ ์•Š๋„๋ก ํ•  ๋•Œ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • enabled๋ฅผ false๋ฅผ ์ฃผ๋ฉด ์ฟผ๋ฆฌ๊ฐ€ ์ž๋™ ์‹คํ–‰๋˜์ง€ ์•Š๋Š”๋‹ค.
    • useQuery ๋ฐ˜ํ™˜ ๊ฐ’ ์ค‘ status๊ฐ€ pending ์ƒํƒœ๋กœ ์‹œ์ž‘ํ•œ๋‹ค.
  • refetch๋Š” ์ฟผ๋ฆฌ๋ฅผ ์ˆ˜๋™์œผ๋กœ ๋‹ค์‹œ ์š”์ฒญํ•˜๋Š” ๊ธฐ๋Šฅ์ด๋‹ค. ์ฟผ๋ฆฌ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ์˜ค๋ฅ˜๋งŒ ๊ธฐ๋ก๋œ๋‹ค.
    • ์˜ค๋ฅ˜๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๋ ค๋ฉด throwOnError ์†์„ฑ์„ true๋กœ ํ•ด์„œ ์ „๋‹ฌํ•ด์•ผ ํ•œ๋‹ค.
  • ๋ณดํ†ต ์ž๋™์œผ๋กœ ์ฟผ๋ฆฌ ์š”์ฒญ์„ ํ•˜์ง€ ์•Š๊ณ  ๋ฒ„ํŠผ ํด๋ฆญ์ด๋‚˜ ํŠน์ • ์ด๋ฒคํŠธ๋ฅผ ํ†ตํ•ด ์š”์ฒญ์„ ์‹œ๋„ํ•  ๋•Œ ๊ฐ™์ด ์‚ฌ์šฉํ•œ๋‹ค.
  • ๐Ÿ’ก ์ฃผ์˜ํ•  ์ ์€, enabled: false๋ฅผ ์คฌ๋‹ค๋ฉด queryClient๊ฐ€ ์ฟผ๋ฆฌ๋ฅผ ๋‹ค์‹œ ๊ฐ€์ ธ์˜ค๋Š” ๋ฐฉ๋ฒ• ์ค‘ invalidateQueries์™€ refetchQueries๋ฅผ ๋ฌด์‹œํ•œ๋‹ค.

retry

const {
  data,
  refetch,
  // ...
} = useQuery({
  queryKey: ["super-heroes"],
  queryFn: getAllSuperHero,
  retry: 10, // ์˜ค๋ฅ˜๋ฅผ ํ‘œ์‹œํ•˜๊ธฐ ์ „์— ์‹คํŒจํ•œ ์š”์ฒญ์„ 10๋ฒˆ ์žฌ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค.
});
  • retry: (boolean | number | (failureCount: number, error: TError) => boolean)
  • retry๋Š” ์ฟผ๋ฆฌ๊ฐ€ ์‹คํŒจํ•˜๋ฉด useQuery๋ฅผ ํŠน์ • ํšŸ์ˆ˜๋งŒํผ ์žฌ์š”์ฒญํ•˜๋Š” ์˜ต์…˜์ด๋‹ค.
  • retry๊ฐ€ false์ธ ๊ฒฝ์šฐ, ์‹คํŒจํ•œ ์ฟผ๋ฆฌ๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ๋‹ค์‹œ ์‹œ๋„ํ•˜์ง€ ์•Š๋Š”๋‹ค. true์ธ ๊ฒฝ์šฐ์—๋Š” ์‹คํŒจํ•œ ์ฟผ๋ฆฌ์— ๋Œ€ํ•ด์„œ ๋ฌดํ•œ ์žฌ์š”์ฒญ์„ ์‹œ๋„ํ•œ๋‹ค.
  • ๊ฐ’์œผ๋กœ ์ˆซ์ž๋ฅผ ๋„ฃ์„ ๊ฒฝ์šฐ, ์‹คํŒจํ•œ ์ฟผ๋ฆฌ๊ฐ€ ํ•ด๋‹น ์ˆซ์ž๋ฅผ ์ถฉ์กฑํ•  ๋•Œ๊นŒ์ง€ ์š”์ฒญ์„ ์žฌ์‹œ๋„ํ•œ๋‹ค.
  • ๊ธฐ๋ณธ๊ฐ’์€ ํด๋ผ์ด์–ธํŠธ ํ™˜๊ฒฝ์—์„œ๋Š” 3, ์„œ๋ฒ„ ํ™˜๊ฒฝ์—์„œ๋Š” 0์ด๋‹ค.

onSuccess, onError, onSettled

  • NOTE: v4๊นŒ์ง€ ์žˆ๋˜ onSuccess, onError, onSettled Callback์€ useQuery ์˜ต์…˜์—์„œ @Deprecated ๋๋‹ค. ๋‹จ, useMutation์—์„œ๋Š” ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๋‹ค.
  • Breaking React Query's API on purpose ์ฐธ๊ณ 

select

const {
  data,
  // ...
} = useQuery({
  queryKey: ["super-heroes"],
  queryFn: getAllSuperHero,
  select: (data) => {
    const superHeroNames = data.data.map((hero) => hero.name);
    return superHeroNames;
  },
});

return (
  <div>
    {data.map((heroName, idx) => (
      <div key={`${heroName}-${idx}`}>{heroName}</div>
    ))}
  </div>
);
  • select: (data: TData) => unknown
  • select ์˜ต์…˜์„ ์‚ฌ์šฉํ•˜์—ฌ ์ฟผ๋ฆฌ ํ•จ์ˆ˜์—์„œ ๋ฐ˜ํ™˜๋œ ๋ฐ์ดํ„ฐ์˜ ์ผ๋ถ€๋ฅผ ๋ณ€ํ™˜ํ•˜๊ฑฐ๋‚˜ ์„ ํƒํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ์ฐธ๊ณ ๋กœ, ๋ฐ˜ํ™˜๋œ ๋ฐ์ดํ„ฐ ๊ฐ’์—๋Š” ์˜ํ–ฅ์„ ์ฃผ์ง€๋งŒ ์ฟผ๋ฆฌ ์บ์‹œ์— ์ €์žฅ๋˜๋Š” ๋‚ด์šฉ์—๋Š” ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š๋Š”๋‹ค.

placeholderData

const placeholderData = useMemo(() => generateFakeHeroes(), []);

const {
  data,
  // ...
} = useQuery({
  queryKey: ["super-heroes"],
  queryFn: getAllSuperHero,
  placeholderData: placeholderData,
});
  • placeholderData: TData | (previousValue: TData | undefined; previousQuery: Query | undefined,) => TData
  • placeholderData๋ฅผ ์„ค์ •ํ•˜๋ฉด ์ฟผ๋ฆฌ๊ฐ€ pending ์ƒํƒœ์ธ ๋™์•ˆ ํŠน์ • ์ฟผ๋ฆฌ์— ๋Œ€ํ•œ placeholder data๋กœ ์‚ฌ์šฉ๋œ๋‹ค.
  • placeholderData๋Š” ์บ์‹œ์— ์œ ์ง€๋˜์ง€ ์•Š์œผ๋ฉฐ, ์„œ๋ฒ„ ๋ฐ์ดํ„ฐ์™€ ๊ด€๊ณ„์—†๋Š” ๋ณด์—ฌ์ฃผ๊ธฐ์šฉ ๊ฐ€์งœ ๋ฐ์ดํ„ฐ๋‹ค.
  • placeholderData์— ํ•จ์ˆ˜๋ฅผ ์ œ๊ณตํ•˜๋Š” ๊ฒฝ์šฐ ์ฒซ ๋ฒˆ์งธ ์ธ์ž๋กœ ์ด์ „์— ๊ด€์ฐฐ๋œ ์ฟผ๋ฆฌ ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์‹ ํ•˜๊ณ , ๋‘ ๋ฒˆ์งธ ์ธ์ž๋Š” ์ด์ „ ์ฟผ๋ฆฌ ์ธ์Šคํ„ด์Šค๊ฐ€ ๋œ๋‹ค.

keepPreviousData

  • v4๊นŒ์ง€ ์žˆ๋˜ keepPreviousData์€ ํŽ˜์ด์ง€๋„ค์ด์…˜๊ณผ ๊ฐ™์€ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•  ๋•Œ ๋งŽ์ด ์‚ฌ์šฉํ•˜๋˜ ์˜ต์…˜์ด์—ˆ๋‹ค. ์บ์‹ฑ ๋˜์ง€ ์•Š์€ ํŽ˜์ด์ง€๋ฅผ ๊ฐ€์ ธ์˜ฌ ๋•Œ ๋ชฉ๋ก์ด ๊นœ๋นก๊ฑฐ๋ฆฌ๋Š” ํ˜„์ƒ์„ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ๋‹ค.

  • ํ•˜์ง€๋งŒ, v5๋ถ€ํ„ฐ keepPreviousData, isPreviousData์€ ์˜ต์…˜์€ ์ œ๊ฑฐ๋๋‹ค.

  • ์ด๋“ค์€ ๊ฐ๊ฐ placeholderData์™€ isPlaceholderData ํ”Œ๋ž˜๊ทธ์™€ ๊ฑฐ์˜ ์œ ์‚ฌํ•˜๊ฒŒ ๋™์ž‘ํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

  • ์•„๋ž˜ ์˜ˆ์ œ์ฒ˜๋Ÿผ placeholderData๋ฅผ ํ™œ์šฉํ•˜๋ฉด์„œ ์ด์ „ ๋ฒ„์ „์—์„œ keepPreviousData์˜ ๊ฐ’์„ "true"๋กœ ์คฌ์„ ๋•Œ์™€ ๋™์ผํ•œ ๊ธฐ๋Šฅ์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค.

import { useQuery, keepPreviousData } from "@tanstack/react-query";

const {
  data,
  // ...
} = useQuery({
  queryKey: ["super-heroes"],
  queryFn: getAllSuperHero,
  placeholderData: keepPreviousData,
});
  • ์•„๋ž˜ ์˜ˆ์‹œ์ฒ˜๋Ÿผ ์ž‘์„ฑํ•ด์„œ ์œ„์˜ keepPreviousData ์˜ˆ์‹œ์™€ ๋™์ผํ•œ ๋™์ž‘์„ ํ•  ์ˆ˜ ์žˆ๋‹ค.
import { useQuery } from "@tanstack/react-query";

const {
  data,
  // ...
} = useQuery({
  queryKey: ["super-heroes"],
  queryFn: getAllSuperHero,
  placeholderData: (previousData, previousQuery) => previousData,
});

notifyOnChangeProps

import { useQuery } from "@tanstack/react-query";

const { data, dataUpdatedAt } = useQuery({
  queryKey: ["super-heroes"],
  queryFn: getAllSuperHero,
  notifyOnChangeProps: ["data"], // data ๊ฐ’ ๋ณ€๊ฒฝ์‹œ์—๋งŒ ๋ฆฌ๋ Œ๋”๋ง์ด ๋ฐœ์ƒํ•œ๋‹ค
});
  • notifyOnChangeProps: string[] | "all" | (() => string[] | "all")
  • ์ฟผ๋ฆฌ์˜ ํŠน์ • ํ”„๋กœํผํ‹ฐ๋“ค์ด ๋ณ€๊ฒฝ๋˜์—ˆ์„ ๋•Œ๋งŒ ๋ฆฌ๋ Œ๋”๋ง์ด ๋ฐœ์ƒํ•˜๋„๋ก ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ๋ณ„๋„๋กœ ์„ค์ •ํ•˜์ง€ ์•Š์œผ๋ฉด, ์ปดํฌ๋„ŒํŠธ์—์„œ ์ ‘๊ทผํ•œ ๊ฐ’์ด ๋ณ€๊ฒฝ๋˜์—ˆ์„ ๋•Œ ๋ฆฌ๋ Œ๋”๋ง์ด ๋ฐœ์ƒํ•œ๋‹ค (๊ธฐ๋ณธ ๋™์ž‘). ์ฆ‰, ์œ„ ์˜ˆ์‹œ์—์„œ notifyOnChangeProps์— ์„ค์ •๊ฐ’์„ ์ฃผ์ง€ ์•Š์•˜๋‹ค๋ฉด, data, dataUpdatedAt ์ค‘ ์–ด๋Š ํ•˜๋‚˜๊ฐ€ ๋ณ€๊ฒฝ๋˜๋ฉด ๋ฆฌ๋ Œ๋”๋ง์ด ๋ฐœ์ƒํ•œ๋‹ค.
  • "all"๋กœ ์„ค์ •ํ•  ๊ฒฝ์šฐ, ์ฟผ๋ฆฌ์˜ ์–ด๋–ค ํ”„๋กœํผํ‹ฐ๊ฐ€ ๋ณ€๊ฒฝ๋˜๋“  ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ฆฌ๋ Œ๋”๋ง๋œ๋‹ค.
  • ์ฐธ๊ณ : ๊ธฐ๋ณธ ๋™์ž‘์€ Object.defineProperty()๋ฅผ ํ™œ์šฉํ•œ๋‹ค.

Parallel

๋ชฉ์ฐจ ์ด๋™

const { data: superHeroes } = useQuery({
  queryKey: ["super-heroes"],
  queryFn: getAllSuperHero,
});

const { data: friends } = useQuery({
  queryKey: ["friends"],
  queryFn: getFriends,
});
  • ๋ช‡ ๊ฐ€์ง€ ์ƒํ™ฉ์„ ์ œ์™ธํ•˜๋ฉด ์ฟผ๋ฆฌ ์—ฌ๋Ÿฌ ๊ฐœ๊ฐ€ ์„ ์–ธ๋œ ์ผ๋ฐ˜์ ์ธ ์ƒํ™ฉ์ผ ๋•Œ, ์ฟผ๋ฆฌ ํ•จ์ˆ˜๋“ค์€ ๊ทธ๋ƒฅ ๋ณ‘๋ ฌ๋กœ ์š”์ฒญ๋ผ์„œ ์ฒ˜๋ฆฌ๋œ๋‹ค.
  • ์ด๋Ÿฌํ•œ ํŠน์ง•์€ ์ฟผ๋ฆฌ ์ฒ˜๋ฆฌ์˜ ๋™์‹œ์„ฑ์„ ๊ทน๋Œ€ํ™”์‹œํ‚จ๋‹ค.
const queryResults = useQueries({
  queries: [
    {
      queryKey: ["super-hero", 1],
      queryFn: () => getSuperHero(1),
      staleTime: Infinity, // ๋‹ค์Œ๊ณผ ๊ฐ™์ด option ์ถ”๊ฐ€ ๊ฐ€๋Šฅ!
    },
    {
      queryKey: ["super-hero", 2],
      queryFn: () => getSuperHero(2),
      staleTime: 0,
    },
    // ...
  ],
});
  • ํ•˜์ง€๋งŒ, ์ฟผ๋ฆฌ ์—ฌ๋Ÿฌ ๊ฐœ๋ฅผ ๋™์‹œ์— ์ˆ˜ํ–‰ํ•ด์•ผ ํ•˜๋Š”๋ฐ, ๋ Œ๋”๋ง์ด ๊ฑฐ๋“ญ๋˜๋Š” ์‚ฌ์ด์‚ฌ์ด์— ๊ณ„์† ์ฟผ๋ฆฌ๊ฐ€ ์ˆ˜ํ–‰๋˜์–ด์•ผ ํ•œ๋‹ค๋ฉด ์ฟผ๋ฆฌ๋ฅผ ์ˆ˜ํ–‰ํ•˜๋Š” ๋กœ์ง์ด hook ๊ทœ์น™์— ์–ด๊ธ‹๋‚  ์ˆ˜๋„ ์žˆ๋‹ค. ์ด๋Ÿด ๋•Œ๋Š” useQueries๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.
  • useQueries ํ›…์€ ๋ชจ๋“  ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ๊ฐ€ ํฌํ•จ๋œ ๋ฐฐ์—ด์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. ๋ฐ˜ํ™˜๋˜๋Š” ์ˆœ์„œ๋Š” ์ฟผ๋ฆฌ๊ฐ€ ์ž…๋ ฅ๋œ ์ˆœ์„œ์™€ ๋™์ผํ•˜๋‹ค.

Queries Combine

  • useQueries Combine ๊ณต์‹ ๋ฌธ์„œ

  • 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 ํ”„๋กœํผํ‹ฐ๋ฅผ ๊ฐ–๋Š”๋‹ค.
  • Note: ์ฐธ๊ณ ๋กœ ๊ฒฐํ•ฉํ•˜๋ฉด ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ์˜ ๋‚˜๋จธ์ง€ ๋‹ค๋ฅธ ํ”„๋กœํผํ‹ฐ๋“ค์€ ์†์‹ค๋œ๋‹ค.

Dependent Queries

๋ชฉ์ฐจ ์ด๋™

  • Dependent Queries ๊ณต์‹ ๋ฌธ์„œ
  • ์ข…์† ์ฟผ๋ฆฌ๋Š” ์–ด๋–ค A๋ผ๋Š” ์ฟผ๋ฆฌ๊ฐ€ ์žˆ๋Š”๋ฐ ์ด A ์ฟผ๋ฆฌ๋ฅผ ์‹คํ–‰ํ•˜๊ธฐ ์ „์— ์‚ฌ์ „์— ์™„๋ฃŒ๋˜์–ด์•ผ ํ•˜๋Š” B ์ฟผ๋ฆฌ๊ฐ€ ์žˆ๋Š”๋ฐ, ์ด๋Ÿฌํ•œ B ์ฟผ๋ฆฌ์— ์˜์กดํ•˜๋Š” A ์ฟผ๋ฆฌ๋ฅผ ์ข…์† ์ฟผ๋ฆฌ๋ผ๊ณ  ํ•œ๋‹ค.
  • react-query์—์„œ๋Š” enabled ์˜ต์…˜์„ ํ†ตํ•ด ์ข…์† ์ฟผ๋ฆฌ๋ฅผ ์‰ฝ๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค.
// ์‚ฌ์ „์— ์™„๋ฃŒ๋˜์–ด์•ผ ํ•  ์ฟผ๋ฆฌ
const { data: user } = useQuery({
  queryKey: ["user", email],
  queryFn: () => getUserByEmail(email),
});

const channelId = user?.data.channelId;

// user ์ฟผ๋ฆฌ์— ์ข…์† ์ฟผ๋ฆฌ
const { data: courses } = useQuery({
  queryKey: ["courses", channelId],
  queryFn: () => getCoursesByChannelId(channelId),
  enabled: !!channelId,
});

useQueryClient

๋ชฉ์ฐจ ์ด๋™

  • useQueryClient๋Š” QueryClient ์ธ์Šคํ„ด์Šค๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
  • QueryClient๋Š” ์บ์‹œ์™€ ์ƒํ˜ธ์ž‘์šฉํ•œ๋‹ค.
  • QueryClient๋Š” ๋‹ค์Œ ๋ฌธ์„œ์—์„œ ์ž์„ธํ•˜๊ฒŒ ๋‹ค๋ฃฌ๋‹ค.
import { useQueryClient } from "@tanstack/react-query";

const queryClient = useQueryClient();

Initial Query Data

๋ชฉ์ฐจ ์ด๋™

  • Initial Query Data ๊ณต์‹ ๋ฌธ์„œ
  • ์ฟผ๋ฆฌ์— ๋Œ€ํ•œ ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ๊ฐ€ ํ•„์š”ํ•˜๊ธฐ ์ „์— ์บ์‹œ์— ์ œ๊ณตํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ์žˆ๋‹ค.
  • initialData ์˜ต์…˜์„ ํ†ตํ•ด์„œ ์ฟผ๋ฆฌ๋ฅผ ๋ฏธ๋ฆฌ ์ฑ„์šฐ๋Š” ๋ฐ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ดˆ๊ธฐ ๋กœ๋“œ ์ƒํƒœ๋„ ๊ฑด๋„ˆ๋›ธ ์ˆ˜๋„ ์žˆ๋‹ค.
const useSuperHeroData = (heroId: string) => {
  const queryClient = useQueryClient();

  return useQuery({
    queryKey: ["super-hero", heroId],
    queryFn: () => getSuperHero(heroId),
    initialData: () => {
      const queryData = queryClient.getQueryData(["super-heroes"]) as any;
      const hero = queryData?.data?.find(
        (hero: Hero) => hero.id === parseInt(heroId)
      );

      if (hero) return { data: hero };
    },
  });
};

  • ์ฐธ๊ณ ๋กœ ์œ„ ์˜ˆ์ œ์—์„œ queryClient.getQueryData ๋ฉ”์„œ๋“œ๋Š” ๊ธฐ์กด ์ฟผ๋ฆฌ์˜ ์บ์‹ฑ ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋ฐ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋™๊ธฐ ํ•จ์ˆ˜์ด๋‹ค. ์ฟผ๋ฆฌ๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์œผ๋ฉด undefined๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

Prefetching

๋ชฉ์ฐจ ์ด๋™

  • prefetching ๊ณต์‹ ๋ฌธ์„œ
  • prefetch๋Š” ๋ง ๊ทธ๋Œ€๋กœ ๋ฏธ๋ฆฌ fetchํ•ด์˜ค๊ฒ ๋‹ค๋Š” ์˜๋ฏธ์ด๋‹ค.
  • ๋น„๋™๊ธฐ ์š”์ฒญ์€ ๋ฐ์ดํ„ฐ์–‘์ด ํด์ˆ˜๋ก ๋ฐ›์•„์˜ค๋Š” ์†๋„๊ฐ€ ๋Š๋ฆฌ๊ณ , ์‹œ๊ฐ„์ด ์˜ค๋ž˜ ๊ฑธ๋ฆฐ๋‹ค. ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ์œ„ํ•ด ๋ฐ์ดํ„ฐ๋ฅผ ๋ฏธ๋ฆฌ ๋ฐ›์•„์™€์„œ ์บ์‹ฑํ•ด ๋†“์œผ๋ฉด? ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›๊ธฐ ์ „์— ์‚ฌ์šฉ์ž๊ฐ€ ์บ์‹ฑ ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ์–ด UX์— ์ข‹์€ ์˜ํ–ฅ์„ ์ค„ ์ˆ˜ ์žˆ๋‹ค.
    • ์˜ˆ๋ฅผ ๋“ค์–ด ํŽ˜์ด์ง€๋„ค์ด์…˜์„ ๊ตฌํ˜„ํ–ˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•˜๋ฉด, ํŽ˜์ด์ง€1์—์„œ ํŽ˜์ด์ง€2๋กœ ์ด๋™ํ–ˆ์„ ๋•Œ ํŽ˜์ด์ง€3์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฏธ๋ฆฌ ๋ฐ›์•„๋†“๋Š” ๊ฒƒ์ด๋‹ค!
  • react query์—์„œ๋Š” queryClient.prefetchQuery์„ ํ†ตํ•ด์„œ prefetch ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•œ๋‹ค.

prefetchQuery

const prefetchNextPosts = async (nextPage: number) => {
  const queryClient = useQueryClient();
  // ํ•ด๋‹น ์ฟผ๋ฆฌ์˜ ๊ฒฐ๊ณผ๋Š” ์ผ๋ฐ˜ ์ฟผ๋ฆฌ๋“ค์ฒ˜๋Ÿผ ์บ์‹ฑ ๋œ๋‹ค.
  await queryClient.prefetchQuery({
    queryKey: ["posts", nextPage],
    queryFn: () => fetchPosts(nextPage),
    // ...options
  });
};

// ๋‹จ์ˆœ ์˜ˆ
useEffect(() => {
  const nextPage = currentPage + 1;

  if (nextPage < maxPage) {
    prefetchNextPosts(nextPage);
  }
}, [currentPage]);
  • ์ฐธ๊ณ ๋กœ prefetchQuery๋ฅผ ํ†ตํ•ด ๊ฐ€์ ธ์˜ค๋Š” ์ฟผ๋ฆฌ์— ๋Œ€ํ•œ ๋ฐ์ดํ„ฐ๊ฐ€ ์ด๋ฏธ ์บ์‹ฑ ๋˜์–ด ์žˆ์œผ๋ฉด ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค์ง€ ์•Š๋Š”๋‹ค.

prefetchInfiniteQuery

  • infinite ์ฟผ๋ฆฌ๋Š” ๋ฐ”๋กœ ์•„๋ž˜์— ๋‚˜์˜ค๊ฒ ์ง€๋งŒ ์ผ๋ฐ˜ ์ฟผ๋ฆฌ๋“ค์ฒ˜๋Ÿผ infinite ์ฟผ๋ฆฌ๋„ prefetch ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ๊ธฐ๋ณธ์ ์œผ๋กœ ์ฟผ๋ฆฌ์˜ ์ฒซ ๋ฒˆ์งธ ํŽ˜์ด์ง€๋งŒ prefetch ๋˜๋ฉฐ, ๊ทธ ์ด์ƒ์„ prefetch ํ•˜๋ ค๋ฉด pages ์˜ต์…˜์„ ํ™œ์šฉํ•ด์•ผ ํ•œ๋‹ค.
    • ์ด ๊ฒฝ์šฐ์—๋Š” getNextPageParam ํ•จ์ˆ˜๋ฅผ ๋ฌด์กฐ๊ฑด ์ œ๊ณตํ•ด ์ค˜์•ผ ํ•œ๋‹ค๋Š” ์ ์„ ์ฃผ์˜ํ•˜์ž.
const prefetchTodos = async () => {
  await queryClient.prefetchInfiniteQuery({
    queryKey: ["projects"],
    queryFn: fetchProjects,
    initialPageParam: 0,
    getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
    pages: 3, // prefetch the first 3 pages
  });
};

Infinite Queries

๋ชฉ์ฐจ ์ด๋™

  • Infinite Queries ๊ณต์‹ ๋ฌธ์„œ
  • useInfiniteQuery ๊ณต์‹ ๋ฌธ์„œ
  • Infinite Queries(๋ฌดํ•œ ์ฟผ๋ฆฌ)๋Š” ๋ฌดํ•œ ์Šคํฌ๋กค์ด๋‚˜ load more(๋” ๋ณด๊ธฐ)๊ณผ ๊ฐ™์ด ํŠน์ • ์กฐ๊ฑด์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์ถ”๊ฐ€์ ์œผ๋กœ ๋ฐ›์•„์˜ค๋Š” ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•  ๋•Œ ์‚ฌ์šฉํ•˜๋ฉด ์œ ์šฉํ•˜๋‹ค.
  • react-query๋Š” ์ด๋Ÿฌํ•œ ๋ฌดํ•œ ์ฟผ๋ฆฌ๋ฅผ ์ง€์›ํ•˜๊ธฐ ์œ„ํ•ด useQuery์˜ ์œ ์šฉํ•œ ๋ฒ„์ „์ธ useInfiniteQuery์„ ์ง€์›ํ•œ๋‹ค.
import { useInfiniteQuery } from "@tanstack/react-query";

// useInfiniteQuery์˜ queryFn์˜ ๋งค๊ฐœ๋ณ€์ˆ˜๋Š” `pageParam`์ด๋ผ๋Š” ํ”„๋กœํผํ‹ฐ๋ฅผ ๊ฐ€์งˆ ์ˆ˜ ์žˆ๋‹ค.
const fetchColors = async ({
  pageParam,
}: {
  pageParam: number;
}): Promise<AxiosResponse<PaginationColors>> => {
  return await axios.get(`http://localhost:4000/colors?page=${pageParam}`);
};

const InfiniteQueries = () => {
  const { data, hasNextPage, isFetching, isFetchingNextPage, fetchNextPage } =
    useInfiniteQuery({
      queryKey: ["colors"],
      queryFn: fetchColors,
      initialPageParam: 1,
      getNextPageParam: (lastPage, allPages) => {
        return allPages.length < 4 && allPages.length + 1;
      },
      // ...
    });

  return (
    <div>
      {data?.pages.map((group, idx) => ({
        /* ... */
      }))}
      <div>
        <button disabled={!hasNextPage} onClick={() => fetchNextPage()}>
          LoadMore
        </button>
      </div>
      <div>{isFetching && !isFetchingNextPage ? "Fetching..." : null}</div>
    </div>
  );
};

์ฃผ์š” ๋ฐ˜ํ™˜

  • useInfiniteQuery๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ useQuery์™€ ์‚ฌ์šฉ๋ฒ•์€ ๋น„์Šทํ•˜์ง€๋งŒ, ์ฐจ์ด์ ์ด ์žˆ๋‹ค.
  • useInfiniteQuery๋Š” ๋ฐ˜ํ™˜ ๊ฐ’์œผ๋กœ isFetchingNextPage, isFetchingPreviousPage, fetchNextPage, fetchPreviousPage, hasNextPage ๋“ฑ์ด ์ถ”๊ฐ€์ ์œผ๋กœ ์žˆ๋‹ค.
    • data.pages: ๋ชจ๋“  ํŽ˜์ด์ง€ ๋ฐ์ดํ„ฐ๋ฅผ ํฌํ•จํ•˜๋Š” ๋ฐฐ์—ด์ด๋‹ค.
    • data.pageParams: ๋ชจ๋“  ํŽ˜์ด์ง€ ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ํฌํ•จํ•˜๋Š” ๋ฐฐ์—ด์ด๋‹ค.
    • fetchNextPage: ๋‹ค์Œ ํŽ˜์ด์ง€๋ฅผ fetch ํ•  ์ˆ˜ ์žˆ๋‹ค.
    • fetchPreviousPage: ์ด์ „ ํŽ˜์ด์ง€๋ฅผ fetch ํ•  ์ˆ˜ ์žˆ๋‹ค.
    • isFetchingNextPage: fetchNextPage ๋ฉ”์„œ๋“œ๊ฐ€ ๋‹ค์Œ ํŽ˜์ด์ง€๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋™์•ˆ true์ด๋‹ค.
    • isFetchingPreviousPage: fetchPreviousPage ๋ฉ”์„œ๋“œ๊ฐ€ ์ด์ „ ํŽ˜์ด์ง€๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋™์•ˆ true์ด๋‹ค.
    • hasNextPage: ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋Š” ๋‹ค์Œ ํŽ˜์ด์ง€๊ฐ€ ์žˆ์„ ๊ฒฝ์šฐ true์ด๋‹ค.
    • hasPreviousPage: ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋Š” ์ด์ „ ํŽ˜์ด์ง€๊ฐ€ ์žˆ์„ ๊ฒฝ์šฐ true์ด๋‹ค.

์ฃผ์š” ์˜ต์…˜

  1. initialPageParam: TPageParam
  • initialPageParam์„ ์ด์šฉํ•ด์„œ ์ฒซ ํŽ˜์ด์ง€๋ฅผ ๊ฐ€์ ธ์˜ฌ ๋•Œ ์‚ฌ์šฉํ•  ๊ธฐ๋ณธ ํŽ˜์ด์ง€ ๋งค๊ฐœ๋ณ€์ˆ˜์ด๋‹ค. ํ•„์ˆ˜๊ฐ’์ด๋‹ค.
  1. getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => TPageParam | undefined | null
  • getNextPageParam ์„ ์ด์šฉํ•ด์„œ ํŽ˜์ด์ง€๋ฅผ ์ฆ๊ฐ€์‹œํ‚ฌ ์ˆ˜ ์žˆ๋‹ค. ํ•„์ˆ˜๊ฐ’์ด๋‹ค.
    • getNextPageParam์˜ ์ฒซ ๋ฒˆ์งธ ์ธ์ž lastPage๋Š” fetch ํ•ด์˜จ ๊ฐ€์žฅ ์ตœ๊ทผ์— ๊ฐ€์ ธ์˜จ ํŽ˜์ด์ง€ ๋ชฉ๋ก์ด๋‹ค.
    • ๋‘ ๋ฒˆ์งธ ์ธ์ž allPages๋Š” ํ˜„์žฌ๊นŒ์ง€ ๊ฐ€์ ธ์˜จ ๋ชจ๋“  ํŽ˜์ด์ง€ ๋ฐ์ดํ„ฐ์ด๋‹ค.
    • ์„ธ ๋ฒˆ์งธ ์ธ์ž firstPageParam ๋Š” ์ฒซ ๋ฒˆ์งธ ํŽ˜์ด์ง€์˜ ๋งค๊ฐœ๋ณ€์ˆ˜์ด๋‹ค.
    • ๋„ค ๋ฒˆ์งธ ์ธ์ž allPageParams ๋Š” ๋ชจ๋“  ํŽ˜์ด์ง€์˜ ๋งค๊ฐœ๋ณ€์ˆ˜์ด๋‹ค.
  • ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋‹ค์Œ ํŽ˜์ด์ง€๊ฐ€ ์—†์Œ์„ ํ‘œ์‹œํ•˜๋ ค๋ฉด undefined ๋˜๋Š” null์„ ๋ฐ˜ํ™˜ํ•˜๋ฉด ๋œ๋‹ค.
  • getPreviousPageParam๋„ ์กด์žฌํ•˜๋ฉฐ, getNextPageParam์™€ ๋ฐ˜๋Œ€์˜ ์†์„ฑ์„ ๊ฐ–๊ณ  ์žˆ๋‹ค.
  1. maxPages: number | undefined
  • infinite ์ฟผ๋ฆฌ์— ์ €์žฅํ•  ์ตœ๋Œ€ ํŽ˜์ด์ง€ ์ˆ˜์ด๋‹ค.
  • ์ตœ๋Œ€ ํŽ˜์ด์ง€ ์ˆ˜์— ๋„๋‹ฌํ–ˆ๋Š”๋ฐ ์ƒˆ ํŽ˜์ด์ง€๋ฅผ ๊ฐ€์ ธ์˜ค๋ฉด ์ง€์ •๋œ ๋ฐฉํ–ฅ(next, previous)์— ๋”ฐ๋ผ ํŽ˜์ด์ง€ ๋ฐฐ์—ด์—์„œ ์ฒซ ๋ฒˆ์งธ ํŽ˜์ด์ง€ ๋˜๋Š” ๋งˆ์ง€๋ง‰ ํŽ˜์ด์ง€๊ฐ€ ์ œ๊ฑฐ๋œ๋‹ค.
  • 0 ๋˜๋Š” undefined๋ผ๋ฉด ํŽ˜์ด์ง€ ์ˆ˜๋Š” ๋ฌด์ œํ•œ์ด๋‹ค.

๐Ÿ’ก pageParam

  • queryFn์— ๋„˜๊ฒจ์ฃผ๋Š” pageParam๊ฐ€ ๋‹จ์ˆœํžˆ ๋‹ค์Œ page์˜ ๊ฐ’๋งŒ์„ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์€ ์•„๋‹ˆ๋‹ค.
  • pageParam ๊ฐ’์€ getNextPageParam์—์„œ ์›ํ•˜๋Š” ํ˜•ํƒœ๋กœ ๋ณ€๊ฒฝ์‹œ์ผœ ์ค„ ์ˆ˜ ์žˆ๋‹ค.
  • ๋ฌด์Šจ ๋ง์ธ์ง€ ์˜ˆ์‹œ๋ฅผ ๋ณด๋ฉด ์ดํ•ด๊ฐ€ ์‰ฝ๋‹ค. ๐Ÿ‘ ์•„๋ž˜์™€ ๊ฐ™์ด getNextPageParam์—์„œ ๋ฐ˜ํ™˜ ๋ฐ์ดํ„ฐ๊ฐ€ ๋‹จ์ˆœํžˆ ๋‹ค์Œ ํŽ˜์ด์ง€๊ฐ’์ด ์•„๋‹Œ ๊ฐ์ฒด๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค๊ณ  ํ•ด๋ณด์ž.
const { data, hasNextPage, isFetching, isFetchingNextPage, fetchNextPage } =
  useInfiniteQuery({
    queryKey: ["colors"],
    queryFn: ({ pageParam }) => fetchColors(pageParam), // pageParam: { page: number; etc: string }
    initialPageParam: {
      page: number,
      etc: "hi",
    },
    getNextPageParam: (lastPage, allPages) => {
      return (
        allPages.length < 4 && {
          page: allPages.length + 1,
          etc: "bye",
        };
      )
    },
  });
  • ๊ทธ๋Ÿฌ๋ฉด queryFn์— ๋„ฃ์€ pageParam์—์„œ getNextPageParam์—์„œ ๋ฐ˜ํ™˜ํ•œ ๊ฐ์ฒด๋ฅผ ๋ฐ›์•„์˜ฌ ์ˆ˜ ์žˆ๋‹ค.
const fetchColors = async ({
  page,
  etc,
}: {
  page: number;
  etc: string;
}): Promise<AxiosResponse<PaginationColors>> => {
  return await axios.get(
    `http://localhost:4000/colors?page=${page}?etc=${etc}`
  );
};
  • ์ฆ‰, getNextPageParam์˜ ๋ฐ˜ํ™˜ ๊ฐ’์ด pageParams๋กœ ๋“ค์–ด๊ฐ€๊ธฐ ๋•Œ๋ฌธ์— pageParams๋ฅผ ์›ํ•˜๋Š” ํ˜•ํƒœ๋กœ ๋ณ€๊ฒฝํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด getNextPageParam์˜ ๋ฐ˜ํ™˜ ๊ฐ’์„ ์„ค์ •ํ•˜๋ฉด ๋œ๋‹ค.

useMutation

๋ชฉ์ฐจ ์ด๋™

  • useMutation ๊ณต์‹ ๋ฌธ์„œ
  • react-query์—์„œ ๊ธฐ๋ณธ์ ์œผ๋กœ ์„œ๋ฒ„์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ Get ํ•  ๋•Œ๋Š” useQuery๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.
  • ๋งŒ์•ฝ ์„œ๋ฒ„์˜ data๋ฅผ post, patch, put, delete์™€ ๊ฐ™์ด ์ˆ˜์ •ํ•˜๊ณ ์ž ํ•œ๋‹ค๋ฉด ์ด๋•Œ๋Š” useMutation์„ ์ด์šฉํ•œ๋‹ค.
  • ์š”์•ฝํ•˜์ž๋ฉด R(read)๋Š” useQuery, CUD(Create, Update, Delete)๋Š” useMutation์„ ์‚ฌ์šฉํ•œ๋‹ค.
const mutation = useMutation({
  mutationFn: createTodo,
  onMutate() {
    /* ... */
  },
  onSuccess(data) {
    console.log(data);
  },
  onError(err) {
    console.log(err);
  },
  onSettled() {
    /* ... */
  },
});

const onCreateTodo = (e) => {
  e.preventDefault();
  mutate({ title });
};
  • useMutation์˜ ๋ฐ˜ํ™˜ ๊ฐ’์ธ mutation ๊ฐ์ฒด์˜ mutate ๋ฉ”์„œ๋“œ๋ฅผ ์ด์šฉํ•ด์„œ ์š”์ฒญ ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๋‹ค.
  • mutate๋Š” onSuccess, onError ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด ์„ฑ๊ณตํ–ˆ์„ ์‹œ, ์‹คํŒจํ–ˆ์„ ์‹œ response ๋ฐ์ดํ„ฐ๋ฅผ ํ•ธ๋“ค๋งํ•  ์ˆ˜ ์žˆ๋‹ค.
  • onMutate๋Š” mutation ํ•จ์ˆ˜๊ฐ€ ์‹คํ–‰๋˜๊ธฐ ์ „์— ์‹คํ–‰๋˜๊ณ , mutation ํ•จ์ˆ˜๊ฐ€ ๋ฐ›์„ ๋™์ผํ•œ ๋ณ€์ˆ˜๊ฐ€ ์ „๋‹ฌ๋œ๋‹ค.
  • onSettled๋Š” try...catch...finally ๊ตฌ๋ฌธ์˜ finally์ฒ˜๋Ÿผ ์š”์ฒญ์ด ์„ฑ๊ณตํ•˜๋“  ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜๋“  ์ƒ๊ด€์—†์ด ๋งˆ์ง€๋ง‰์— ์‹คํ–‰๋œ๋‹ค.
const mutation = useMutation(addTodo);

try {
  const todo = await mutation.mutateAsync(todo);
  console.log(todo);
} catch (error) {
  console.error(error);
} finally {
  console.log("done");
}
  • ๋งŒ์•ฝ, useMutation์„ ์‚ฌ์šฉํ•  ๋•Œ promise ํ˜•ํƒœ์˜ response๊ฐ€ ํ•„์š”ํ•œ ๊ฒฝ์šฐ๋ผ๋ฉด mutateAsync๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์–ป์–ด์˜ฌ ์ˆ˜ ์žˆ๋‹ค.

๐Ÿ’ก mutate์™€ mutateAsync๋Š” ๋ฌด์—‡์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒŒ ์ข‹์„๊นŒ?

  • ๋Œ€๋ถ€๋ถ„์˜ ๊ฒฝ์šฐ์— ์šฐ๋ฆฌ๋Š” mutate๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์œ ๋ฆฌํ•˜๋‹ค. ์™œ๋ƒํ•˜๋ฉด mutate๋Š” ์ฝœ๋ฐฑ(onSuccess, onError)๋ฅผ ํ†ตํ•ด data์™€ error์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์šฐ๋ฆฌ๊ฐ€ ํŠน๋ณ„ํžˆ ํ•ธ๋“ค๋งํ•ด ์ค„ ํ•„์š”๊ฐ€ ์—†๋‹ค.
  • ํ•˜์ง€๋งŒ mutateAsync๋Š” Promise๋ฅผ ์ง์ ‘ ๋‹ค๋ฃจ๊ธฐ ๋•Œ๋ฌธ์— ์ด๋Ÿฐ ์—๋Ÿฌ ํ•ธ๋“ค๋ง ๊ฐ™์€ ๋ถ€๋ถ„์„ ์ง์ ‘ ๋‹ค๋ค„์•ผ ํ•œ๋‹ค.
    • ๋งŒ์•ฝ ์ด๋ฅผ ๋‹ค๋ฃจ์ง€ ์•Š์œผ๋ฉด unhandled promise rejection ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค.
  • TkDodo Blog: Mutate or MutateAsync

๐Ÿ’ก useMutation callback๊ณผ mutate callback์˜ ์ฐจ์ด

  • useMutation์€ onSuccess, onError, onSettled์™€ ๊ฐ™์€ Callback ํ•จ์ˆ˜๋“ค์„ ๊ฐ€์งˆ ์ˆ˜ ์žˆ๋‹ค.
  • ๊ทธ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ, mutate ์—ญ์‹œ ์œ„์™€ ๊ฐ™์€ Callback ํ•จ์ˆ˜๋“ค์„ ๊ฐ€์งˆ ์ˆ˜ ์žˆ๋‹ค.
  • ๋‘˜์˜ ๋™์ž‘์€ ๊ฐ™๋‹ค๊ณ  ์ƒ๊ฐํ•  ์ˆ˜ ์žˆ์ง€๋งŒ ์•ฝ๊ฐ„์˜ ์ฐจ์ด๊ฐ€ ์žˆ๋‹ค. ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.
    • useMutation์˜ Callback ํ•จ์ˆ˜์™€ mutate์˜ Callback ํ•จ์ˆ˜๋Š” ๋…๋ฆฝ์ ์œผ๋กœ ์‹คํ–‰๋œ๋‹ค.
    • ์ˆœ์„œ๋Š” useMutation์˜ Callback -> mutate์˜ Callback ์ˆœ์œผ๋กœ ์‹คํ–‰๋œ๋‹ค.
    • mutation์ด ์™„๋ฃŒ๋˜๊ธฐ ์ „์— ์ปดํฌ๋„ŒํŠธ๊ฐ€ unmount๋œ๋‹ค๋ฉด mutate์˜ Callback์€ ์‹คํ–‰๋˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ๋‹ค.
  • TkDodo๋Š” ์œ„์™€ ๊ฐ™์€ ์ด์œ ๋กœ ๋‘˜์„ ๋ถ„๋ฆฌํ•ด์„œ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ ์ ˆํ•˜๋‹ค๊ณ  ํ•œ๋‹ค.
    • ๊ผญ ํ•„์š”ํ•œ ๋กœ์ง(ex. ์ฟผ๋ฆฌ ์ดˆ๊ธฐํ™”)์€ useMutation์˜ Callback์œผ๋กœ ์‹คํ–‰์‹œํ‚จ๋‹ค.
    • ๋ฆฌ๋‹ค์ด๋ ‰์…˜ ๋ฐ UI ๊ด€๋ จ ์ž‘์—…์€ mutate Callback์—์„œ ์‹คํ–‰์‹œํ‚จ๋‹ค.
  • TkDodo Blog: Some callbacks might not fire

cancelQueries

๋ชฉ์ฐจ ์ด๋™

  • Query Cancellation ๊ณต์‹ ๋ฌธ์„œ
  • ์ฟผ๋ฆฌ๋ฅผ ์ˆ˜๋™์œผ๋กœ ์ทจ์†Œํ•˜๊ณ  ์‹ถ์„ ์ˆ˜๋„ ์žˆ๋‹ค.
    • ์˜ˆ๋ฅผ ๋“ค์–ด ์š”์ฒญ์„ ์™„๋ฃŒํ•˜๋Š” ๋ฐ ์‹œ๊ฐ„์ด ์˜ค๋ž˜ ๊ฑธ๋ฆฌ๋Š” ๊ฒฝ์šฐ ์‚ฌ์šฉ์ž๊ฐ€ ์ทจ์†Œ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜์—ฌ ์š”์ฒญ์„ ์ค‘์ง€ํ•˜๋„๋ก ํ—ˆ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.
    • ๋˜๋Š”, ์•„์ง HTTP ์š”์ฒญ์ด ๋๋‚˜์ง€ ์•Š์•˜์„ ๋•Œ, ํŽ˜์ด์ง€๋ฅผ ๋ฒ—์–ด๋‚  ๋•Œ๋„ ์ค‘๊ฐ„์— ์ทจ์†Œํ•ด์„œ ๋ถˆํ•„์š”ํ•œ ๋„คํŠธ์›Œํฌ ๋ฆฌ์†Œ์Šค๋ฅผ ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ์ด๋ ‡๊ฒŒ ํ•˜๋ ค๋ฉด ์ฟผ๋ฆฌ๋ฅผ ์ทจ์†Œํ•˜๊ณ  ์ด์ „ ์ƒํƒœ๋กœ ๋˜๋Œ๋ฆฌ๊ธฐ ์œ„ํ•ด queryClient.cancelQueries(queryKey)๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. ๋˜ํ•œ react-query๋Š” ์ฟผ๋ฆฌ ์ทจ์†Œ๋ฟ๋งŒ์•„๋‹ˆ๋ผ queryFn์˜ Promise๋„ ์ทจ์†Œํ•œ๋‹ค.
const query = useQuery({
  queryKey: ["super-heroes"],
  queryFn: getAllSuperHero,
});

const queryClient = useQueryClient();

const onCancelQuery = (e) => {
  e.preventDefault();

  queryClient.cancelQueries({
    queryKey: ["super-heroes"],
  });
};

return <button onClick={onCancelQuery}>Cancel</button>;

์ฟผ๋ฆฌ ๋ฌดํšจํ™”

๋ชฉ์ฐจ ์ด๋™

  • invalidateQueries์€ ํ™”๋ฉด์„ ์ตœ์‹  ์ƒํƒœ๋กœ ์œ ์ง€ํ•˜๋Š” ๊ฐ€์žฅ ๊ฐ„๋‹จํ•œ ๋ฐฉ๋ฒ•์ด๋‹ค.
  • ์˜ˆ๋ฅผ ๋“ค๋ฉด, ๊ฒŒ์‹œํŒ ๋ชฉ๋ก์—์„œ ์–ด๋–ค ๊ฒŒ์‹œ๊ธ€์„ ์ž‘์„ฑ(Post)ํ•˜๊ฑฐ๋‚˜ ๊ฒŒ์‹œ๊ธ€์„ ์ œ๊ฑฐ(Delete)ํ–ˆ์„ ๋•Œ ํ™”๋ฉด์— ๋ณด์—ฌ์ฃผ๋Š” ๊ฒŒ์‹œํŒ ๋ชฉ๋ก์„ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ตœ์‹ ํ™”ํ•ด์•ผ ํ•  ๋•Œ๊ฐ€ ์žˆ๋‹ค.
  • ํ•˜์ง€๋งŒ ์ด๋•Œ, query Key๊ฐ€ ๋ณ€ํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ ๊ฐ•์ œ๋กœ ์ฟผ๋ฆฌ๋ฅผ ๋ฌดํšจํ™”ํ•˜๊ณ  ์ตœ์‹ ํ™”๋ฅผ ์ง„ํ–‰ํ•ด์•ผ ํ•˜๋Š”๋ฐ, ์ด๋Ÿฐ ๊ฒฝ์šฐ์— invalidateQueries() ๋ฉ”์†Œ๋“œ๋ฅผ ์ด์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ์ฆ‰, query๊ฐ€ ์˜ค๋ž˜๋˜์—ˆ๋‹ค๋Š” ๊ฒƒ์„ ํŒ๋‹จํ•˜๊ณ  ๋‹ค์‹œ refetch๋ฅผ ํ•  ๋•Œ ์‚ฌ์šฉํ•œ๋‹ค!
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";

const useAddSuperHeroData = () => {
  const queryClient = useQueryClient();

  return useMutation(addSuperHero, {
    onSuccess(data) {
      queryClient.invalidateQueries({ queryKey: ["super-heroes"] }); // ์ด key์— ํ•ด๋‹นํ•˜๋Š” ์ฟผ๋ฆฌ๊ฐ€ ๋ฌดํšจํ™”!
      console.log(data);
    },
    onError(err) {
      console.log(err);
    },
  });
};
  • ์ฐธ๊ณ ๋กœ, queryKey์— ["super-heroes"]์„ ๋„˜๊ฒจ์ฃผ๋ฉด queryKey์— "super-heroes"๋ฅผ ํฌํ•จํ•˜๋Š” ๋ชจ๋“  ์ฟผ๋ฆฌ๊ฐ€ ๋ฌดํšจํ™”๋œ๋‹ค.
queryClient.invalidateQueries({ queryKey: ["super-heroes"] });

// ์•„๋ž˜ query๋“ค ๋ชจ๋‘ ๋ฌดํšจํ™” ๋œ๋‹ค.
const query = useQuery({
  queryKey: ["super-heroes", "superman"],
  queryFn: fetchSuperHero,
});
const query = useQuery({
  queryKey: ["super-heroes", { id: 1 }],
  queryFn: fetchSuperHero,
});
  • ์œ„์— enabled/refetch์—์„œ๋„ ์–ธ๊ธ‰ํ–ˆ์ง€๋งŒ enabled: false ์˜ต์…˜์„ ์ฃผ๋ฉดqueryClient๊ฐ€ ์ฟผ๋ฆฌ๋ฅผ ๋‹ค์‹œ ๊ฐ€์ ธ์˜ค๋Š” ๋ฐฉ๋ฒ• ์ค‘ invalidateQueries์™€ refetchQueries๋ฅผ ๋ฌด์‹œํ•œ๋‹ค.
  • ์ž์„ธํ•œ ๋‚ด์šฉ์€ queryClient.invalidateQueries ์ •๋ฆฌ๋ฅผ ์ฐธ๊ณ ํ•˜์ž.

์บ์‹œ ๋ฐ์ดํ„ฐ ์ฆ‰์‹œ ์—…๋ฐ์ดํŠธ

๋ชฉ์ฐจ ์ด๋™

  • queryClient.setQueryData ๊ณต์‹ ๋ฌธ์„œ
  • ๋ฐ”๋กœ ์œ„์—์„œ queryClient.invalidateQueries๋ฅผ ์ด์šฉํ•ด ์บ์‹œ ๋ฐ์ดํ„ฐ๋ฅผ ์ตœ์‹ ํ™”ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ดค๋Š”๋ฐ queryClient.setQueryData๋ฅผ ์ด์šฉํ•ด์„œ๋„ ๋ฐ์ดํ„ฐ๋ฅผ ์ฆ‰์‹œ ์—…๋ฐ์ดํŠธํ•  ์ˆ˜ ์žˆ๋‹ค.
  • queryClient.setQueryData๋Š” ์ฟผ๋ฆฌ์˜ ์บ์‹œ ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์ฆ‰์‹œ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๋ฐ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋™๊ธฐ ํ•จ์ˆ˜์ด๋‹ค.
  • setQueryData์˜ ๋‘ ๋ฒˆ์งธ ์ธ์ž๋Š” updater ํ•จ์ˆ˜์ด๋‹ค. ํ•ด๋‹น ํ•จ์ˆ˜์˜ ์ฒซ ๋ฒˆ์งธ ๋งค๊ฐœ๋ณ€์ˆ˜๋Š” oldData๋กœ ๊ธฐ์กด ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค.
const useAddSuperHeroData = () => {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: addSuperHero,
    onSuccess(data) {
      queryClient.setQueryData(["super-heroes"], (oldData: any) => {
        return {
          ...oldData,
          data: [...oldData.data, data.data],
        };
      });
    },
    onError(err) {
      console.log(err);
    },
  });
};

Optimistic Update

๋ชฉ์ฐจ ์ด๋™

  • Optimistic Update(๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ)๋ž€ ์„œ๋ฒ„ ์—…๋ฐ์ดํŠธ ์‹œ UI์—์„œ๋„ ์–ด์ฐจํ”ผ ์—…๋ฐ์ดํŠธํ•  ๊ฒƒ์ด๋ผ๊ณ (๋‚™๊ด€์ ์ธ) ๊ฐ€์ •ํ•ด์„œ ๋ฏธ๋ฆฌ UI๋ฅผ ์—…๋ฐ์ดํŠธ ์‹œ์ผœ์ฃผ๊ณ  ์„œ๋ฒ„๋ฅผ ํ†ตํ•ด ๊ฒ€์ฆ๋ฐ›๊ณ  ์—…๋ฐ์ดํŠธ ๋˜๋Š” ๋กค๋ฐฑํ•˜๋Š” ๋ฐฉ์‹์ด๋‹ค.
  • ์˜ˆ๋ฅผ ๋“ค์–ด facebook์— ์ข‹์•„์š” ๋ฒ„ํŠผ์ด ์žˆ๋Š”๋ฐ ์ด๊ฒƒ์„ ์œ ์ €๊ฐ€ ๋ˆ„๋ฅธ๋‹ค๋ฉด, ์ผ๋‹จ client ์ชฝ state๋ฅผ ๋จผ์ € ์—…๋ฐ์ดํŠธํ•œ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๋งŒ์•ฝ์— ์‹คํŒจํ•œ๋‹ค๋ฉด, ์˜ˆ์ „ state๋กœ ๋Œ์•„๊ฐ€๊ณ  ์„ฑ๊ณตํ•˜๋ฉด ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค์‹œ fetch ํ•ด์„œ ์„œ๋ฒ„ ๋ฐ์ดํ„ฐ์™€ ํ™•์‹คํžˆ ์—ฐ๋™์„ ์ง„ํ–‰ํ•œ๋‹ค.
  • Optimistic Update๊ฐ€ ์ •๋ง ์œ ์šฉํ•  ๋•Œ๋Š” ์ธํ„ฐ๋„ท ์†๋„๊ฐ€ ๋Š๋ฆฌ๊ฑฐ๋‚˜ ์„œ๋ฒ„๊ฐ€ ๋Š๋ฆด ๋•Œ์ด๋‹ค. ์œ ์ €๊ฐ€ ํ–‰ํ•œ ์•ก์…˜์„ ๊ธฐ๋‹ค๋ฆด ํ•„์š” ์—†์ด ๋ฐ”๋กœ ์—…๋ฐ์ดํŠธ๋˜๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ๋ณด์ด๊ธฐ ๋•Œ๋ฌธ์— ์‚ฌ์šฉ์ž ๊ฒฝํ—˜(UX) ์ธก๋ฉด์—์„œ ์ข‹๋‹ค.
const useAddSuperHeroData = () => {
  const queryClient = useQueryClient();
  return useMutation({
    mutateFn: addSuperHero,
    onMutate: async () => {
      await queryClient.cancelQueries(["super-heroes"]);

      // ์ด์ „ ๊ฐ’
      const previousHeroData = queryClient.getQueryData(["super-heroes"]);

      // ์ƒˆ๋กœ์šด ๊ฐ’์œผ๋กœ ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ ์ง„ํ–‰
      queryClient.setQueryData(["super-heroes"], (oldData: any) => {
        return {
          ...oldData,
          data: [
            ...oldData.data,
            { ...newHero, id: oldData?.data?.length + 1 },
          ],
        };
      });

      // ๊ฐ’์ด ๋“ค์–ด์žˆ๋Š” context ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜
      return { previousHeroData };
    },
    // mutation์ด ์‹คํŒจํ•˜๋ฉด onMutate์—์„œ ๋ฐ˜ํ™˜๋œ context๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋กค๋ฐฑ ์ง„ํ–‰
    onError(error, hero, context: any) {
      queryClient.setQueryData(["super-heroes"], context.previousHeroData);
    },
    // ์˜ค๋ฅ˜ ๋˜๋Š” ์„ฑ๊ณต ํ›„์—๋Š” ํ•ญ์ƒ refetch
    onSettled() {
      queryClient.invalidateQueries(["super-heroes"]);
    },
  });
};
  • ์ฐธ๊ณ ๋กœ ์œ„ ์˜ˆ์ œ์—์„œ cancelQueries๋Š” ์ฟผ๋ฆฌ๋ฅผ ์ˆ˜๋™์œผ๋กœ ์ทจ์†Œ์‹œํ‚ฌ ์ˆ˜ ์žˆ๋‹ค. ์ทจ์†Œ์‹œํ‚ฌ query์˜ queryKey๋ฅผ cancelQueries์˜ ์ธ์ž๋กœ ๋ณด๋‚ด ์‹คํ–‰์‹œํ‚จ๋‹ค.
  • ์˜ˆ๋ฅผ ๋“ค์–ด, ์š”์ฒญ์„ ์™„๋ฃŒํ•˜๋Š” ๋ฐ ์‹œ๊ฐ„์ด ์˜ค๋ž˜ ๊ฑธ๋ฆฌ๋Š” ๊ฒฝ์šฐ, ์‚ฌ์šฉ์ž๊ฐ€ ์ทจ์†Œ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜์—ฌ ์š”์ฒญ์„ ์ค‘์ง€ํ•˜๋Š” ๊ฒฝ์šฐ ์ด์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

useQueryErrorResetBoundary

๋ชฉ์ฐจ ์ด๋™

  • useQueryErrorResetBoundary ๊ณต์‹ ๋ฌธ์„œ
  • react-query์—์„œ ErrorBoundary์™€ useQueryErrorResetBoundary๋ฅผ ๊ฒฐํ•ฉํ•ด ์„ ์–ธ์ ์œผ๋กœ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ๋•Œ Fallback UI๋ฅผ ๋ณด์—ฌ์ค„ ์ˆ˜ ์žˆ๋‹ค.
  • ErrorBoundary์— ๋Œ€ํ•œ ์„ค๋ช…์€ ํ•ด๋‹น ๋ฌธ์„œ ์ฐธ๊ณ  ErrorBoundary

  • useQueryErrorResetBoundary๋Š” ErrorBoundary์™€ ํ•จ๊ป˜ ์‚ฌ์šฉ๋˜๋Š”๋ฐ ์ด๋Š”, ๊ธฐ๋ณธ์ ์œผ๋กœ ๋ฆฌ์•กํŠธ ๊ณต์‹ ๋ฌธ์„œ์—์„œ ๊ธฐ๋ณธ ์ฝ”๋“œ ๋ฒ ์ด์Šค๊ฐ€ ์ œ๊ณต๋˜๊ธด ํ•˜์ง€๋งŒ ์ข€ ๋” ์‰ฝ๊ฒŒ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋Š” react-error-boundary ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์กด์žฌํ•˜๊ณ , react-query ๊ณต์‹ ๋ฌธ์„œ์—์„œ๋„ ํ•ด๋‹น ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ์„ ์˜ˆ์‹œ๋กœ ์ œ๊ณตํ•ด ์ฃผ๊ธฐ ๋•Œ๋ฌธ์— react-error-boundary๋ฅผ ์„ค์น˜ํ•ด์„œ ์‚ฌ์šฉํ•ด ๋ณด์ž.
$ npm i react-error-boundary
# or
$ pnpm add react-error-boundary
# or
$ yarn add react-error-boundary
# or
$ bun add react-error-boundary
  • ์„ค์น˜ ํ›„์— ์•„๋ž˜์™€ ๊ฐ™์€ QueryErrorBoundary๋ผ๋Š” ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ƒ์„ฑํ•˜๊ณ , ๊ทธ ๋‚ด๋ถ€์— useQueryErrorResetBoundary ํ›…์„ ํ˜ธ์ถœํ•ด reset ํ•จ์ˆ˜๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค.
  • ์•„๋ž˜ ์ฝ”๋“œ ๋‚ด์šฉ์€ ๋‹จ์ˆœํ•˜๋‹ค.
    • Error๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ErrorBoundary์˜ fallbackRender prop์œผ๋กœ ๋„˜๊ธด ๋‚ด์šฉ์ด ๋ Œ๋”๋ง ๋˜๊ณ , ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด children ๋‚ด์šฉ์ด ๋ Œ๋”๋ง ๋œ๋‹ค.
    • ๋˜ํ•œ, fallbackRender์— ๋„ฃ์–ด์ฃผ๋Š” ์ฝœ๋ฐฑ ํ•จ์ˆ˜ ๋งค๊ฐœ ๋ณ€์ˆ˜๋กœ resetErrorBoundary๋ฅผ ๊ตฌ์กฐ ๋ถ„ํ•ด ํ• ๋‹น์„ ํ†ตํ•ด ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋Š”๋ฐ, ์ด๋ฅผ ํ†ตํ•ด ๋ชจ๋“  ์ฟผ๋ฆฌ ์—๋Ÿฌ๋ฅผ ์ดˆ๊ธฐํ™”ํ•  ์ˆ˜ ์žˆ๋‹ค. ์•„๋ž˜ ์ฝ”๋“œ ๊ฐ™์€ ๊ฒฝ์šฐ์—๋Š” button์„ ํด๋ฆญํ•˜๋ฉด ์—๋Ÿฌ๋ฅผ ์ดˆ๊ธฐํ™”ํ•˜๊ฒŒ๋” ์ž‘์„ฑํ–ˆ๋‹ค.
import { useQueryErrorResetBoundary } from "@tanstack/react-query"; // (*)
import { ErrorBoundary } from "react-error-boundary"; // (*)

interface Props {
  children: React.ReactNode;
}

const QueryErrorBoundary = ({ children }: Props) => {
  const { reset } = useQueryErrorResetBoundary(); // (*)

  return (
    <ErrorBoundary
      onReset={reset}
      fallbackRender={({ resetErrorBoundary }) => (
        <div>
          Error!!
          <button onClick={() => resetErrorBoundary()}>Try again</button>
        </div>
      )}
    >
      {children}
    </ErrorBoundary>
  );
};

export default QueryErrorBoundary;
  • ๊ทธ๋ฆฌ๊ณ  App.js์—๋‹ค QueryErrorBoundary ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ถ”๊ฐ€ํ•˜๋ฉด ๋œ๋‹ค. ์—ฌ๊ธฐ์„œ ์ฃผ์˜ํ•  ์ ์€ queryClient ์˜ต์…˜์—๋‹ค { throwOnError: true }๋ฅผ ์ถ”๊ฐ€ํ•ด์•ผ ํ•œ๋‹ค๋Š” ์ ์ด๋‹ค. ๊ทธ๋ž˜์•ผ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ๋•Œ ErrorBoundary ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๊ฐ์ง€ํ•  ์ˆ˜ ์žˆ๋‹ค.
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
import QueryErrorBoundary from "./components/ErrorBoundary"; // (*)

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      throwOnError: true, // (*) ์—ฌ๊ธฐ์„œ๋Š” ๊ธ€๋กœ๋ฒŒ๋กœ ์„ธํŒ…ํ–ˆ์ง€๋งŒ, ๊ฐœ๋ณ„ ์ฟผ๋ฆฌ๋กœ ์„ธํŒ… ๊ฐ€๋Šฅ
    },
  },
});

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <QueryErrorBoundary>{/* ํ•˜์œ„ ์ปดํฌ๋„ŒํŠธ๋“ค */}</QueryErrorBoundary>
    </QueryClientProvider>
  );
}

Suspense

๋ชฉ์ฐจ ์ด๋™

  • ErrorBoundary๋Š” ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ๋•Œ ๋ณด์—ฌ์ฃผ๋Š” Fallback UI๋ฅผ ์„ ์–ธ์ ์œผ๋กœ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๊ณ , ๋ฆฌ์•กํŠธ ์ฟผ๋ฆฌ๋Š” Suspense์™€๋„ ๊ฒฐํ•ฉํ•ด์„œ ์„œ๋ฒ„ ํ†ต์‹  ์ƒํƒœ๊ฐ€ ๋กœ๋”ฉ ์ค‘์ผ ๋•Œ Fallback UI๋ฅผ ๋ณด์—ฌ์ค„ ์ˆ˜ ์žˆ๊ฒŒ ์„ ์–ธ์ ์œผ๋กœ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ์ฐธ๊ณ ๋กœ, Suspense ์ปดํฌ๋„ŒํŠธ๋Š” ๋ฆฌ์•กํŠธ v16๋ถ€ํ„ฐ ์ œ๊ณต๋˜๋Š” Component Lazy Loading์ด๋‚˜ Data Fetching ๋“ฑ์˜ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ๋ฅผ ํ•  ๋•Œ, ์‘๋‹ต์„ ๊ธฐ๋‹ค๋ฆฌ๋Š” ๋™์•ˆ Fallback UI(ex: Loader)๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ๊ธฐ๋Šฅ์„ ํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ๋‹ค.
import { Suspense } from "react";

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      // suspense: true, - ๐Ÿ’ก v5๋ถ€ํ„ฐ Deprecated
      // useQuery/useInfiniteQuery์™€ ๊ฐ™์€ ์ผ๋ฐ˜ ํ›… ๋Œ€์‹  useSuspenseQuery/useSuspenseInfiniteQuery์™€ ๊ฐ™์€ suspense ํ›… ์‚ฌ์šฉ
      throwOnError: true,
    },
  },
});

function App() {
  return (
    <QueryErrorBoundary>
      <Suspense fallback={<Loader />}>{/* ํ•˜์œ„ ์ปดํฌ๋„ŒํŠธ๋“ค */}</Suspense>
    </QueryErrorBoundary>;
  );
}
  • ์ฝ”๋“œ๋ฅผ ๋ณด๋ฉด ์šฐ๋ฆฌ๋Š” ์„œ๋ฒ„ ์ƒํƒœ๊ฐ€ ๋กœ๋”ฉ์ผ ๋•Œ Loader ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ณด์—ฌ์ฃผ๊ฒ ๋‹ค!๋ผ๊ณ  ์ดํ•ดํ•  ์ˆ˜ ์žˆ๋‹ค.
  • Suspense์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€์—์„œ ์–ด๋–ค ๋กœ์ง์ด ๋™์ž‘ํ•˜๋Š”์ง€ ์šฐ๋ฆฌ๋Š” ์‹ ๊ฒฝ ์“ฐ์ง€ ์•Š์•„๋„๋œ๋‹ค. ์ด์ฒ˜๋Ÿผ ๋‚ด๋ถ€ ๋ณต์žก์„ฑ์„ ์ถ”์ƒํ™”ํ•˜๋Š” ๊ฒŒ ๋ฐ”๋กœ ์„ ์–ธํ˜• ์ปดํฌ๋„ŒํŠธ์ด๋‹ค.
  • ์œ„์™€ ๊ฐ™์ด react-query(useSuspenseQuery)์™€ ๊ฒฐํ•ฉํ•œ Suspense๋Š” ์•„๋ž˜์™€ ๊ฐ™์€ ๊ณผ์ •์œผ๋กœ ๋™์ž‘ํ•œ๋‹ค.
1. Suspense mount
2. MainComponent mount
3. MainComponent์—์„œ useSuspenseQuery ํ›…์„ ์‚ฌ์šฉํ•˜์—ฌ ๋น„๋™๊ธฐ ๋ฐ์ดํ„ฐ ์š”์ฒญ
4. MainComponent unmount, fallback UI์ธ Loader mount
5. ๋น„๋™๊ธฐ ๋ฐ์ดํ„ฐ ์š”์ฒญ์ด ์™„๋ฃŒ๋˜๋ฉด fallback UI์ธ Loader unmount
6. MainComponent mount

๐Ÿ’ก New hooks for suspense

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

const fetchGroups = async (): Promise<{ data: Group[] }> => {
  const res = await axios.get("/groups");
  return res;
};

// as-is
const { data } = useQuery({
  queryKey: ["groups"],
  queryFn: fetchGroups,
  select: (data) => data.data,
});

// to-be
const { data } = useSuspenseQuery({
  queryKey: ["groups"],
  queryFn: fetchGroups,
  select: (data) => data.data,
});

๐Ÿ’ก @suspensive/react-query

  • TanStack Query(React) ๊ณต์‹ ๋ฌธ์„œ์˜ Community Resources์—์„œ๋Š” Suspense๋ฅผ ๋” ํƒ€์ž… ์„ธ์ดํ”„ํ•˜๊ฒŒ ์ž˜ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด useSuspenseQuery, useSuspenseQueries, useSuspenseInfiniteQuery๋ฅผ ์ œ๊ณตํ•˜๋Š” @suspensive/react-query๋ฅผ ์†Œ๊ฐœํ•˜๊ณ  ์žˆ๋‹ค.

  • suspensive/react-query์˜ ํ›…(useSuspenseQuery, useSuspenseQueries, useSuspenseInfiniteQuery)์€ @tanstack/react-query v5 ๋ฒ„์ „์— ์ถ”๊ฐ€(๊ด€๋ จ Pull Request)๋˜๊ณ  ๊ณต์‹ API๋กœ ์ด ํŽ˜์ด์ง€์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


Default Query Function

๋ชฉ์ฐจ ์ด๋™

  • Default Query Function ๊ณต์‹ ๋ฌธ์„œ
  • ์•ฑ ์ „์ฒด์—์„œ ๋™์ผํ•œ ์ฟผ๋ฆฌ ํ•จ์ˆ˜๋ฅผ ๊ณต์œ ํ•˜๊ณ , queryKey๋ฅผ ์‚ฌ์šฉํ•ด ๊ฐ€์ ธ์™€์•ผ ํ•  ๋ฐ์ดํ„ฐ๋ฅผ ์‹๋ณ„ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด QueryClient์— queryFn ์˜ต์…˜์„ ํ†ตํ•ด Default Query Function์„ ์ง€์ •ํ•ด ์ค„ ์ˆ˜ ์žˆ๋‹ค.
// ๊ธฐ๋ณธ ์ฟผ๋ฆฌ ํ•จ์ˆ˜
const getSuperHero = async ({ queryKey }: any) => {
  const heroId = queryKey[1];

  return await axios.get(`http://localhost:4000/superheroes/${heroId}`);
};

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      queryFn: getSuperHero,
      // ...queries options
    },
  },
});

function App() {
  return (
    <QueryClientProvider client={queryClient}>{/* ... */}</QueryClientProvider>
  );
}
  • QueryClient์— ์•ฑ ์ „์ฒด์—์„œ ์‚ฌ์šฉํ•  ์ฟผ๋ฆฌ ํ•จ์ˆ˜๋ฅผ ์ง€์ •ํ•ด ์ค€๋‹ค.
// ์‚ฌ์šฉ ์˜ˆ์‹œ
const useSuperHeroData = (heroId: string) => {
  return useQuery({ queryKey: ["super-hero", heroId] });
};
// ๋‹ค์Œ ํ˜•ํƒœ๋Š” ๋ถˆ๊ฐ€๋Šฅ
const useSuperHeroData = (heroId: string) => {
  return useQuery({
    queryKey: ["super-hero", heroId],
    queryFn: () => getSuperHero(heroId),
  });
};
  • useQuery์˜ ์ฒซ ๋ฒˆ์งธ ์ธ์ž๋กœ queryKey๋งŒ ๋„ฃ์–ด์ฃผ๋ฉด ๋‘ ๋ฒˆ์งธ ์ธ์ž์— ๋“ค์–ด๊ฐˆ queryFn์€ ์ž๋™์œผ๋กœ ์„ค์ •๋œ ๊ธฐ๋ณธ ์ฟผ๋ฆฌ ํ•จ์ˆ˜๊ฐ€ ๋“ค์–ด๊ฐ„๋‹ค.
  • ์ผ๋ฐ˜์ ์œผ๋กœ useQuery๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ์™€ ๋‹ฌ๋ฆฌ queryFn์„ ์ง€์ •ํ•˜์ง€ ์•Š๊ธฐ์— ์ฟผ๋ฆฌ ํ•จ์ˆ˜์— ์ง์ ‘ ์ธ์ž๋ฅผ ๋„ฃ์–ด์ฃผ๋Š” ํ˜•ํƒœ์˜ ์‚ฌ์šฉ์€ ๋ถˆ๊ฐ€๋Šฅํ•˜๋‹ค.

React Query Typescript

๋ชฉ์ฐจ ์ด๋™

  • React Query๋Š” TypeScript์˜ ์ œ๋„ค๋ฆญ(Generics)์„ ๋งŽ์ด ์‚ฌ์šฉํ•œ๋‹ค. ์ด๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์‹ค์ œ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค์ง€ ์•Š๊ณ  API๊ฐ€ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ฐ์ดํ„ฐ ์œ ํ˜•์„ ์•Œ ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

useQuery

ํ˜„์žฌ useQuery๊ฐ€ ๊ฐ–๊ณ  ์žˆ๋Š” ์ œ๋„ค๋ฆญ์€ 4๊ฐœ์ด๋ฉฐ, ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

  1. TQueryFnData: useQuery๋กœ ์‹คํ–‰ํ•˜๋Š” query function์˜ ์‹คํ–‰ ๊ฒฐ๊ณผ์˜ ํƒ€์ž…์„ ์ง€์ •ํ•˜๋Š” ์ œ๋„ค๋ฆญ ํƒ€์ž…์ด๋‹ค.
  2. TError: query function์˜ error ํ˜•์‹์„ ์ •ํ•˜๋Š” ์ œ๋„ค๋ฆญ ํƒ€์ž…์ด๋‹ค.
  3. TData: useQuery์˜ data์— ๋‹ด๊ธฐ๋Š” ์‹ค์งˆ์ ์ธ ๋ฐ์ดํ„ฐ์˜ ํƒ€์ž…์„ ๋งํ•œ๋‹ค. ์ฒซ ๋ฒˆ์งธ ์ œ๋„ค๋ฆญ๊ณผ์˜ ์ฐจ์ด์ ์€ select์™€ ๊ฐ™์ด query function์˜ ๋ฐ˜ํ™˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ถ”๊ฐ€ ํ•ธ๋“ค๋ง์„ ํ†ตํ•ด ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฒฝ์šฐ์— ๋Œ€์‘ํ•  ์ˆ˜ ์žˆ๋Š” ํƒ€์ž…์ด๋ผ๊ณ  ์ƒ๊ฐํ•˜๋ฉด ์ข‹๋‹ค.
  4. TQueryKey: useQuery์˜ ์ฒซ ๋ฒˆ์งธ ์ธ์ž queryKey์˜ ํƒ€์ž…์„ ๋ช…์‹œ์ ์œผ๋กœ ์ง€์ •ํ•ด ์ฃผ๋Š” ์ œ๋„ค๋ฆญ ํƒ€์ž…์ด๋‹ค.
// useQuery์˜ ํƒ€์ž…
export function useQuery<
  TQueryFnData = unknown,
  TError = DefaultError,
  TData = TQueryFnData,
  TQueryKey extends QueryKey = QueryKey
>
import { AxiosError } from "axios";

// useQuery ํƒ€์ž… ์ ์šฉ ์˜ˆ์‹œ
const { data } = useQuery<
  AxiosResponse<Hero[]>,
  AxiosError,
  string[],
  ["super-heroes", number]
>({
  queryKey: ["super-heroes", id],
  queryFn: getSuperHero,
  select: (data) => {
    const superHeroNames = data.data.map((hero) => hero.name);
    return superHeroNames;
  },
});

/**
 ์ฃผ์š” ํƒ€์ž…
 * data: string[] | undefined
 * error: AxiosError<any, any>
 * select: (data: AxiosResponse<Hero[]>): string[]
 */

useMutation

useMutation๋„ useQuery์™€ ๋™์ผํ•˜๊ฒŒ ํ˜„์žฌ 4๊ฐœ์ด๋ฉฐ, ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

  1. TData: useMutation์— ๋„˜๊ฒจ์ค€ mutation function์˜ ์‹คํ–‰ ๊ฒฐ๊ณผ์˜ ํƒ€์ž…์„ ์ง€์ •ํ•˜๋Š” ์ œ๋„ค๋ฆญ ํƒ€์ž…์ด๋‹ค.
    • data์˜ ํƒ€์ž…๊ณผ onSuccess(1๋ฒˆ์งธ ์ธ์ž) ์ธ์ž์˜ ํƒ€์ž…์œผ๋กœ ํ™œ์šฉ๋œ๋‹ค.
  2. TError: useMutation์— ๋„˜๊ฒจ์ค€ mutation function์˜ error ํ˜•์‹์„ ์ •ํ•˜๋Š” ์ œ๋„ค๋ฆญ ํƒ€์ž…์ด๋‹ค.
  3. TVariables: mutate ํ•จ์ˆ˜์— ์ „๋‹ฌ ํ•  ์ธ์ž๋ฅผ ์ง€์ •ํ•˜๋Š” ์ œ๋„ค๋ฆญ ํƒ€์ž…์ด๋‹ค.
    • onSuccess(2๋ฒˆ์งธ ์ธ์ž), onError(2๋ฒˆ์งธ ์ธ์ž), onMutate(1๋ฒˆ์งธ ์ธ์ž), onSettled(3๋ฒˆ์งธ ์ธ์ž) ์ธ์ž์˜ ํƒ€์ž…์œผ๋กœ ํ™œ์šฉ๋œ๋‹ค.
  4. TContext: mutation function์„ ์‹คํ–‰ํ•˜๊ธฐ ์ „์— ์ˆ˜ํ–‰ํ•˜๋Š” onMutate ํ•จ์ˆ˜์˜ return๊ฐ’์„ ์ง€์ •ํ•˜๋Š” ์ œ๋„ค๋ฆญ ํƒ€์ž…์ด๋‹ค.
    • onMutate์˜ ๊ฒฐ๊ณผ๊ฐ’์˜ ํƒ€์ž…์„ onSuccess(3๋ฒˆ์งธ ์ธ์ž), onError(3๋ฒˆ์งธ ์ธ์ž), onSettled(4๋ฒˆ์งธ ์ธ์ž)์—์„œ ํ™œ์šฉํ•˜๋ ค๋ฉด ํ•ด๋‹น ํƒ€์ž…์„ ์ง€์ •ํ•ด์•ผ ํ•œ๋‹ค.
export function useMutation<
  TData = unknown,
  TError = DefaultError,
  TVariables = void,
  TContext = unknown
>
// useMutation ํƒ€์ž… ์ ์šฉ ์˜ˆ์‹œ
const { mutate } = useMutation<Todo, AxiosError, number, number>({
  mutationFn: postTodo,
  onSuccess: (res, id, nextId) => {},
  onError: (err, id, nextId) => {},
  onMutate: (id) => id + 1,
  onSettled: (res, err, id, nextId) => {},
});

const onClick = () => {
  mutate(5);
};

/** 
 ์ฃผ์š” ํƒ€์ž…
 * data: Todo
 * error: AxiosError<any, any>
 * onSuccess: (res: Todo, id: number, nextId: number)
 * onError: (err: AxiosError, id: number, nextId: number)
 * onMutate: (id: number)
 * onSettled: (res: Todo, err: AxiosError, id: number, nextId: number),
*/

useInfiniteQuery

ํ˜„์žฌ useInfiniteQuery ๊ฐ–๊ณ  ์žˆ๋Š” ์ œ๋„ค๋ฆญ์€ 4๊ฐœ์ด๋ฉฐ, useQuery์™€ ์œ ์‚ฌํ•˜๋‹ค.

  1. TQueryFnData: useInfiniteQuery๋กœ ์‹คํ–‰ํ•˜๋Š” query function์˜ ์‹คํ–‰ ๊ฒฐ๊ณผ์˜ ํƒ€์ž…์„ ์ง€์ •ํ•˜๋Š” ์ œ๋„ค๋ฆญ ํƒ€์ž…์ด๋‹ค.
  2. TError: query function์˜ error ํ˜•์‹์„ ์ •ํ•˜๋Š” ์ œ๋„ค๋ฆญ ํƒ€์ž…์ด๋‹ค.
  3. TData: useInfiniteQuery์˜ data์— ๋‹ด๊ธฐ๋Š” ์‹ค์งˆ์ ์ธ ๋ฐ์ดํ„ฐ์˜ ํƒ€์ž…์„ ๋งํ•œ๋‹ค. ์ฒซ ๋ฒˆ์งธ ์ œ๋„ค๋ฆญ๊ณผ์˜ ์ฐจ์ด์ ์€ select์™€ ๊ฐ™์ด query function์˜ ๋ฐ˜ํ™˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ถ”๊ฐ€ ํ•ธ๋“ค๋ง์„ ํ†ตํ•ด ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฒฝ์šฐ์— ๋Œ€์‘ํ•  ์ˆ˜ ์žˆ๋Š” ํƒ€์ž…์ด๋ผ๊ณ  ์ƒ๊ฐํ•˜๋ฉด ์ข‹๋‹ค.
  4. TQueryKey: useInfiniteQuery์˜ ์ฒซ ๋ฒˆ์งธ ์ธ์ž queryKey์˜ ํƒ€์ž…์„ ๋ช…์‹œ์ ์œผ๋กœ ์ง€์ •ํ•ด ์ฃผ๋Š” ์ œ๋„ค๋ฆญ ํƒ€์ž…์ด๋‹ค.
export function useInfiniteQuery<
  TQueryFnData = unknown,
  TError = DefaultError,
  TData = InfiniteData<TQueryFnData>,
  TQueryKey extends QueryKey = QueryKey
>
const {
  data,
  hasNextPage,
  fetchNextPage,
  //...
} = useInfiniteQuery<
  AxiosResponse<PaginationColors>,
  AxiosError,
  InfiniteData<AxiosResponse<PaginationColors>, number>,
  ["colors"]
>({
  queryKey: ["colors"],
  queryFn: fetchColors,
  initialPageParam: 1,
  getNextPageParam: (lastPage, allPages) => {
    return allPages.length < 4 && allPages.length + 1;
  },
  // ...options
});

/**
 ์ฃผ์š” ํƒ€์ž…
 * data: InfiniteData<AxiosResponse<PaginationColors>, number> | undefined
 * error: AxiosError<any, any>
 * select: (data: InfiniteData<AxiosResponse<PaginationColors>, number>): InfiniteData<AxiosResponse<PaginationColors>, number>
 * getNextPageParam: GetNextPageParamFunction<number, AxiosResponse<PaginationColors>>
*/

๐Ÿ’ก Typescript Best Practice

  • TypeScript ๊ณต์‹ ๋ฌธ์„œ
  • ์œ„์˜ ์ œ๋„ค๋ฆญ์„ ๋ชจ๋‘ ์‚ฌ์šฉํ•˜๋Š” ๊ฑด ์ฝ”๋“œ์˜ ๋ณต์žก๋„๊ฐ€ ๋Š˜์–ด๋‚œ๋‹ค. ํ•˜์ง€๋งŒ react query๋Š” ํƒ€์ž…์„ ์ž˜ ์ „๋‹ฌํ•˜๋ฏ€๋กœ ๊ตณ์ด ์ œ๋„ค๋ฆญ์„ ๋ชจ๋‘ ์ง์ ‘ ์ œ๊ณตํ•  ํ•„์š”๊ฐ€ ์—†๋‹ค.
  • ๊ฐ€์žฅ ์ข‹์€ ๋ฐฉ๋ฒ•์€ queryFn์˜ ํƒ€์ž…์„ ์ž˜ ์ •์˜ํ•ด์„œ ํƒ€์ž… ์ถ”๋ก ์ด ์›ํ™œํ•˜๊ฒŒ ๋˜๊ฒŒ ํ•˜๋Š” ๊ฒƒ์ด๋‹ค.
const fetchGroups = async (): Promise<AxiosResponse<Group[]> => {
  return await axios.get("/groups");
};

const { data } = useQuery({
  queryKey: ["groups"],
  queryFn: fetchGroups,
  select: (data) => data.data,
});

/**
 ์ฃผ์š” ํƒ€์ž…
 * data: AxiosResponse<Group[]> | undefined
 * error: Error | null
 * select: (data: AxiosResponse<Group[]>): Group[]
 */

React Query ESLint Plugin

๋ชฉ์ฐจ ์ด๋™

  • Tanstack Query๋Š” ์ž์ฒด ESLint Plugin์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ํ•ด๋‹น ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ํ†ตํ•ด ๋ชจ๋ฒ” ์‚ฌ๋ก€๋ฅผ ์ ์šฉํ•˜๊ณ , ์ผ๋ฐ˜์ ์ธ ์‹ค์ˆ˜๋ฅผ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์„ค์น˜

$ npm i -D @tanstack/eslint-plugin-query
# or
$ pnpm add -D @tanstack/eslint-plugin-query
# or
$ yarn add -D @tanstack/eslint-plugin-query
# or
$ bun add -D @tanstack/eslint-plugin-query

์‚ฌ์šฉ ๋ฐฉ๋ฒ•(1)

  • ํ”Œ๋Ÿฌ๊ทธ์ธ์— ๋Œ€ํ•œ ๊ถŒ์žฅํ•˜๋Š” ๋ชจ๋“  rule์„ ์ ์šฉํ•˜๋ ค๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด .eslintrc.js ํŒŒ์ผ์˜ extends๋ฐฐ์—ด ์•ˆ์— plugin:@tanstack/eslint-plugin-query/recommended์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
module.exports = {
  // ...
  extends: ["plugin:@tanstack/eslint-plugin-query/recommended"],
  rules: {
    /* ์ˆ˜์ •ํ•˜๊ณ ์ž ํ•˜๋Š” rule ์ถ”๊ฐ€ ๊ฐ€๋Šฅ... */
  },
};
  • ๋ฌผ๋ก , rule์„ ๋ณ€๊ฒฝํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด rules์— ์•„๋ž˜ ์‚ฌ์šฉ๋ฐฉ๋ฒ•(2)์™€ ๊ฐ™์ด rule์„ ์ถ”๊ฐ€ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

์‚ฌ์šฉ ๋ฐฉ๋ฒ•(2)

  • ์›ํ•˜๋Š” rule์„ ๊ฐœ๋ณ„์ ์œผ๋กœ ์„ค์ •ํ•ด์„œ ์ ์šฉํ•˜๋ ค๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด .eslintrc.js ํŒŒ์ผ์˜ plugins๋ฐฐ์—ด ์•ˆ์— @tanstack/query๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ , ์ ์šฉํ•˜๊ณ ์ž ํ•˜๋Š” rules์— ๊ทœ์น™์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
module.exports = {
  // ...
  plugins: ["@tanstack/query"],
  rules: {
    "@tanstack/query/exhaustive-deps": "error",
    "@tanstack/query/no-rest-destructuring": "warn",
    "@tanstack/query/stable-query-client": "error",
  },
};

์ง€์› ๋ฒ„์ „

๋ชฉ์ฐจ ์ด๋™

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

  • Tanstack Query v5์— ํ•„์š”ํ•œ React ์ตœ์†Œ ๋ฒ„์ „์€ v18์ž…๋‹ˆ๋‹ค.

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

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

About

๐Ÿ˜ƒ TanStack Query(aka. react query) ์—์„œ ์ž์ฃผ ์‚ฌ์šฉ๋˜๋Š” ๊ฐœ๋… ์ •๋ฆฌ

Topics

Resources

Stars

Watchers

Forks

Packages

No packages published