Skip to content

Latest commit

ย 

History

History
1629 lines (1251 loc) ยท 60.7 KB

README.v4.md

File metadata and controls

1629 lines (1251 loc) ยท 60.7 KB

๐Ÿ’ป TanStack Query(React)

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

๐ŸŒŸ Contributors

contributors


TanStack Query(React v4)

แ„‰แ…ณแ„แ…ณแ„…แ…ตแ†ซแ„‰แ…ฃแ†บ 2022-08-17 แ„‹แ…ฉแ„’แ…ฎ 2 20 01

  • TanStack Query(React) v4๋Š” React Query v3์˜ ๋Œ€๋ถ€๋ถ„์˜ ๊ธฐ๋Šฅ์„ ํ˜ธํ™˜ํ•ฉ๋‹ˆ๋‹ค. ์ฃผ์š” ์ฐจ์ด์ ์€ ์•„๋ž˜ ๋ฌธ์„œ์— ๊ฐ„๋žตํ•˜๊ฒŒ ์ •๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค. ์ฐธ๊ณ ํ•ด์ฃผ์‹œ๋ฉด ๊ฐ์‚ฌ๋“œ๋ฆฝ๋‹ˆ๋‹ค ๐Ÿ™‡โ€โ™‚๏ธ

  • Migrating to TanStack Query(React) v5

  • Migrating to TanStack Query(React) v4


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

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

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

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

๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป ์ฃผ์š” ์ฐธ๊ณ  ๋ธ”๋กœ๊ทธ


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

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

๊ฐœ์š”

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

๊ธฐ๋Šฅ

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

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

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

// v4
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๋ฅผ ์ œ๊ณตํ•œ๋‹ค.
  • devtools๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด React Query์˜ ๋ชจ๋“  ๋‚ด๋ถ€ ๋™์ž‘์„ ์‹œ๊ฐํ™”ํ•˜๋Š” ๋ฐ ๋„์›€์ด ๋˜๋ฉฐ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ๋””๋ฒ„๊น… ์‹œ๊ฐ„์„ ์ ˆ์•ฝํ•  ์ˆ˜ ์žˆ๋‹ค.
  • devtools๋Š” ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ process.env.NODE_ENV === 'development' ์ธ ๊ฒฝ์šฐ์—๋งŒ ์‹คํ–‰๋œ๋‹ค, ์ฆ‰ ์ผ๋ฐ˜์ ์œผ๋กœ ๊ฐœ๋ฐœํ™˜๊ฒฝ์—์„œ๋งŒ ์ž‘๋™ํ•˜๋ฏ€๋กœ ์„ค์ •๋˜์–ด์žˆ์œผ๋ฏ€๋กœ, ํ”„๋กœ์ ํŠธ ๋ฐฐํฌ ์‹œ์— Devtools ์‚ฝ์ž…์ฝ”๋“œ๋ฅผ ์ œ๊ฑฐํ•ด์ค„ ํ•„์š”๊ฐ€ ์—†๋‹ค.
// v3
import { ReactQueryDevtools } from "react-query/devtools";

<AppContext.Provider value={user}>
  <QueryClientProvider client={queryClient}>
    // ...
    <ReactQueryDevtools initialIsOpen={false} />
  </QueryClientProvider>
</AppContext.Provider>;

options

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

v4

  • v4๋ถ€ํ„ฐ๋Š” devtools๋ฅผ ์œ„ํ•œ ๋ณ„๋„์˜ ํŒจํ‚ค์ง€ ์„ค์น˜๊ฐ€ ํ•„์š”ํ•˜๋‹ค.
$ npm i @tanstack/react-query-devtools
# or
$ pnpm add @tanstack/react-query-devtools
# or
$ yarn 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>
  );
}

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

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

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

useQuery

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

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

// ์‚ฌ์šฉ๋ฒ•(1)
const { data, isLoading, ... } =  useQuery(queryKey, queryFn, {
  // ...options ex) enabled, staleTime, ...
});

// ์‚ฌ์šฉ๋ฒ•(2)
const result = useQuery({
  queryKey,
  queryFn,
  // ...options ex) enabled, staleTime, ...
});

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

const { data, isLoading } = useQuery(["super-heroes"], getAllSuperHero);
  • useQuery๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ 3๊ฐœ์˜ ์ธ์ž๋ฅผ ๋ฐ›๋Š”๋‹ค. ์ฒซ ๋ฒˆ์งธ ์ธ์ž๊ฐ€ queryKey(ํ•„์ˆ˜), ๋‘ ๋ฒˆ์งธ ์ธ์ž๊ฐ€ queryFn(ํ•„์ˆ˜), ์„ธ ๋ฒˆ์งธ ์ธ์ž๊ฐ€ options(optional)์ด๋‹ค.

1. queryKey

