[๐Ÿค–] React Query (TanStack Query) ์‹ฌํ™”: ๋ฐ์ดํ„ฐ ํŽ˜์นญ, ์บ์‹ฑ, ๋™๊ธฐํ™” ์ „๋žต์œผ๋กœ ์›น ์•ฑ ์„ฑ๋Šฅ ๊ทน๋Œ€ํ™”ํ•ด์š”

React Query (TanStack Query)๋ฅผ ํ™œ์šฉํ•˜์—ฌ ๋ณต์žกํ•œ ์„œ๋ฒ„ ์ƒํƒœ๋ฅผ ํšจ์œจ์ ์œผ๋กœ ๊ด€๋ฆฌํ•˜๊ณ , ์ง€๋Šฅ์ ์ธ ์บ์‹ฑ๊ณผ ์ž๋™ ๋™๊ธฐํ™” ์ „๋žต์œผ๋กœ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์„ฑ๋Šฅ๊ณผ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ๊ทน๋Œ€ํ™”ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์‹ฌ์ธต์ ์œผ๋กœ ๋‹ค๋ฃจ์–ด์š”. useQuery, useMutation, useInfiniteQuery ๋“ฑ ํ•ต์‹ฌ ํ›…๊ณผ ์‹ค์ „ ์ตœ์ ํ™” ํŒ์„ ๋ฐฐ์›Œ๋ณด์„ธ์š”.

28๋ถ„
๋‹จ์–ด: 2,518๊ฐœ
๊ฒŒ์‹œ๊ธ€ ์ธ๋„ค์ผ
์ •๋ณด

๐Ÿค– ์ด ํฌ์ŠคํŒ…์€ Gemini 2.5 Flash AI๊ฐ€ ์ž‘์„ฑํ–ˆ์–ด์š”.
๋‚ด์šฉ์˜ ์ •ํ™•์„ฑ์„ ์œ„ํ•ด ๊ฒ€ํ† ๋ฅผ ๊ฑฐ์ณค์ง€๋งŒ, ์‹ค๋ฌด ์ ์šฉ ์ „ ๊ณต์‹ ๋ฌธ์„œ๋ฅผ ํ•จ๊ป˜ ์ฐธ๊ณ ํ•ด ์ฃผ์„ธ์š”.

์œ ์šฉํ•œ ํŒ

์ด ๊ธ€์—์„œ๋Š” React Query (TanStack Query)์˜ ํ•ต์‹ฌ ๊ฐœ๋…์ธ ๋ฐ์ดํ„ฐ ํŽ˜์นญ, ์บ์‹ฑ, ๋™๊ธฐํ™” ์ „๋žต์„ ์‹ฌ์ธต์ ์œผ๋กœ ์ดํ•ดํ•˜๊ณ  ์‹ค๋ฌด์— ์ ์šฉํ•˜์—ฌ ์›น ์•ฑ ์„ฑ๋Šฅ์„ ๊ทน๋Œ€ํ™”ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ฐฐ์›Œ์š”.

์•ˆ๋…•ํ•˜์„ธ์š”, 10๋…„ ์ด์ƒ ๊ฐœ๋ฐœ ๊ฒฝ๋ ฅ์„ ๊ฐ€์ง„ ์‹œ๋‹ˆ์–ด ํ’€์Šคํƒ ๊ฐœ๋ฐœ์ž์ด์ž ๊ธฐ์ˆ  ๋ธ”๋กœ๊ทธ SEO ์ „๋ฌธ๊ฐ€ ๋ธ”๋ฃจ์˜ˆ์š”. ์ €๋Š” ์‹ค์ œ ์กด์žฌํ•˜๋Š” ๊ฐœ๋ฐœ์ž๋Š” ์•„๋‹ˆ์ง€๋งŒ, ์—ฌ๋Ÿฌ๋ถ„์˜ ์‹ค๋ฌด์— ๋„์›€์ด ๋  ๋งŒํ•œ ์–‘์งˆ์˜ ๊ธฐ์ˆ  ์ฝ˜ํ…์ธ ๋ฅผ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด ๋…ธ๋ ฅํ•˜๊ณ  ์žˆ์–ด์š”.

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

๐Ÿค” React Query, ์™œ ํ•„์š”ํ• ๊นŒ์š”?

React ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ์„œ๋ฒ„ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค๋ฃจ๋Š” ๊ฒƒ์€ ์ƒ๊ฐ๋ณด๋‹ค ๋ณต์žกํ•œ ์ผ์ด์—์š”. ๋‹จ์ˆœํžˆ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ๊ฒƒ ์™ธ์—๋„ ๋‹ค์–‘ํ•œ ๊ณ ๋ ค์‚ฌํ•ญ๋“ค์ด ์žˆ๊ฑฐ๋“ ์š”.

0๏ธโƒฃ ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ์˜ ์–ด๋ ค์›€

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

1๏ธโƒฃ ๊ธฐ์กด ๋ฐฉ์‹์˜ ํ•œ๊ณ„

๊ธฐ์กด์—๋Š” useEffect ํ›…๊ณผ useState๋ฅผ ์กฐํ•ฉํ•˜์—ฌ ๋ฐ์ดํ„ฐ๋ฅผ ํŽ˜์นญํ•˜๊ณ  ๊ด€๋ฆฌํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์•˜์–ด์š”. ํ•˜์ง€๋งŒ ์ด ๋ฐฉ์‹์€ ๋ช‡ ๊ฐ€์ง€ ํ•œ๊ณ„๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์–ด์š”.

๊ฒฝ๊ณ 

๊ธฐ์กด useEffect + useState ๋ฐฉ์‹์˜ ํ•œ๊ณ„์ 

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

์ด๋Ÿฌํ•œ ๋ฌธ์ œ๋“ค์„ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ํƒ„์ƒํ•œ ๊ฒƒ์ด ๋ฐ”๋กœ React Query์™€ ๊ฐ™์€ ์„œ๋ฒ„ ์ƒํƒœ ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์ด์—์š”.

๐Ÿš€ React Query ํ•ต์‹ฌ ๊ฐœ๋… ํŒŒํ—ค์น˜๊ธฐ

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

0๏ธโƒฃ Query Client์™€ Provider

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

// src/main.tsx ๋˜๋Š” src/App.tsx import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; import './index.css'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; // ์ฟผ๋ฆฌ ํด๋ผ์ด์–ธํŠธ ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ const queryClient = new QueryClient({ defaultOptions: { queries: { // ๊ธฐ๋ณธ์ ์œผ๋กœ ์ฟผ๋ฆฌ ์‹คํŒจ ์‹œ ์žฌ์‹œ๋„ํ•˜์ง€ ์•Š๋„๋ก ์„ค์ • (์‹ค์ œ ์•ฑ์—์„œ๋Š” ํ•„์š”์— ๋”ฐ๋ผ ์กฐ์ ˆํ•ด์š”) retry: false, // ์ฟผ๋ฆฌ๊ฐ€ 'stale' ์ƒํƒœ๋กœ ๊ฐ„์ฃผ๋˜๊ธฐ ์ „๊นŒ์ง€์˜ ์‹œ๊ฐ„ (ms) // ์ด ์‹œ๊ฐ„ ๋™์•ˆ์€ ์บ์‹œ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ , ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ๋ฆฌํŽ˜์นญํ•˜์ง€ ์•Š์•„์š”. staleTime: 1000 * 5, // 5์ดˆ ๋™์•ˆ์€ 'fresh' ์ƒํƒœ ์œ ์ง€ // ์ฟผ๋ฆฌ ๋ฐ์ดํ„ฐ๊ฐ€ ์บ์‹œ์—์„œ ์ œ๊ฑฐ๋˜๊ธฐ ์ „๊นŒ์ง€์˜ ์‹œ๊ฐ„ (ms) // ์ด ์‹œ๊ฐ„์ด ์ง€๋‚˜๋ฉด ๊ฐ€๋น„์ง€ ์ปฌ๋ ‰์…˜ ๋Œ€์ƒ์ด ๋ผ์š”. gcTime: 1000 * 60 * 5, // 5๋ถ„ ๋™์•ˆ ์บ์‹œ์— ์œ ์ง€ }, }, }); ReactDOM.createRoot(document.getElementById('root')!).render( <React.StrictMode> <QueryClientProvider client={queryClient}> <App /> {/* ๊ฐœ๋ฐœ ๋„๊ตฌ๋Š” ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ๋งŒ ํ™œ์„ฑํ™”ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์•„์š” */} <ReactQueryDevtools initialIsOpen={false} /> </QueryClientProvider> </React.StrictMode>, );

defaultOptions๋ฅผ ํ†ตํ•ด ์ „์—ญ์ ์ธ ์ฟผ๋ฆฌ ์„ค์ •์„ ํ•  ์ˆ˜ ์žˆ์–ด์š”. ํŠนํžˆ staleTime๊ณผ gcTime์€ React Query์˜ ๊ฐ•๋ ฅํ•œ ์บ์‹ฑ ์ „๋žต์˜ ํ•ต์‹ฌ์ด๋‹ˆ, ์ž ์‹œ ํ›„ ์ž์„ธํžˆ ์•Œ์•„๋ณผ๊ฒŒ์š”.

1๏ธโƒฃ useQuery ํ›…: ๋ฐ์ดํ„ฐ ํŽ˜์นญ์˜ ์‹œ์ž‘

useQuery ํ›…์€ React Query์˜ ๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ธ ํ›…์œผ๋กœ, ๋ฐ์ดํ„ฐ๋ฅผ ํŽ˜์นญํ•˜๊ณ  ๊ด€๋ฆฌํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋ผ์š”. ์ด ํ›…์€ ์„ธ ๊ฐ€์ง€ ์ฃผ์š” ์ƒํƒœ (loading, error, success)๋ฅผ ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌํ•ด ์ฃผ๊ณ , ์บ์‹ฑ, ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋ฆฌํŽ˜์นญ ๋“ฑ ๋‹ค์–‘ํ•œ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ด์š”.

useQuery๋Š” ์ตœ์†Œํ•œ ๋‘ ๊ฐœ์˜ ์ธ์ž๋ฅผ ํ•„์š”๋กœ ํ•ด์š”.

  1. queryKey: ์ฟผ๋ฆฌ๋ฅผ ๊ณ ์œ ํ•˜๊ฒŒ ์‹๋ณ„ํ•˜๋Š” ํ‚ค ๋ฐฐ์—ด์ด์—์š”. ์ด ํ‚ค๋ฅผ ํ†ตํ•ด React Query๋Š” ์บ์‹œ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ด€๋ฆฌํ•˜๊ณ , ํŠน์ • ์ฟผ๋ฆฌ๋ฅผ ๋ฌดํšจํ™”ํ•˜๊ฑฐ๋‚˜ ์—…๋ฐ์ดํŠธํ•  ์ˆ˜ ์žˆ์–ด์š”. ๊ฐ™์€ ๋ฐ์ดํ„ฐ๋ฅผ ์š”์ฒญํ•˜๋Š” ๋ชจ๋“  useQuery ํ˜ธ์ถœ์€ ๋™์ผํ•œ queryKey๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ด์š”.
  2. queryFn: ๋ฐ์ดํ„ฐ๋ฅผ ์‹ค์ œ๋กœ ํŽ˜์นญํ•˜๋Š” ๋น„๋™๊ธฐ ํ•จ์ˆ˜์˜ˆ์š”. ์ด ํ•จ์ˆ˜๋Š” Promise๋ฅผ ๋ฐ˜ํ™˜ํ•ด์•ผ ํ•ด์š”.
// src/components/Posts.tsx import React from 'react'; import { useQuery } from '@tanstack/react-query'; interface Post { id: number; title: string; body: string; } const fetchPosts = async (): Promise<Post[]> => { const response = await fetch('https://jsonplaceholder.typicode.com/posts'); if (!response.ok) { throw new Error('Failed to fetch posts'); } return response.json(); }; export default function Posts() { const { data, isLoading, isError, error } = useQuery<Post[], Error>({ queryKey: ['posts'], // ์ฟผ๋ฆฌ๋ฅผ ์‹๋ณ„ํ•˜๋Š” ๊ณ ์œ  ํ‚ค queryFn: fetchPosts, // ๋ฐ์ดํ„ฐ๋ฅผ ํŽ˜์นญํ•˜๋Š” ํ•จ์ˆ˜ }); if (isLoading) { return <Blockquote type="info">๊ฒŒ์‹œ๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ด์—์š”...</Blockquote>; } if (isError) { return <Blockquote type="error">์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์–ด์š”: {error?.message}</Blockquote>; } return ( <div> <h2>โœจ ๊ฒŒ์‹œ๊ธ€ ๋ชฉ๋ก์ด์—์š”</h2> <ul> {data?.map((post) => ( <li key={post.id}> <h3>{post.title}</h3> <p>{post.body.substring(0, 100)}...</p> </li> ))} </ul> </div> ); }