// (1)
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) => {
  // ํ•ด๋‹น ์ฟผ๋ฆฌ๋Š” heroId์— ์˜์กด
  return useQuery(["super-hero", heroId], getSuperHero);
};
  • v3๊นŒ์ง€๋Š” queryKey๋กœ ๋ฌธ์ž์—ด ๋˜๋Š” ๋ฐฐ์—ด ๋ชจ๋‘ ์ง€์ •ํ•  ์ˆ˜ ์žˆ๋Š”๋ฐ, v4๋ถ€ํ„ฐ๋Š” ๋ฌด์กฐ๊ฑด ๋ฐฐ์—ด๋กœ ์ง€์ •ํ•ด์•ผ ํ•œ๋‹ค.

  • useQuery๋Š” ์ฒซ ๋ฒˆ์งธ ์ธ์ž์ธ queryKey๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋ฐ์ดํ„ฐ ์บ์‹ฑ์„ ๊ด€๋ฆฌํ•œ๋‹ค.

    • ๋งŒ์•ฝ, ์ฟผ๋ฆฌ๊ฐ€ ํŠน์ • ๋ณ€์ˆ˜์— ์˜์กดํ•œ๋‹ค๋ฉด ๋ฐฐ์—ด์—๋‹ค ์ด์–ด์„œ ๋„ฃ์–ด์ฃผ๋ฉด ๋œ๋‹ค. ex: ["super-hero", heroId, ...]
    • ์ด๋Š” ์‚ฌ์‹ค ๊ต‰์žฅํžˆ ์ค‘์š”ํ•˜๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, queryClient.setQueryData ๋“ฑ๊ณผ ๊ฐ™์ด ํŠน์ • ์ฟผ๋ฆฌ์— ์ ‘๊ทผ์ด ํ•„์š” ํ• ๋•Œ ์ดˆ๊ธฐ์— ์„ค์ •ํ•ด๋‘” ํฌ๋งท์„ ์ง€์ผœ์ค˜์•ผ ์ œ๋Œ€๋กœ ์ฟผ๋ฆฌ์— ์ ‘๊ทผ ํ•  ์ˆ˜ ์žˆ๋‹ค.
    • ์•„๋ž˜ options ์˜ˆ์ œ๋ฅผ ์‚ดํŽด๋ณด๋ฉด useSuperHeroData์˜ queryKey๋Š” ["super-hero", heroId]์ด๋‹ค. ๊ทธ๋ ‡๋‹ค๋ฉด queryClient.setQueryData๋ฅผ ์ด์šฉํ•  ๋•Œ ๋˜‘๊ฐ™์ด ["super-hero", heroId] ํฌ๋งท์„ ๊ฐ€์ ธ์•ผ ํ•œ๋‹ค. ์•ˆ๊ทธ๋Ÿฌ๋ฉด ์ œ๋Œ€๋กœ ์›ํ•˜๋Š” ์ฟผ๋ฆฌ ์ ‘๊ทผ์ด ์•ˆ๋œ๋‹ค.

2. queryFn

// (2)
const getSuperHero = async (heroId: string): Promise<AxiosResponse<Hero>> => {
  return await axios.get(`http://localhost:4000/superheroes/${heroId}`);
};

const useSuperHeroData = (heroId: string) => {
  return useQuery(["super-hero", heroId], () => getSuperHero(heroId));
};
  • useQuery์˜ ๋‘ ๋ฒˆ์งธ ์ธ์ž์ธ queryFn๋Š” Promise๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ํ•จ์ˆ˜๋ฅผ ๋„ฃ์–ด์•ผํ•œ๋‹ค.
  • ์ฐธ๊ณ ๋กœ, queryKey์˜ ์˜ˆ์ œ์™€ queryFn ์˜ˆ์ œ๊ฐ€ ์•ฝ๊ฐ„ ์ฐจ์ด์ ์ด ์žˆ๋‹ค.
    • queryKey ์˜ˆ์ œ๋Š” 2๋ฒˆ์งธ queryFn์— getSuperHero ํ•จ์ˆ˜๋ฅผ ๋ฐ”๋กœ ๋„˜๊ฒจ์ฃผ๊ณ , getSuperHero์—์„œ ๋งค๊ฐœ ๋ณ€์ˆ˜๋กœ ๊ฐ์ฒด๋ฅผ ๋ฐ›์•„์™€ ํ•ด๋‹น ๊ฐ์ฒด์˜ queryKey๋ฅผ ํ™œ์šฉํ•˜๊ณ  ์žˆ๋‹ค.
    • queryFn ์˜ˆ์ œ๋Š” ๊ทธ๋ƒฅ 2๋ฒˆ์งธ queryFn์— ํ™”์‚ดํ‘œ ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๊ณ , getSuperHero์˜ ์ธ์ž๋กœ heroId๋ฅผ ๋„˜๊ฒจ์ฃผ๊ณ  ์žˆ๋‹ค.
    • ํ•ด๋‹น ๋‘ ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์€ ๋ชจ๋‘ ์•Œ์•„์•ผ๋˜๊ณ , ๊ฒฐ๊ณผ๋Š” ๋™์ผํ•˜๋‹ค.

3. options

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

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

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

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

๐Ÿ’ก v4๋ถ€ํ„ฐ๋Š” status์˜ idle ์ƒํƒœ๊ฐ’์ด ์ œ๊ฑฐ๋˜๊ณ  fetchStatus๊ฐ€ ์ถ”๊ฐ€

  • TanStack Query(v4) ๋ถ€ํ„ฐ๋Š” status์˜ idle์ด ์ œ๊ฑฐ๋˜๊ณ , ์ƒˆ๋กœ์šด ์ƒํƒœ๊ฐ’์ธ fetchStatus๊ฐ€ ์ถ”๊ฐ€๋๋‹ค.
  • fetchStatus
    • fetching: ์ฟผ๋ฆฌ๊ฐ€ ํ˜„์žฌ ์‹คํ–‰์ค‘์ด๋‹ค.
    • paused: ์ฟผ๋ฆฌ๋ฅผ ์š”์ฒญํ–ˆ์ง€๋งŒ, ์ž ์‹œ ์ค‘๋‹จ๋œ ์ƒํƒœ์ด๋‹ค.
    • idle: ์ฟผ๋ฆฌ๊ฐ€ ํ˜„์žฌ ์•„๋ฌด ์ž‘์—…๋„ ์ˆ˜ํ–‰ํ•˜์ง€ ์•Š๋Š” ์ƒํƒœ์ด๋‹ค.

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

  • fetchStatus๋Š” HTTP ๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ ์ƒํƒœ์™€ ์ข€ ๋” ๊ด€๋ จ๋œ ์ƒํƒœ ๋ฐ์ดํ„ฐ์ด๋‹ค.

    • ์˜ˆ๋ฅผ ๋“ค์–ด, status๊ฐ€ success ์ƒํƒœ๋ผ๋ฉด ์ฃผ๋กœ fetchStatus๋Š” idle ์ƒํƒœ์ง€๋งŒ, ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ re-fetch๊ฐ€ ๋ฐœ์ƒํ•  ๋•Œ fetching ์ƒํƒœ์ผ ์ˆ˜ ์žˆ๋‹ค.
    • status๊ฐ€ ๋ณดํ†ต loading ์ƒํƒœ์ผ ๋•Œ fetchStatus๋Š” ์ฃผ๋กœ fetching๋ฅผ ๊ฐ–์ง€๋งŒ, ๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ์ด ๋˜์–ด ์žˆ์ง€ ์•Š์€ ๊ฒฝ์šฐ paused ์ƒํƒœ๋ฅผ ๊ฐ€์งˆ ์ˆ˜ ์žˆ๋‹ค.
  • ์ •๋ฆฌํ•˜์ž๋ฉด ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

    • status๋Š” data๊ฐ€ ์žˆ๋Š”์ง€ ์—†๋Š”์ง€์— ๋Œ€ํ•œ ์ƒํƒœ๋ฅผ ์˜๋ฏธํ•œ๋‹ค.
    • fetchStatus๋Š” ์ฟผ๋ฆฌ ์ฆ‰, queryFn ์š”์ฒญ์ด ์ง„ํ–‰์ค‘์ธ์ง€ ์•„๋‹Œ์ง€์— ๋Œ€ํ•œ ์ƒํƒœ๋ฅผ ์˜๋ฏธํ•œ๋‹ค.
  • why-two-different-states


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

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


staleTime๊ณผ cacheTime

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

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

refetchOnMount

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

refetchOnWindowFocus

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

Polling

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

enabled refetch