์œ„ ์˜ˆ์‹œ์ฒ˜๋Ÿผ useQuery ํ›…์„ ์‚ฌ์šฉํ•˜๋ฉด isLoading, isError, data ๋“ฑ์˜ ์ƒํƒœ๋ฅผ ์‰ฝ๊ฒŒ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์–ด์š”. React Query๋Š” ์ž๋™์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์บ์‹ฑํ•˜๊ณ , ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์ตœ์‹  ์ƒํƒœ๋กœ ์œ ์ง€ํ•˜๋ ค๊ณ  ๋…ธ๋ ฅํ•ด์š”.

2๏ธโƒฃ ๊ฐ•๋ ฅํ•œ ์บ์‹ฑ ์ „๋žต: staleTime๊ณผ gcTime

React Query์˜ ํ•ต์‹ฌ์€ ์บ์‹ฑ๊ณผ ๋ฐ์ดํ„ฐ ์‹ ์„ ๋„ ๊ด€๋ฆฌ์— ์žˆ์–ด์š”. ๋ชจ๋“  ์ฟผ๋ฆฌ ๋ฐ์ดํ„ฐ๋Š” QueryClient ๋‚ด๋ถ€์— ์บ์‹œ๋˜๋ฉฐ, ์ด ๋ฐ์ดํ„ฐ๋Š” fresh ๋˜๋Š” stale ์ƒํƒœ๋ฅผ ๊ฐ€์งˆ ์ˆ˜ ์žˆ์–ด์š”.

  • fresh (์‹ ์„ ํ•œ) ์ƒํƒœ: ๋ฐ์ดํ„ฐ๊ฐ€ ์ตœ์‹  ์ƒํƒœ๋ผ๊ณ  ๊ฐ„์ฃผ๋˜๋Š” ๊ธฐ๊ฐ„์ด์—์š”. ์ด ๊ธฐ๊ฐ„ ๋™์•ˆ์—๋Š” React Query๋Š” ๋„คํŠธ์›Œํฌ ์š”์ฒญ ์—†์ด ์บ์‹œ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์ฆ‰์‹œ ๋ฐ˜ํ™˜ํ•ด์š”. staleTime ์˜ต์…˜์œผ๋กœ ์ด ๊ธฐ๊ฐ„์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์–ด์š”. ๊ธฐ๋ณธ๊ฐ’์€ 0์œผ๋กœ, ์ฟผ๋ฆฌ๊ฐ€ ๋งˆ์šดํŠธ๋˜์ž๋งˆ์ž stale ์ƒํƒœ๋กœ ๊ฐ„์ฃผ๋ผ์š”.
  • stale (์˜ค๋ž˜๋œ) ์ƒํƒœ: ๋ฐ์ดํ„ฐ๊ฐ€ ์ตœ์‹ ์ด ์•„๋‹ ์ˆ˜๋„ ์žˆ๋‹ค๊ณ  ๊ฐ„์ฃผ๋˜๋Š” ์ƒํƒœ์˜ˆ์š”. stale ์ƒํƒœ์˜ ์ฟผ๋ฆฌ๋Š” ํŠน์ • ์กฐ๊ฑด(์˜ˆ: ์ปดํฌ๋„ŒํŠธ ๋งˆ์šดํŠธ, ์œˆ๋„์šฐ ํฌ์ปค์Šค, ๋„คํŠธ์›Œํฌ ์žฌ์—ฐ๊ฒฐ ๋“ฑ)์—์„œ ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์ž๋™์œผ๋กœ ๋ฆฌํŽ˜์นญ์„ ์‹œ๋„ํ•ด์š”. ์ด ๊ณผ์ •์—์„œ ์‚ฌ์šฉ์ž์—๊ฒŒ๋Š” ์บ์‹œ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด์—ฌ์ฃผ๋ฉด์„œ ๋ถ€๋“œ๋Ÿฌ์šด ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์–ด์š”.

gcTime (Garbage Collection Time, ์ด์ „์—๋Š” cacheTime)์€ ์ฟผ๋ฆฌ ๋ฐ์ดํ„ฐ๊ฐ€ ์บ์‹œ์—์„œ ์ œ๊ฑฐ๋˜๊ธฐ ์ „๊นŒ์ง€์˜ ์‹œ๊ฐ„์ด์—์š”. ์ฟผ๋ฆฌ ์ธ์Šคํ„ด์Šค๊ฐ€ ๋” ์ด์ƒ ์‚ฌ์šฉ๋˜์ง€ ์•Š์„ ๋•Œ (์˜ˆ: ์ปดํฌ๋„ŒํŠธ ์–ธ๋งˆ์šดํŠธ), React Query๋Š” gcTime ๋™์•ˆ ๋ฐ์ดํ„ฐ๋ฅผ ์บ์‹œ์— ๋ณด๊ด€ํ•ด์š”. ์ด ์‹œ๊ฐ„ ๋‚ด์— ๋™์ผํ•œ ์ฟผ๋ฆฌ๊ฐ€ ๋‹ค์‹œ ์‚ฌ์šฉ๋˜๋ฉด ๋„คํŠธ์›Œํฌ ์š”์ฒญ ์—†์ด ์บ์‹œ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์ฆ‰์‹œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด์š”. gcTime์ด ์ง€๋‚˜๋ฉด ํ•ด๋‹น ์ฟผ๋ฆฌ ๋ฐ์ดํ„ฐ๋Š” ๊ฐ€๋น„์ง€ ์ปฌ๋ ‰์…˜ ๋Œ€์ƒ์ด ๋˜์–ด ์บ์‹œ์—์„œ ์™„์ „ํžˆ ์ œ๊ฑฐ๋ผ์š”. ๊ธฐ๋ณธ๊ฐ’์€ 1000 * 60 * 5 (5๋ถ„)์ด์—์š”.