const {
  data,
  refetch,
  // ...
} = useQuery(["super-heroes"], 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๋Š” ์ฟผ๋ฆฌ๊ฐ€ ์ž๋™์œผ๋กœ ์‹คํ–‰๋˜์ง€ ์•Š๋„๋ก ํ•  ๋•Œ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค. false๋ฅผ ์ฃผ๋ฉด ์ž๋™ ์‹คํ–‰๋˜์ง€ ์•Š๋Š”๋‹ค. ๋˜ํ•œ, useQuery ๋ฆฌํ„ด ๋ฐ์ดํ„ฐ ์ค‘ status๊ฐ€ idle ์ƒํƒœ๋กœ ์‹œ์ž‘ํ•œ๋‹ค.
  • refetch๋Š” ์ฟผ๋ฆฌ๋ฅผ ์ˆ˜๋™์œผ๋กœ ๋‹ค์‹œ ์š”์ฒญํ•˜๋Š” ๊ธฐ๋Šฅ์ด๋‹ค. ์ฟผ๋ฆฌ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ์˜ค๋ฅ˜๋งŒ ๊ธฐ๋ก๋œ๋‹ค. ์˜ค๋ฅ˜๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๋ ค๋ฉด throwOnError์†์„ฑ์„ true๋กœ ํ•ด์„œ ์ „๋‹ฌํ•ด์•ผ ํ•œ๋‹ค.
  • ๋ณดํ†ต ์ž๋™์œผ๋กœ ์ฟผ๋ฆฌ ์š”์ฒญ์„ ํ•˜์ง€ ์•Š๊ณ  ๋ฒ„ํŠผ ํด๋ฆญ์ด๋‚˜ ํŠน์ • ์ด๋ฒคํŠธ๋ฅผ ํ†ตํ•ด ์š”์ฒญ์„ ์‹œ๋„ํ•  ๋•Œ ๊ฐ™์ด ์‚ฌ์šฉํ•œ๋‹ค.
  • ๋งŒ์•ฝ enabled: false๋ฅผ ์คฌ๋‹ค๋ฉด queryClient๊ฐ€ ์ฟผ๋ฆฌ๋ฅผ ๋‹ค์‹œ ๊ฐ€์ ธ์˜ค๋Š” ๋ฐฉ๋ฒ• ์ค‘ invalidateQueries์™€ refetchQueries๋ฅผ ๋ฌด์‹œํ•œ๋‹ค.

retry

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

onSuccess, onError, onSettled


const onSuccess = useCallback((data) => {
  console.log("Success", data);
}, []);

const onError = useCallback((err) => {
  console.log("Error", err);
}, []);

const onSettled = useCallback(() => {
  console.log("Settled");
}, []);

const { isLoading, isFetching, data, isError, error, refetch } = useQuery(
  ["super-hero"],
  getSuperHero,
  {
    onSuccess,
    onError,
    onSettled,
  }
);
  • onSuccess ํ•จ์ˆ˜๋Š” ์ฟผ๋ฆฌ ์š”์ฒญ์ด ์„ฑ๊ณต์ ์œผ๋กœ ์ง„ํ–‰๋ผ์„œ ์ƒˆ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๊ฑฐ๋‚˜ ์บ์‹œ๊ฐ€ ์—…๋ฐ์ดํŠธ๋  ๋•Œ๋งˆ๋‹ค ์‹คํ–‰๋œ๋‹ค.
  • onError ํ•จ์ˆ˜๋Š” ์ฟผ๋ฆฌ์— ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๊ณ  ์˜ค๋ฅ˜๊ฐ€ ์ „๋‹ฌ๋˜๋ฉด ์‹คํ–‰๋œ๋‹ค.
  • onSettled ํ•จ์ˆ˜๋Š” ์ฟผ๋ฆฌ ์š”์ฒญ์ด ์„ฑ๊ณต, ์‹คํŒจ ๋ชจ๋‘ ์‹คํ–‰๋œ๋‹ค.

select

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

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

keepPreviousData

const {
  data,
  // ...
} = useQuery(["super-heroes"], getAllSuperHero, {
  keepPreviousData: true,
});
  • keepPreviousData๋ฅผ true๋กœ ์„ค์ •ํ•˜๋ฉด ์ฟผ๋ฆฌ ํ‚ค๊ฐ€ ๋ณ€๊ฒฝ๋˜์–ด์„œ ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ๋ฅผ ์š”์ฒญํ•˜๋Š” ๋™์•ˆ์—๋„ ๋งˆ์ง€๋ง‰ data ๊ฐ’์„ ์œ ์ง€ํ•œ๋‹ค.
  • keepPreviousData์€ ํŽ˜์ด์ง€๋„ค์ด์…˜๊ณผ ๊ฐ™์€ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•  ๋•Œ ํŽธ๋ฆฌํ•˜๋‹ค. ์บ์‹ฑ ๋˜์ง€ ์•Š์€ ํŽ˜์ด์ง€๋ฅผ ๊ฐ€์ ธ์˜ฌ ๋•Œ ๋ชฉ๋ก์ด ๊นœ๋นก๊ฑฐ๋ฆฌ๋Š” ํ˜„์ƒ์„ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ๋˜ํ•œ, isPreviousData ๊ฐ’์œผ๋กœ ํ˜„์žฌ์˜ ์ฟผ๋ฆฌ ํ‚ค์— ํ•ด๋‹นํ•˜๋Š” ๊ฐ’์ธ์ง€ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค. ํŽ˜์ด์ง€๋„ค์ด์…˜์„ ์˜ˆ๋กœ ๋“ค๋ฉด, ์•„์ง ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ๊ฐ€ ์บ์‹ฑ ๋˜์ง€ ์•Š์•˜๋‹ค๋ฉด, ์ด์ „ ๋ฐ์ดํ„ฐ์ด๋ฏ€๋กœ true๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ณ  ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ๋ฐ›์•„์ ธ ์™”๋‹ค๋ฉด ์ด์ „๋ฐ์ดํ„ฐ๊ฐ€ ์•„๋‹ˆ๋ฏ€๋กœ false๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

placeholderData

function Todos() {
  const placeholderData = useMemo(() => generateFakeTodos(), []);
  const result = useQuery(["todos"], fetchTodos, {
    placeholderData,
  });
}
  • placeholderData๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด mock ๋ฐ์ดํ„ฐ ์„ค์ •๋„ ๊ฐ€๋Šฅํ•˜๋‹ค. ๋Œ€์‹  ์บ์‹ฑ์ด ์•ˆ๋œ๋‹ค๋Š” ๋‹จ์ ์ด ์žˆ๋‹ค.

Parallel

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

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

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

  • useQueries๊ฐ€ v4๋ถ€ํ„ฐ ์ฟผ๋ฆฌ๋ฅผ ๋„˜๊ธฐ๋Š” ๋ฐฉ์‹์ด ๋ณ€๊ฒฝ๋๋‹ค. ์ฐจ์ด์ ์œผ๋กœ๋Š” queriesํ”„๋กœํผํ‹ฐ๋ฅผ ๊ฐ€์ง„ ๊ฐ์ฒด๋ฅผ ๋„˜๊ฒจ์ค˜์•ผ ํ•œ๋‹ค.
// v4
const queryResults = useQueries({
  queries: [
    {
      queryKey: ["super-hero", 1],
      queryFn: () => fetchSuperHero(1),
      staleTime: Infinity, // ๋‹ค์Œ๊ณผ ๊ฐ™์ด option ์ถ”๊ฐ€ ๊ฐ€๋Šฅ
    },
    {
      queryKey: ["super-hero", 2],
      queryFn: () => fetchSuperHero(2),
      staleTime: 0,
    },
    // ...
  ],
});

Dependent Queries

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

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

const channelId = user?.data.channelId;

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

useQueryClient

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

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

const queryClient = useQueryClient();

Initial Query Data

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

  • ์ฟผ๋ฆฌ์— ๋Œ€ํ•œ ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ๊ฐ€ ํ•„์š”ํ•˜๊ธฐ ์ „์— ์บ์‹œ์— ์ œ๊ณตํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ์žˆ๋‹ค.
  • initialData ์˜ต์…˜์„ ํ†ตํ•ด์„œ ์ฟผ๋ฆฌ๋ฅผ ๋ฏธ๋ฆฌ ์ฑ„์šฐ๋Š” ๋ฐ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ดˆ๊ธฐ ๋กœ๋“œ ์ƒํƒœ๋„ ๊ฑด๋„ˆ๋›ธ ์ˆ˜๋„ ์žˆ๋‹ค.
const useSuperHeroData = (heroId: string) => {
  const queryClient = useQueryClient();
  return useQuery(["super-hero", heroId], fetchSuperHero, {
    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 };
      return undefined;
    },
  });
};

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

Prefetching

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

  • prefetch๋Š” ๋ง ๊ทธ๋Œ€๋กœ ๋ฏธ๋ฆฌ fetchํ•ด์˜ค๊ฒ ๋‹ค๋Š” ์˜๋ฏธ์ด๋‹ค.
  • ๋น„๋™๊ธฐ ์š”์ฒญ์€ ๋ฐ์ดํ„ฐ ์–‘์ด ํด ์ˆ˜๋ก ๋ฐ›์•„์˜ค๋Š” ์†๋„๊ฐ€ ๋Š๋ฆฌ๊ณ , ์‹œ๊ฐ„์ด ์˜ค๋ž˜๊ฑธ๋ฆฐ๋‹ค. ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ์œ„ํ•ด ๋ฐ์ดํ„ฐ๋ฅผ ๋ฏธ๋ฆฌ ๋ฐ›์•„์™€์„œ ์บ์‹ฑํ•ด๋†“์œผ๋ฉด? ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›๊ธฐ์ „์— ์‚ฌ์šฉ์ž๊ฐ€ ์บ์‹ฑ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ์–ด UX์— ์ข‹์€ ์˜ํ–ฅ์„ ์ค„ ์ˆ˜ ์žˆ๋‹ค.
    • ์˜ˆ๋ฅผ ๋“ค์–ด ํŽ˜์ด์ง€๋„ค์ด์…˜์„ ๊ตฌํ˜„ํ–ˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•˜๋ฉด, ํŽ˜์ด์ง€1์—์„œ ํŽ˜์ด์ง€2๋กœ ์ด๋™ํ–ˆ์„ ๋•Œ ํŽ˜์ด์ง€3์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฏธ๋ฆฌ ๋ฐ›์•„๋†“๋Š” ๊ฒƒ์ด๋‹ค!
  • react query์—์„œ๋Š” queryClient.prefetchQuery์„ ํ†ตํ•ด์„œ prefetch ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•œ๋‹ค.
const prefetchNextPosts = async (nextPage: number) => {
  const queryClient = useQueryClient();
  // ํ•ด๋‹น ์ฟผ๋ฆฌ์˜ ๊ฒฐ๊ณผ๋Š” ์ผ๋ฐ˜ ์ฟผ๋ฆฌ๋“ค์ฒ˜๋Ÿผ ์บ์‹ฑ๋œ๋‹ค.
  await queryClient.prefetchQuery(
    ["posts", nextPage],
    () => fetchPosts(nextPage),
    { ...options }
  );
};

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

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

Infinite Queries

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

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