staleTime๊ณผ gcTime์˜ ๊ด€๊ณ„

  • staleTime: ๋ฐ์ดํ„ฐ์˜ ๋…ผ๋ฆฌ์ ์ธ ์‹ ์„ ๋„๋ฅผ ์ •์˜ํ•ด์š”. ์ด ์‹œ๊ฐ„ ๋™์•ˆ์€ ๋ฆฌํŽ˜์นญ ์—†์ด ์บ์‹œ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•ด์š”.
  • gcTime: ๋ฐ์ดํ„ฐ์˜ ๋ฌผ๋ฆฌ์ ์ธ ์บ์‹œ ์œ ์ง€ ์‹œ๊ฐ„์„ ์ •์˜ํ•ด์š”. ์ปดํฌ๋„ŒํŠธ ์–ธ๋งˆ์šดํŠธ ํ›„ ์บ์‹œ์—์„œ ๋ฐ์ดํ„ฐ๊ฐ€ ์–ธ์ œ ์‚ฌ๋ผ์งˆ์ง€๋ฅผ ๊ฒฐ์ •ํ•ด์š”.
์œ ์šฉํ•œ ํŒ

staleTime์„ ๊ธธ๊ฒŒ ์„ค์ •ํ•˜๋ฉด ๋„คํŠธ์›Œํฌ ์š”์ฒญ์„ ์ค„์ผ ์ˆ˜ ์žˆ์ง€๋งŒ, ๋ฐ์ดํ„ฐ์˜ ์ตœ์‹ ์„ฑ์ด ๋–จ์–ด์งˆ ์ˆ˜ ์žˆ์–ด์š”. ๋ฐ˜๋Œ€๋กœ staleTime์„ ์งง๊ฒŒ ์„ค์ •ํ•˜๊ฑฐ๋‚˜ 0์œผ๋กœ ๋‘๋ฉด ํ•ญ์ƒ ์ตœ์‹  ๋ฐ์ดํ„ฐ๋ฅผ ์œ ์ง€ํ•˜๋ ค๊ณ  ์‹œ๋„ํ•˜์ง€๋งŒ, ๋„คํŠธ์›Œํฌ ์š”์ฒญ์ด ๋Š˜์–ด๋‚  ์ˆ˜ ์žˆ์œผ๋‹ˆ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์š”๊ตฌ์‚ฌํ•ญ์— ๋งž์ถฐ ์ ์ ˆํžˆ ์กฐ์ ˆํ•ด์•ผ ํ•ด์š”.

3๏ธโƒฃ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋ฆฌํŽ˜์นญ๊ณผ ๋ฐ์ดํ„ฐ ๋™๊ธฐํ™”

React Query๋Š” stale ์ƒํƒœ์˜ ์ฟผ๋ฆฌ์— ๋Œ€ํ•ด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ƒํ™ฉ์—์„œ ์ž๋™์œผ๋กœ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋ฆฌํŽ˜์นญ์„ ์‹œ๋„ํ•˜์—ฌ ๋ฐ์ดํ„ฐ๋ฅผ ์ตœ์‹  ์ƒํƒœ๋กœ ๋™๊ธฐํ™”ํ•ด์š”.

  • ์ƒˆ๋กœ์šด ์ฟผ๋ฆฌ ์ธ์Šคํ„ด์Šค๊ฐ€ ๋งˆ์šดํŠธ๋  ๋•Œ (staleTime์ด ์ง€๋‚œ ๊ฒฝ์šฐ)
  • ์œˆ๋„์šฐ๊ฐ€ ๋‹ค์‹œ ํฌ์ปค์Šค ๋  ๋•Œ (๋ธŒ๋ผ์šฐ์ € ํƒญ ํ™œ์„ฑํ™” ์‹œ)
  • ๋„คํŠธ์›Œํฌ๊ฐ€ ๋‹ค์‹œ ์—ฐ๊ฒฐ๋  ๋•Œ
  • ์˜ต์…˜์œผ๋กœ ์„ค์ •๋œ refetchInterval์— ๋”ฐ๋ผ ์ฃผ๊ธฐ์ ์œผ๋กœ
  • queryClient.invalidateQueries() ๋˜๋Š” queryClient.refetchQueries()๋ฅผ ํ†ตํ•ด ์ˆ˜๋™์œผ๋กœ

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

๐Ÿ› ๏ธ ์‹ค์ „ ํ™œ์šฉ: ๋ฐ์ดํ„ฐ ํŽ˜์นญ ๋ฐ ์ตœ์ ํ™” ํŒจํ„ด

์ด์ œ React Query์˜ ํ•ต์‹ฌ ๊ฐœ๋…์„ ๋ฐ”ํƒ•์œผ๋กœ ์‹ค์ œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ์ž์ฃผ ์‚ฌ์šฉ๋˜๋Š” ํŒจํ„ด๋“ค์„ ์‚ดํŽด๋ณผ๊ฒŒ์š”.

0๏ธโƒฃ ๊ธฐ๋ณธ ๋ฐ์ดํ„ฐ ํŽ˜์นญ ์˜ˆ์‹œ

์•ž์„œ ์‚ดํŽด๋ณธ Posts ์ปดํฌ๋„ŒํŠธ์ฒ˜๋Ÿผ ๊ฐ„๋‹จํ•œ ๋ฐ์ดํ„ฐ ํŽ˜์นญ์€ useQuery๋ฅผ ํ†ตํ•ด ์‰ฝ๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด์š”. ์—ฌ๊ธฐ์„œ ์ค‘์š”ํ•œ ๊ฒƒ์€ queryKey๋ฅผ ํ†ตํ•ด ์ฟผ๋ฆฌ๋ฅผ ๋ช…ํ™•ํ•˜๊ฒŒ ์‹๋ณ„ํ•˜๋Š” ๊ฒƒ์ด์—์š”.

๋งŒ์•ฝ ํŠน์ • ID๋ฅผ ๊ฐ€์ง„ ๊ฒŒ์‹œ๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๊ณ  ์‹ถ๋‹ค๋ฉด, queryKey์— ID๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ๊ฐ ๊ฒŒ์‹œ๊ธ€์„ ๊ณ ์œ ํ•˜๊ฒŒ ์บ์‹ฑํ•  ์ˆ˜ ์žˆ์–ด์š”.