const fetchColors = async ({
  pageParam = 1,
}: {
  pageParam: number;
}): Promise<AxiosResponse<PaginationColors>> => {
  return await axios.get(`http://localhost:4000/colors?page=${pageParam}`);
};

const InfiniteQueries = () => {
  const { data, hasNextPage, isFetching, isFetchingNextPage, fetchNextPage } =
    useInfiniteQuery(["colors"], fetchColors, {
      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 ๋“ฑ์ด ์ถ”๊ฐ€์ ์œผ๋กœ ์žˆ๋‹ค.
    • fetchNextPage: ๋‹ค์Œ ํŽ˜์ด์ง€๋ฅผ fetch ํ•  ์ˆ˜ ์žˆ๋‹ค.
    • fetchPreviousPage: ์ด์ „ ํŽ˜์ด์ง€๋ฅผ fetch ํ•  ์ˆ˜ ์žˆ๋‹ค.
    • isFetchingNextPage: fetchNextPage ๋ฉ”์„œ๋“œ๊ฐ€ ๋‹ค์Œ ํŽ˜์ด์ง€๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋™์•ˆ true์ด๋‹ค.
    • isFetchingPreviousPage: fetchPreviousPage ๋ฉ”์„œ๋“œ๊ฐ€ ์ด์ „ ํŽ˜์ด์ง€๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋™์•ˆ true์ด๋‹ค.
    • hasNextPage: ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋Š” ๋‹ค์Œ ํŽ˜์ด์ง€๊ฐ€ ์žˆ์„ ๊ฒฝ์šฐ true์ด๋‹ค.
    • hasPreviousPage: ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋Š” ์ด์ „ ํŽ˜์ด์ง€๊ฐ€ ์žˆ์„ ๊ฒฝ์šฐ true์ด๋‹ค.

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

  • pageParam์ด๋ผ๋Š” ํ”„๋กœํผํ‹ฐ๊ฐ€ ์กด์žฌํ•˜๋ฉฐ, queryFn์— ํ• ๋‹นํ•ด์ค˜์•ผ ํ•œ๋‹ค. ์ด๋•Œ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ์ดˆ๊ธฐ ํŽ˜์ด์ง€ ๊ฐ’์„ ์„ค์ • ํ•ด์ค˜์•ผํ•œ๋‹ค.
  • getNextPageParam์„ ์ด์šฉํ•ด์„œ ํŽ˜์ด์ง€๋ฅผ ์ฆ๊ฐ€์‹œํ‚ฌ ์ˆ˜ ์žˆ๋‹ค.
    • getNextPageParam์˜ ์ฒซ ๋ฒˆ์งธ ์ธ์ž lastPage๋Š” fetch ํ•ด์˜จ ๊ฐ€์žฅ ์ตœ๊ทผ์— ๊ฐ€์ ธ์˜จ ํŽ˜์ด์ง€ ๋ชฉ๋ก์ด๋‹ค.
    • ๋‘ ๋ฒˆ์งธ ์ธ์ž allPages๋Š” ํ˜„์žฌ๊นŒ์ง€ ๊ฐ€์ ธ์˜จ ๋ชจ๋“  ํŽ˜์ด์ง€ ๋ฐ์ดํ„ฐ์ด๋‹ค.
  • getPreviousPageParam๋„ ์กด์žฌํ•˜๋ฉฐ, getNextPageParam์™€ ๋ฐ˜๋Œ€์˜ ์†์„ฑ์„ ๊ฐ–๊ณ  ์žˆ๋‹ค.

๐Ÿ’ก pageParam

  • queryFn์— ๋„˜๊ฒจ์ฃผ๋Š” pageParam๊ฐ€ ๋‹จ์ˆœํžˆ ๋‹ค์Œ page์˜ ๊ฐ’๋งŒ์„ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์€ ์•„๋‹ˆ๋‹ค.
  • pageParam ๊ฐ’์€ getNextPageParam์—์„œ ์›ํ•˜๋Š” ํ˜•ํƒœ๋กœ ๋ณ€๊ฒฝ์‹œ์ผœ์ค„ ์ˆ˜ ์žˆ๋‹ค.
  • ๋ฌด์Šจ ๋ง์ธ์ง€ ์˜ˆ์‹œ๋ฅผ ๋ณด๋ฉด ์ดํ•ด๊ฐ€ ์‰ฝ๋‹ค. ๐Ÿ‘ ์•„๋ž˜์™€ ๊ฐ™์ด getNextPageParam์—์„œ ๋ฐ˜ํ™˜ ๋ฐ์ดํ„ฐ๊ฐ€ ๋‹จ์ˆœํžˆ ๋‹ค์Œ ํŽ˜์ด์ง€ ๊ฐ’์ด ์•„๋‹Œ ๊ฐ์ฒด๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค๊ณ  ํ•ด๋ณด์ž.
const { data } = useInfiniteQuery(["colors"], fetchColors, {
  getNextPageParam: (lastPage, allPages) => {
    return (
      allPages.length < 4 && {
        page: allPages.length + 1,
        etc: "hi",
      };
    )
  },
});
  • ๊ทธ๋Ÿฌ๋ฉด queryFn์— ๋„ฃ์€ pageParams์—์„œ 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์˜ ๋ฐ˜ํ™˜ ๊ฐ’์„ ์„ค์ •ํ•˜๋ฉด ๋œ๋‹ค.

๐Ÿ’ก refetchPage

  • ์ „์ฒด ํŽ˜์ด์ง€ ์ค‘ ์ผ๋ถ€๋งŒ ์ง์ ‘ refetchํ•˜๊ณ  ์‹ถ์„ ๋•Œ์—๋Š”, useInfiniteQuery๊ฐ€ ๋ฐ˜ํ™˜ํ•˜๋Š” refetch ํ•จ์ˆ˜์— refetchPage๋ฅผ ๋„˜๊ฒจ์ฃผ๋ฉด ๋œ๋‹ค.
  • refetchPage๋Š” ๊ฐ ํŽ˜์ด์ง€์— ๋Œ€ํ•ด ์‹คํ–‰๋˜๋ฉฐ, ์ด ํ•จ์ˆ˜๊ฐ€ true๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ํŽ˜์ด์ง€๋งŒ refetch๊ฐ€ ๋œ๋‹ค.
const { refetch } = useInfiniteQuery(["colors"], fetchColors, {
  getNextPageParam: (lastPage, allPages) => {
    return allPages.length < 4 && allPages.length + 1;
  },
});

// ์ฒซ๋ฒˆ์งธ ํŽ˜์ด์ง€๋งŒ refetch ํ•ฉ๋‹ˆ๋‹ค.
refetch({ refetchPage: (page, index) => index === 0 });

useMutation

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

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

  const onCreateTodo = (e) => {
    e.preventDefault();
    mutation.mutate({ title });
  };

  return <>...</>;
};
  • 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: 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

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

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

const queryClient = useQueryClient();

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

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

// ์•„๋ž˜ query๋“ค ๋ชจ๋‘ ๋ฌดํšจํ™” ๋œ๋‹ค.
const query = useQuery(["super-heroes", "superman"], fetchSuperHero);

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

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

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

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

  return useMutation(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(addSuperHero, {
    async onMutate(newHero) {
      // ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ๋ฅผ ๋ฎ์–ด์“ฐ์ง€ ์•Š๊ธฐ ์œ„ํ•ด ์ฟผ๋ฆฌ๋ฅผ ์ˆ˜๋™์œผ๋กœ ์‚ญ์ œํ•œ๋‹ค.
      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);
    },
    // ์˜ค๋ฅ˜ ๋˜๋Š” ์„ฑ๊ณต ํ›„์—๋Š” ํ•ญ์ƒ ๋ฆฌํ”„๋ ˆ์‰ฌ
    onSettled() {
      queryClient.invalidateQueries(["super-heroes"]);
    },
  });
};
  • ์ฐธ๊ณ ๋กœ ์œ„ ์˜ˆ์ œ์—์„œ cancelQueries๋Š” ์ฟผ๋ฆฌ๋ฅผ ์ˆ˜๋™์œผ๋กœ ์ทจ์†Œ์‹œํ‚ฌ ์ˆ˜ ์žˆ๋‹ค. ์ทจ์†Œ์‹œํ‚ฌ query์˜ queryKey๋ฅผ cancelQueries์˜ ์ธ์ž๋กœ ๋ณด๋‚ด ์‹คํ–‰์‹œํ‚จ๋‹ค.
  • ์˜ˆ๋ฅผ ๋“ค์–ด, ์š”์ฒญ์„ ์™„๋ฃŒํ•˜๋Š” ๋ฐ ์‹œ๊ฐ„์ด ์˜ค๋ž˜ ๊ฑธ๋ฆฌ๋Š” ๊ฒฝ์šฐ, ์‚ฌ์šฉ์ž๊ฐ€ ์ทจ์†Œ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜์—ฌ ์š”์ฒญ์„ ์ค‘์ง€ํ•˜๋Š” ๊ฒฝ์šฐ ์ด์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

useQueryErrorResetBoundary

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

  • useQueryErrorResetBoundary v4
  • 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
  • ์„ค์น˜ ํ›„์— ์•„๋ž˜์™€ ๊ฐ™์€ 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 ์˜ต์…˜์—๋‹ค { useErrorBoundary: true }๋ฅผ ์ถ”๊ฐ€ํ•ด์•ผ ํ•œ๋‹ค๋Š” ์ ์ด๋‹ค. ๊ทธ๋ž˜์•ผ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ๋•Œ ErrorBoundary ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๊ฐ์ง€ํ•  ์ˆ˜ ์žˆ๋‹ค.
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
import QueryErrorBoundary from "./components/ErrorBoundary"; // (*)

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      useErrorBoundary: 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: {
      useErrorBoundary: true,
      suspense: true,  // (*) ์—ฌ๊ธฐ์„œ๋Š” ๊ธ€๋กœ๋ฒŒ๋กœ ์…‹ํŒ…ํ–ˆ์ง€๋งŒ ๊ฐœ๋ณ„ ์ฟผ๋ฆฌ๋กœ ์…‹ํŒ…๊ฐ€๋Šฅ
    },
  },
});

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

๐Ÿ’ก @suspensive/react-query

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

AS-IS (@tanstack/react-query)

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

const Example = () => {
  const query = useQuery({
    queryKey,
    queryFn,
    suspense: true,
  });

  query.data; // TData | undefined

  if (query.isSuccess) {
    query.data; // TData
  }
};

TO-BE (@suspensive/react-query)

import { useSuspenseQuery } from "@suspensive/react-query";

const Example = () => {
  const query = useSuspenseQuery({
    queryKey,
    queryFn,
  }); // suspense: true๊ฐ€ ๊ธฐ๋ณธ์ž…๋‹ˆ๋‹ค.

  // isSuccess์œผ๋กœ type narrowing์ด ํ•„์š”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
  query.data; // TData
};

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


Default Query Function

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

  • ์•ฑ ์ „์ฒด์—์„œ ๋™์ผํ•œ ์ฟผ๋ฆฌ ํ•จ์ˆ˜๋ฅผ ๊ณต์œ ํ•˜๊ณ , queryKey๋ฅผ ์‚ฌ์šฉํ•ด ๊ฐ€์ ธ์™€์•ผ ํ•  ๋ฐ์ดํ„ฐ๋ฅผ ์‹๋ณ„ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด QueryClient์— queryFn ์˜ต์…˜์„ ํ†ตํ•ด Default Query Function์„ ์ง€์ •ํ•ด ์ค„ ์ˆ˜ ์žˆ๋‹ค.
  • Default Query Function v4