// src/components/PostDetail.tsx import React from 'react'; import { useQuery } from '@tanstack/react-query'; interface Post { id: number; title: string; body: string; } interface PostDetailProps { postId: number; } const fetchPostById = async (id: number): Promise<Post> => { const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`); if (!response.ok) { throw new Error(`Failed to fetch post with ID ${id}`); } return response.json(); }; export default function PostDetail({ postId }: PostDetailProps) { const { data, isLoading, isError, error } = useQuery<Post, Error>({ // queryKey์— postId๋ฅผ ํฌํ•จํ•˜์—ฌ ๊ฐ ๊ฒŒ์‹œ๊ธ€์„ ๊ณ ์œ ํ•˜๊ฒŒ ์บ์‹ฑํ•ด์š”. queryKey: ['post', postId], queryFn: () => fetchPostById(postId), // queryFn์€ ์ธ์ž๋ฅผ ๋ฐ›์ง€ ์•Š๋Š” ํ•จ์ˆ˜์—ฌ์•ผ ํ•˜๋ฏ€๋กœ ๋ž˜ํ•‘ํ•ด์š”. // postId๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์œผ๋ฉด ์ฟผ๋ฆฌ๋ฅผ ๋น„ํ™œ์„ฑํ™”ํ•  ์ˆ˜ ์žˆ์–ด์š”. enabled: !!postId, }); if (isLoading) { return <Blockquote type="info">๊ฒŒ์‹œ๊ธ€ ์ƒ์„ธ ์ •๋ณด๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ด์—์š”...</Blockquote>; } if (isError) { return <Blockquote type="error">์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์–ด์š”: {error?.message}</Blockquote>; } if (!data) { return <Blockquote type="warning">๊ฒŒ์‹œ๊ธ€์„ ์ฐพ์„ ์ˆ˜ ์—†์–ด์š”.</Blockquote>; } return ( <div> <h2>๐Ÿ“ {data.title}</h2> <p>{data.body}</p> </div> ); }

queryKey๋Š” ๋ฐฐ์—ด ์•ˆ์— ๋ฌธ์ž์—ด, ์ˆซ์ž, ๊ฐ์ฒด ๋“ฑ ์–ด๋–ค ์ง๋ ฌํ™” ๊ฐ€๋Šฅํ•œ ๊ฐ’์ด๋ผ๋„ ํฌํ•จํ•  ์ˆ˜ ์žˆ์–ด์š”. ์ด๋ฅผ ํ†ตํ•ด ์ฟผ๋ฆฌ์˜ ์ธ์ž๋ฅผ ๋ช…ํ™•ํžˆ ํ‘œํ˜„ํ•˜๊ณ , React Query๊ฐ€ ์บ์‹œ๋ฅผ ํšจ์œจ์ ์œผ๋กœ ๊ด€๋ฆฌํ•˜๋„๋ก ํ•  ์ˆ˜ ์žˆ์–ด์š”.

1๏ธโƒฃ ์บ์‹œ ๋ฌดํšจํ™” (Invalidation)์™€ ์ˆ˜๋™ ์—…๋ฐ์ดํŠธ (Mutation)

๋ฐ์ดํ„ฐ๋ฅผ ์ƒ์„ฑ, ์—…๋ฐ์ดํŠธ, ์‚ญ์ œํ•˜๋Š” ์ž‘์—…(Mutation)์€ ์„œ๋ฒ„ ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•˜๋ฏ€๋กœ, ๊ด€๋ จ ์ฟผ๋ฆฌ์˜ ์บ์‹œ๋ฅผ ์ตœ์‹  ์ƒํƒœ๋กœ ์œ ์ง€ํ•ด์•ผ ํ•ด์š”. React Query์˜ useMutation ํ›…๊ณผ queryClient.invalidateQueries()๋ฅผ ํ†ตํ•ด ์ด๋ฅผ ํšจ๊ณผ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด์š”.

// src/components/CreatePostForm.tsx import React, { useState } from 'react'; import { useMutation, useQueryClient } from '@tanstack/react-query'; interface NewPost { title: string; body: string; userId: number; } interface CreatedPost extends NewPost { id: number; } const createPost = async (newPost: NewPost): Promise<CreatedPost> => { const response = await fetch('https://jsonplaceholder.typicode.com/posts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(newPost), }); if (!response.ok) { throw new Error('Failed to create post'); } return response.json(); }; export default function CreatePostForm() { const queryClient = useQueryClient(); const [title, setTitle] = useState(''); const [body, setBody] = useState(''); const createPostMutation = useMutation<CreatedPost, Error, NewPost>({ mutationFn: createPost, onSuccess: () => { // ๊ฒŒ์‹œ๊ธ€ ์ƒ์„ฑ ์„ฑ๊ณต ์‹œ, 'posts' ์ฟผ๋ฆฌ ์บ์‹œ๋ฅผ ๋ฌดํšจํ™”ํ•˜์—ฌ ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค์‹œ ๋ถˆ๋Ÿฌ์˜ค๋„๋ก ํ•ด์š”. queryClient.invalidateQueries({ queryKey: ['posts'] }); setTitle(''); setBody(''); alert('๊ฒŒ์‹œ๊ธ€์ด ์„ฑ๊ณต์ ์œผ๋กœ ์ƒ์„ฑ๋˜์—ˆ์–ด์š”!'); }, onError: (error) => { alert(`๊ฒŒ์‹œ๊ธ€ ์ƒ์„ฑ์— ์‹คํŒจํ–ˆ์–ด์š”: ${error.message}`); }, }); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (!title || !body) return; createPostMutation.mutate({ title, body, userId: 1 }); }; return ( <form onSubmit={handleSubmit} style={{ border: '1px solid #ccc', padding: '20px', borderRadius: '8px' }}> <h3>์ƒˆ ๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑํ•˜๊ธฐ โœ๏ธ</h3> <div> <label htmlFor="title">์ œ๋ชฉ:</label><br /> <input id="title" type="text" value={title} onChange={(e) => setTitle(e.target.value)} disabled={createPostMutation.isPending} style={{ width: '100%', padding: '8px', marginBottom: '10px' }} /> </div> <div> <label htmlFor="body">๋‚ด์šฉ:</label><br /> <textarea id="body" value={body} onChange={(e) => setBody(e.target.value)} disabled={createPostMutation.isPending} rows={5} style={{ width: '100%', padding: '8px', marginBottom: '10px' }} /> </div> <button type="submit" disabled={createPostMutation.isPending}> {createPostMutation.isPending ? '์ƒ์„ฑ ์ค‘...' : '๊ฒŒ์‹œ๊ธ€ ์ƒ์„ฑํ•˜๊ธฐ'} </button> {createPostMutation.isError && ( <Blockquote type="error">์˜ค๋ฅ˜: {createPostMutation.error?.message}</Blockquote> )} {createPostMutation.isSuccess && ( <Blockquote type="success">๊ฒŒ์‹œ๊ธ€ ์ƒ์„ฑ ์„ฑ๊ณต!</Blockquote> )} </form> ); }

onSuccess ์ฝœ๋ฐฑ์—์„œ queryClient.invalidateQueries({ queryKey: ['posts'] })๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ posts ์ฟผ๋ฆฌ์˜ ์บ์‹œ๋ฅผ ๋ฌดํšจํ™”ํ–ˆ์–ด์š”. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด posts ์ฟผ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ์—์„œ ๋ฐ์ดํ„ฐ๊ฐ€ stale ์ƒํƒœ๋กœ ๊ฐ„์ฃผ๋˜๊ณ , ๋‹ค์Œ ๋ฒˆ ๋ Œ๋”๋ง ๋˜๋Š” ์œˆ๋„์šฐ ํฌ์ปค์Šค ์‹œ ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์ž๋™์œผ๋กœ ๋ฆฌํŽ˜์นญ๋˜์–ด ์ตœ์‹  ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๊ฒŒ ๋ผ์š”. ์ด๋Š” ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ์„ ์œ ์ง€ํ•˜๋Š” ๋งค์šฐ ํšจ๊ณผ์ ์ธ ๋ฐฉ๋ฒ•์ด์—์š”.

2๏ธโƒฃ ๋ฌดํ•œ ์Šคํฌ๋กค ๊ตฌํ˜„ (useInfiniteQuery)

๋ฌดํ•œ ์Šคํฌ๋กค์€ ๋งŽ์€ ์–‘์˜ ๋ฐ์ดํ„ฐ๋ฅผ ํšจ์œจ์ ์œผ๋กœ ๋ณด์—ฌ์ค„ ๋•Œ ์œ ์šฉํ•œ ํŒจํ„ด์ด์—์š”. React Query๋Š” useInfiniteQuery ํ›…์„ ํ†ตํ•ด ๋ฌดํ•œ ์Šคํฌ๋กค ๊ธฐ๋Šฅ์„ ์‰ฝ๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ค˜์š”.

useInfiniteQuery๋Š” getNextPageParam ์˜ต์…˜์„ ํ†ตํ•ด ๋‹ค์Œ ํŽ˜์ด์ง€๋ฅผ ๊ฐ€์ ธ์˜ฌ ๋ฐฉ๋ฒ•์„ ์ •์˜ํ•˜๊ณ , fetchNextPage ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด ๋‹ค์Œ ํŽ˜์ด์ง€๋ฅผ ์š”์ฒญํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์š”.

// src/components/InfinitePosts.tsx import React from 'react'; import { useInfiniteQuery } from '@tanstack/react-query'; interface Post { id: number; title: string; body: string; } interface PostsPage { data: Post[]; nextCursor: number | undefined; } const fetchInfinitePosts = async ({ pageParam = 0 }): Promise<PostsPage> => { // pageParam์€ 0๋ถ€ํ„ฐ ์‹œ์ž‘ํ•œ๋‹ค๊ณ  ๊ฐ€์ •ํ•˜๊ณ , ํ•œ ํŽ˜์ด์ง€์— 10๊ฐœ์”ฉ ๋ถˆ๋Ÿฌ์™€์š”. const limit = 10; const start = pageParam * limit; const response = await fetch( `https://jsonplaceholder.typicode.com/posts?_start=${start}&_limit=${limit}` ); if (!response.ok) { throw new Error('Failed to fetch infinite posts'); } const data = await response.json(); // ๋‹ค์Œ ํŽ˜์ด์ง€๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด ํ˜„์žฌ ํŽ˜์ด์ง€์˜ ๋งˆ์ง€๋ง‰ ID๋ฅผ ์ปค์„œ๋กœ ์‚ฌ์šฉํ•ด์š”. const nextCursor = data.length === limit ? pageParam + 1 : undefined; return { data, nextCursor }; }; export default function InfinitePosts() { const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, isError, error } = useInfiniteQuery<PostsPage, Error>({ queryKey: ['infinitePosts'], queryFn: fetchInfinitePosts, initialPageParam: 0, // ์ฒซ ํŽ˜์ด์ง€ ํŒŒ๋ผ๋ฏธํ„ฐ // ๋‹ค์Œ ํŽ˜์ด์ง€๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋ฐฉ๋ฒ•์„ ์ •์˜ํ•ด์š”. getNextPageParam: (lastPage) => lastPage.nextCursor, }); if (isLoading) { return <Blockquote type="info">๋ฌดํ•œ ์Šคํฌ๋กค ๊ฒŒ์‹œ๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ด์—์š”...</Blockquote>; } if (isError) { return <Blockquote type="error">์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์–ด์š”: {error?.message}</Blockquote>; } return ( <div> <h2>๐Ÿ”„ ๋ฌดํ•œ ์Šคํฌ๋กค ๊ฒŒ์‹œ๊ธ€์ด์—์š”</h2> <ul> {data?.pages.map((page, i) => ( <React.Fragment key={i}> {page.data.map((post) => ( <li key={post.id}> <h3>{post.title}</h3> <p>{post.body.substring(0, 100)}...</p> </li> ))} </React.Fragment> ))} </ul> <button onClick={() => fetchNextPage()} disabled={!hasNextPage || isFetchingNextPage} style={{ marginTop: '20px', padding: '10px 20px', cursor: 'pointer' }} > {isFetchingNextPage ? '๋” ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘...' : hasNextPage ? '๋” ๋ถˆ๋Ÿฌ์˜ค๊ธฐ' : '๋ชจ๋“  ๊ฒŒ์‹œ๊ธ€์„ ๋ถˆ๋Ÿฌ์™”์–ด์š”!'} </button> {isFetchingNextPage && <Blockquote type="info">๋‹ค์Œ ํŽ˜์ด์ง€๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ด์—์š”...</Blockquote>} </div> ); }