// ๊ธฐ๋ณธ ์ฟผ๋ฆฌ ํ•จ์ˆ˜
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 queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      queryFn: getSuperHero,
      // ...queries options
    },
  },
});

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

React Query Typescript

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

  • React Query๋Š” TypeScript์˜ ์ œ๋„ค๋ฆญ(Generics)์„ ๋งŽ์ด ์‚ฌ์šฉํ•œ๋‹ค. ์ด๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์‹ค์ œ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค์ง€ ์•Š๊ณ  API๊ฐ€ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ฐ์ดํ„ฐ ์œ ํ˜•์„ ์•Œ ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.
  • ๊ณต์‹ ๋ฌธ์„œ์—์„œ๋Š” ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ๋ฅผ ๊ทธ๋‹ค์ง€ ๊ด‘๋ฒ”์œ„ํ•˜๊ฒŒ ๋‹ค๋ฃจ์ง€๋Š” ์•Š๊ณ , useQuery๋ฅผ ํ˜ธ์ถœํ•  ๋•Œ ๊ธฐ๋Œ€ํ•˜๋Š” ์ œ๋„ค๋ฆญ์„ ๋ช…์‹œ์ ์œผ๋กœ ์ง€์ •ํ•˜๋„๋ก ์•Œ๋ ค์ค€๋‹ค.

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 = unknown,
  TData = TQueryFnData,
  TQueryKey extends QueryKey = QueryKey
>
// useQuery ํƒ€์ž… ์ ์šฉ ์˜ˆ์‹œ
const { data } = useQuery<
  SuperHeros,
  AxiosError,
  SuperHeroName[],
  [string, number]
>(["super-heros", id], 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 = unknown,
  TVariables = void,
  TContext = unknown
>
// useMutation ํƒ€์ž… ์ ์šฉ ์˜ˆ์‹œ
const { mutate } = useMutation<Todo, AxiosError, number, number>(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 = unknown,
  TData = TQueryFnData,
  TQueryKey extends QueryKey = QueryKey
>
const {
  data,
  hasNextPage,
  fetchNextPage,
  // ...
} = useInfiniteQuery<
  AxiosResponse<PaginationColors>,
  AxiosError,
  InfiniteData<AxiosResponse<PaginationColors>, number>,
  ["colors"]
>(["colors"], fetchColors, {
  getNextPageParam: (lastPage) => {
    return allPages.length < 4 && allPages.length + 1;
  },
  ...options,
});

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

๐Ÿ’ก Typescript Best Practice

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

const { data } = useQuery(["groups"], 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-deprecated-options": "error",
    "@tanstack/query/prefer-query-object-syntax": "error",
    "@tanstack/query/stable-query-client": "error",
  },
};

์ง€์› ๋ฒ„์ „

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

  • Tanstack Query v4์— ํ•„์š”ํ•œ TypeScript ์ตœ์†Œ ๋ฒ„์ „์€ v4.1 ์ž…๋‹ˆ๋‹ค.
  • Tanstack Query v4์˜ ๋ธŒ๋ผ์šฐ์ € ๋ณ„ ์ง€์› ๋ฒ„์ „์€ ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.
Chrome >= 73
Firefox >= 78
Edge >= 79
Safari >= 12.1
iOS >= 12.2
Opera >= 53