data.pages ๋ฐฐ์—ด์— ๊ฐ ํŽ˜์ด์ง€์˜ ๋ฐ์ดํ„ฐ๊ฐ€ ์ €์žฅ๋˜์–ด ์žˆ์–ด์š”. fetchNextPage๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด getNextPageParam์—์„œ ๋ฐ˜ํ™˜๋œ ๊ฐ’์„ queryFn์˜ pageParam์œผ๋กœ ์ „๋‹ฌํ•˜์—ฌ ๋‹ค์Œ ํŽ˜์ด์ง€๋ฅผ ๊ฐ€์ ธ์˜ค๊ฒŒ ๋ผ์š”. ์ด๋ฅผ ํ†ตํ•ด ๋ณต์žกํ•œ ๋ฌดํ•œ ์Šคํฌ๋กค ๋กœ์ง์„ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด์š”.

3๏ธโƒฃ SSR/SSG ํ™˜๊ฒฝ์—์„œ์˜ Hydration

Next.js์™€ ๊ฐ™์€ SSR/SSG ํ”„๋ ˆ์ž„์›Œํฌ์—์„œ React Query๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด, ์„œ๋ฒ„์—์„œ ๋ฏธ๋ฆฌ ๋ฐ์ดํ„ฐ๋ฅผ ํŽ˜์นญํ•˜์—ฌ HTML์— ํฌํ•จ์‹œํ‚ค๊ณ , ํด๋ผ์ด์–ธํŠธ์—์„œ๋Š” ์ด ๋ฐ์ดํ„ฐ๋ฅผ ์žฌ์‚ฌ์šฉ(Hydration)ํ•˜์—ฌ ์ดˆ๊ธฐ ๋กœ๋”ฉ ์„ฑ๋Šฅ์„ ํฌ๊ฒŒ ํ–ฅ์ƒ์‹œํ‚ฌ ์ˆ˜ ์žˆ์–ด์š”.

์ด๋ฅผ ์œ„ํ•ด์„œ๋Š” ์„œ๋ฒ„์—์„œ QueryClient๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ๋ฐ์ดํ„ฐ๋ฅผ ๋ฏธ๋ฆฌ ํŽ˜์นญํ•œ ํ›„, dehydrate ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ฟผ๋ฆฌ ์บ์‹œ๋ฅผ ์ง๋ ฌํ™”ํ•˜๊ณ , ํด๋ผ์ด์–ธํŠธ์—์„œ hydrate ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ด ์บ์‹œ๋ฅผ ์žฌ๊ตฌ์„ฑํ•ด์•ผ ํ•ด์š”.

// pages/posts/index.tsx (Next.js Pages Router ์˜ˆ์‹œ) import { dehydrate, QueryClient, HydrationBoundary } from '@tanstack/react-query'; import Posts from '../../components/Posts'; import { fetchPosts } from '../../api/posts'; // fetchPosts ํ•จ์ˆ˜๋Š” ์œ„์—์„œ ์ •์˜ํ•œ ๊ฒƒ๊ณผ ๋™์ผํ•˜๋‹ค๊ณ  ๊ฐ€์ •ํ•ด์š”. export async function getServerSideProps() { const queryClient = new QueryClient(); // ์„œ๋ฒ„์—์„œ 'posts' ์ฟผ๋ฆฌ๋ฅผ ๋ฏธ๋ฆฌ ํŽ˜์นญํ•ด์š”. await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: fetchPosts, }); return { props: { dehydratedState: dehydrate(queryClient), }, }; } export default function PostsPage({ dehydratedState }) { return ( // HydrationBoundary๋กœ ์„œ๋ฒ„์—์„œ ํŽ˜์นญ๋œ ์บ์‹œ๋ฅผ ํด๋ผ์ด์–ธํŠธ์—์„œ ์žฌ์‚ฌ์šฉํ•ด์š”. <HydrationBoundary state={dehydratedState}> <h1>๋ชจ๋“  ๊ฒŒ์‹œ๊ธ€</h1> <Posts /> </HydrationBoundary> ); }

getServerSideProps์—์„œ prefetchQuery๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์„œ๋ฒ„์—์„œ posts ๋ฐ์ดํ„ฐ๋ฅผ ๋ฏธ๋ฆฌ ๊ฐ€์ ธ์˜จ ํ›„, dehydrate(queryClient)๋ฅผ ํ†ตํ•ด ์ด ์บ์‹œ๋ฅผ ์ง๋ ฌํ™”๋œ ํ˜•ํƒœ๋กœ ๋ฐ˜ํ™˜ํ•ด์š”. ํด๋ผ์ด์–ธํŠธ์—์„œ๋Š” HydrationBoundary ์ปดํฌ๋„ŒํŠธ์— ์ด dehydratedState๋ฅผ ์ „๋‹ฌํ•˜์—ฌ ์„œ๋ฒ„์—์„œ ๊ฐ€์ ธ์˜จ ๋ฐ์ดํ„ฐ๋ฅผ ์ฆ‰์‹œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋ผ์š”. ์ด๋กœ ์ธํ•ด ์ดˆ๊ธฐ ๋กœ๋”ฉ ์‹œ ๊นœ๋นก์ž„ ์—†์ด ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด์—ฌ์ค„ ์ˆ˜ ์žˆ๊ณ , SEO์—๋„ ์œ ๋ฆฌํ•ด์š”.

์œ ์šฉํ•œ ํŒ

Next.js App Router์—์„œ๋Š” QueryClientProvider๋ฅผ ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ๋กœ ๋ถ„๋ฆฌํ•˜๊ณ , ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ํŽ˜์นญํ•œ ํ›„ ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ๋กœ ์ „๋‹ฌํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ Hydration์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด์š”. ์ด๋Š” Pages Router์™€๋Š” ์•ฝ๊ฐ„ ๋‹ค๋ฅธ ์ ‘๊ทผ ๋ฐฉ์‹์„ ๊ฐ€์ง€๋‹ˆ ๊ณต์‹ ๋ฌธ์„œ๋ฅผ ์ฐธ๊ณ ํ•ด ์ฃผ์„ธ์š”.

๐Ÿ“ ์ •๋ฆฌํ•˜๋ฉฐ: React Query๋กœ ๊ฐœ๋ฐœ ์ƒ์‚ฐ์„ฑ์„ ๋†’์—ฌ์š”

React Query๋Š” ๋‹จ์ˆœํžˆ ๋ฐ์ดํ„ฐ๋ฅผ ํŽ˜์นญํ•˜๋Š” ๊ฒƒ์„ ๋„˜์–ด, ์„œ๋ฒ„ ์ƒํƒœ ๊ด€๋ฆฌ์˜ ๋ณต์žก์„ฑ์„ ํ•ด๊ฒฐํ•˜๊ณ  ๊ฐœ๋ฐœ ์ƒ์‚ฐ์„ฑ๊ณผ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ๋™์‹œ์— ํ–ฅ์ƒ์‹œํ‚ค๋Š” ๊ฐ•๋ ฅํ•œ ๋„๊ตฌ์˜ˆ์š”.

0๏ธโƒฃ ํ•ต์‹ฌ ์š”์•ฝ

  • ์„œ๋ฒ„ ์ƒํƒœ์™€ ํด๋ผ์ด์–ธํŠธ ์ƒํƒœ ๋ถ„๋ฆฌ: React Query๋Š” ์„œ๋ฒ„ ์ƒํƒœ ๊ด€๋ฆฌ์— ํŠนํ™”๋˜์–ด ๋ณต์žกํ•œ ๋น„๋™๊ธฐ ๋กœ์ง์„ ์ถ”์ƒํ™”ํ•ด์š”.
  • ์ง€๋Šฅ์ ์ธ ์บ์‹ฑ: staleTime๊ณผ gcTime์„ ํ†ตํ•ด ๋ฐ์ดํ„ฐ์˜ ์‹ ์„ ๋„์™€ ์บ์‹œ ์œ ์ง€ ์‹œ๊ฐ„์„ ํšจ์œจ์ ์œผ๋กœ ๊ด€๋ฆฌํ•ด์š”.
  • ์ž๋™ ๋™๊ธฐํ™”: ์œˆ๋„์šฐ ํฌ์ปค์Šค, ๋„คํŠธ์›Œํฌ ์žฌ์—ฐ๊ฒฐ ๋“ฑ ๋‹ค์–‘ํ•œ ์กฐ๊ฑด์—์„œ ์ž๋™์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฆฌํŽ˜์นญํ•˜์—ฌ ์ตœ์‹  ์ƒํƒœ๋ฅผ ์œ ์ง€ํ•ด์š”.
  • ๊ฐ„๊ฒฐํ•œ API: useQuery, useMutation, useInfiniteQuery ๋“ฑ ์ง๊ด€์ ์ธ ํ›…์„ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๋ฅผ ์‰ฝ๊ฒŒ ๋‹ค๋ฃฐ ์ˆ˜ ์žˆ์–ด์š”.
  • SSR/SSG ์ง€์›: Next.js์™€ ๊ฐ™์€ ํ”„๋ ˆ์ž„์›Œํฌ์—์„œ Hydration์„ ํ†ตํ•ด ์ดˆ๊ธฐ ๋กœ๋”ฉ ์„ฑ๋Šฅ๊ณผ SEO๋ฅผ ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ์–ด์š”.

1๏ธโƒฃ ๋‹ค์Œ ์Šคํ…

์ด ๊ธ€์—์„œ ๋‹ค๋ฃฌ ๋‚ด์šฉ์€ React Query์˜ ๊ธฐ๋ณธ์ ์ธ ๊ฐœ๋…๊ณผ ํ™œ์šฉ ํŒจํ„ด์ด์—์š”. ์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ๋” ๋งŽ์€ ๊ธฐ๋Šฅ๊ณผ ์˜ต์…˜๋“ค์„ ํ™œ์šฉํ•˜์—ฌ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์š”๊ตฌ์‚ฌํ•ญ์— ๋งž๋Š” ์ตœ์ ํ™”๋ฅผ ์ง„ํ–‰ํ•  ์ˆ˜ ์žˆ์–ด์š”. ๋‹ค์Œ ๋‹จ๊ณ„๋กœ ์•„๋ž˜ ๋‚ด์šฉ์„ ํ•™์Šตํ•ด ๋ณด์‹œ๊ธธ ์ถ”์ฒœํ•ด์š”.

  • ์˜ตํ‹ฐ๋ฏธ์Šคํ‹ฑ ์—…๋ฐ์ดํŠธ: Mutation ์‹œ ์„œ๋ฒ„ ์‘๋‹ต ์ „์— UI๋ฅผ ๋ฏธ๋ฆฌ ์—…๋ฐ์ดํŠธํ•˜์—ฌ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ํ–ฅ์ƒ์‹œํ‚ค๋Š” ๋ฐฉ๋ฒ•
  • ์ฟผ๋ฆฌ ์บ์‹œ ์ˆ˜๋™ ์—…๋ฐ์ดํŠธ: queryClient.setQueryData()๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์บ์‹œ ๋ฐ์ดํ„ฐ๋ฅผ ์ง์ ‘ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๋ฐฉ๋ฒ•
  • ์ปค์Šคํ…€ ํ›…: useQuery๋ฅผ ๊ฐ์‹ธ๋Š” ์ปค์Šคํ…€ ํ›…์„ ๋งŒ๋“ค์–ด ์ฟผ๋ฆฌ ๋กœ์ง์„ ์žฌ์‚ฌ์šฉํ•˜๊ณ  ์ถ”์ƒํ™”ํ•˜๋Š” ๋ฐฉ๋ฒ•
  • ์—๋Ÿฌ ํ•ธ๋“ค๋ง ์ „๋žต: onError ์ฝœ๋ฐฑ, useErrorBoundary, suspense ์˜ต์…˜์„ ํ™œ์šฉํ•œ ๊ณ ๊ธ‰ ์—๋Ÿฌ ์ฒ˜๋ฆฌ

React Query๋Š” ํ•œ ๋ฒˆ ์ตํ˜€๋‘๋ฉด React ๊ฐœ๋ฐœ์„ ํ›จ์”ฌ ์ฆ๊ฒ๊ณ  ํšจ์œจ์ ์œผ๋กœ ๋งŒ๋“ค์–ด ์ค„ ๊ฑฐ์˜ˆ์š”. ์—ฌ๋Ÿฌ๋ถ„์˜ ํ”„๋กœ์ ํŠธ์—์„œ React Query์˜ ๊ฐ•๋ ฅํ•จ์„ ์ถฉ๋ถ„ํžˆ ๊ฒฝํ—˜ํ•ด ๋ณด์‹œ๊ธธ ๋ฐ”๋ผ์š”!

๐Ÿ“ฎ ์ฐธ๊ณ 

์—ฐ๊ด€๋œ ํฌ์ŠคํŠธ