Search

React Query

React Query (TanStack Query)

๋ชฉ์ฐจ

1. React Query๋ž€?

์„œ๋ฒ„ ๋ฐ์ดํ„ฐ๋ฅผ React ์ปดํฌ๋„ŒํŠธ์—์„œ ์‰ฝ๊ฒŒ ๊ฐ€์ ธ์˜ค๊ณ , ์บ์‹ฑํ•˜๊ณ , ๋™๊ธฐํ™”ํ•˜๊ธฐ ์œ„ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

๊ธฐ์กด ๋ฐฉ์‹์˜ ๋ฌธ์ œ

// โŒ ๊ธฐ์กด ๋ฐฉ์‹ - ๋งค๋ฒˆ ์ง์ ‘ ๊ด€๋ฆฌํ•ด์•ผ ํ•  ๊ฒƒ๋“ค const [data, setData] = useState(null) const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState(null) useEffect(() => { setIsLoading(true) fetch('/api/boards') .then(res => res.json()) .then(data => { setData(data); setIsLoading(false) }) .catch(err => { setError(err); setIsLoading(false) }) }, [])
JavaScript
๋ณต์‚ฌ

React Query ๋ฐฉ์‹

// โœ… React Query - ์บ์‹ฑ, ๋กœ๋”ฉ, ์—๋Ÿฌ, ๋ฆฌํŒจ์น˜ ์ž๋™ ์ฒ˜๋ฆฌ const { data, isLoading, isError } = useQuery({ queryKey: ['boards'], queryFn: () => fetch('/api/boards').then(res => res.json()), })
JavaScript
๋ณต์‚ฌ

React Query๊ฐ€ ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒƒ๋“ค

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

2. ์„ค์น˜

npm install @tanstack/react-query @tanstack/react-query-devtools
Bash
๋ณต์‚ฌ
ํŒจํ‚ค์ง€
์—ญํ• 
@tanstack/react-query
ํ•ต์‹ฌ ๊ธฐ๋Šฅ (useQuery, useMutation ๋“ฑ)
@tanstack/react-query-devtools
๊ฐœ๋ฐœ์šฉ ์บ์‹œ ์ƒํƒœ ์‹œ๊ฐํ™” ๋„๊ตฌ

3. QueryClient - ์บ์‹œ ์ €์žฅ์†Œ

QueryClient๋Š” React Query์˜ ์บ์‹œ ์ €์žฅ์†Œ์ด์ž ์„ค์ • ์ปจํ…Œ์ด๋„ˆ๋‹ค. ์•ฑ ์ „์ฒด์—์„œ ํ•˜๋‚˜์˜ ์ธ์Šคํ„ด์Šค๋งŒ ์ƒ์„ฑํ•œ๋‹ค.
import { QueryClient } from '@tanstack/react-query' const queryClient = new QueryClient({ defaultOptions: { queries: { retry: 1, // ๋„คํŠธ์›Œํฌ ์‹คํŒจ ์‹œ ์žฌ์‹œ๋„ ํšŸ์ˆ˜ (๊ธฐ๋ณธ๊ฐ’: 3) staleTime: 1000 * 30, // ๋ฐ์ดํ„ฐ๋ฅผ "์‹ ์„ "ํ•˜๋‹ค๊ณ  ๋ณด๋Š” ์‹œ๊ฐ„(ms) (๊ธฐ๋ณธ๊ฐ’: 0) }, }, })
JavaScript
๋ณต์‚ฌ

defaultOptions.queries ์ฃผ์š” ์˜ต์…˜

์˜ต์…˜
ํƒ€์ž…
๊ธฐ๋ณธ๊ฐ’
์„ค๋ช…
retry
number \| boolean
3
์‹คํŒจ ์‹œ ์žฌ์‹œ๋„ ํšŸ์ˆ˜. false๋ฉด ์žฌ์‹œ๋„ ์•ˆ ํ•จ
staleTime
number
0
์บ์‹œ๋ฅผ "์‹ ์„ "ํ•˜๋‹ค๊ณ  ๋ณด๋Š” ์‹œ๊ฐ„(ms). ์ด ์‹œ๊ฐ„ ์ด๋‚ด๋ฉด ์„œ๋ฒ„ ์žฌ์š”์ฒญ ์—†์Œ
gcTime
number
5 * 60 * 1000
์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” ์บ์‹œ๋ฅผ ๋ฉ”๋ชจ๋ฆฌ์—์„œ ์œ ์ง€ํ•˜๋Š” ์‹œ๊ฐ„(ms)
refetchOnWindowFocus
boolean
true
์ฐฝ ํฌ์ปค์Šค ์‹œ ์ž๋™ ๋ฆฌํŒจ์น˜ ์—ฌ๋ถ€
refetchOnMount
boolean
true
์ปดํฌ๋„ŒํŠธ ๋งˆ์šดํŠธ ์‹œ ๋ฆฌํŒจ์น˜ ์—ฌ๋ถ€

์ด ํ”„๋กœ์ ํŠธ ์„ค์ •

// src/main.jsx const queryClient = new QueryClient({ defaultOptions: { queries: { retry: 1, // ๊ธฐ๋ณธ 3ํšŒ โ†’ 1ํšŒ๋กœ ์ค„์—ฌ ๋น ๋ฅธ ์—๋Ÿฌ ๊ฐ์ง€ staleTime: 1000 * 30, // 30์ดˆ๊ฐ„ ์บ์‹œ ์œ ํšจ (๊ฒŒ์‹œํŒ ํŠน์„ฑ์ƒ ์ž์ฃผ ๋ฐ”๋€Œ์ง€ ์•Š์Œ) }, }, })
JavaScript
๋ณต์‚ฌ

4. QueryClientProvider - ์ „์—ญ ๊ณต๊ธ‰์ž

QueryClientProvider๋Š” React Context๋ฅผ ์‚ฌ์šฉํ•ด ์•ฑ ์ „์ฒด์— QueryClient๋ฅผ ๊ณต๊ธ‰ํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ๋‹ค. useQuery, useMutation, useQueryClient ๋“ฑ์˜ ํ›…์ด ์ด Context์—์„œ QueryClient๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค.
// src/main.jsx import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { ReactQueryDevtools } from '@tanstack/react-query-devtools' const queryClient = new QueryClient({ /* ์„ค์ • */ }) createRoot(document.getElementById('root')).render( <StrictMode> <QueryClientProvider client={queryClient}> <App /> <ReactQueryDevtools initialIsOpen={false} /> </QueryClientProvider> </StrictMode> )
JavaScript
๋ณต์‚ฌ

๊ตฌ์กฐ ์›๋ฆฌ

<QueryClientProvider client={queryClient}> โ† QueryClient๋ฅผ Context์— ๋“ฑ๋ก <App> <ListPage> <List> useQuery(...) โ† Context์—์„œ QueryClient๋ฅผ ๊บผ๋‚ด ์บ์‹œ ์กฐํšŒ </List> </ListPage> </App> </QueryClientProvider>
Plain Text
๋ณต์‚ฌ
์ฃผ์˜์‚ฌํ•ญQueryClientProvider ๋ฐ–์—์„œ useQuery ๋“ฑ์„ ํ˜ธ์ถœํ•˜๋ฉด ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. <App />์„ ๋ฐ˜๋“œ์‹œ <QueryClientProvider> ์•ˆ์— ๋„ฃ์–ด์•ผ ํ•œ๋‹ค.

5. useQuery - ๋ฐ์ดํ„ฐ ์กฐํšŒ

์„œ๋ฒ„์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ์–ด์˜ค๋Š” ํ›…. GET ์š”์ฒญ์— ์ฃผ๋กœ ์‚ฌ์šฉํ•œ๋‹ค.

๊ธฐ๋ณธ ๊ตฌ์กฐ

const { data, isLoading, isError, error } = useQuery({ queryKey: ['๊ณ ์œ ํ•œ ํ‚ค'], // ์บ์‹œ ์‹๋ณ„์ž queryFn: () => fetch(...), // ์‹ค์ œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ํ•จ์ˆ˜ })
JavaScript
๋ณต์‚ฌ

queryKey - ์บ์‹œ ์‹๋ณ„์ž

queryKey๋Š” ์บ์‹œ๋ฅผ ๊ตฌ๋ถ„ํ•˜๋Š” ๋ฐฐ์—ด ํ˜•ํƒœ์˜ ๊ณ ์œ  ํ‚ค๋‹ค. ํ‚ค๊ฐ€ ๋ฐ”๋€Œ๋ฉด ์ž๋™์œผ๋กœ ์ƒˆ ๋ฐ์ดํ„ฐ๋ฅผ ์š”์ฒญํ•œ๋‹ค.
// ๋‹จ์ˆœ ํ‚ค - ๋ณ€ํ•˜์ง€ ์•Š๋Š” ๋ฐ์ดํ„ฐ queryKey: ['boards'] // ํŒŒ๋ผ๋ฏธํ„ฐ ํฌํ•จ - ํŽ˜์ด์ง€๋งˆ๋‹ค ๋‹ค๋ฅธ ์บ์‹œ queryKey: ['boards', page, size] // ['boards', 1, 10], ['boards', 2, 10] ๊ฐ๊ฐ ๋ณ„๋„ ์บ์‹œ // ID ํฌํ•จ - ๊ฒŒ์‹œ๊ธ€๋งˆ๋‹ค ๋‹ค๋ฅธ ์บ์‹œ queryKey: ['board', id] // ['board', 1], ['board', 2] ๊ฐ๊ฐ ๋ณ„๋„ ์บ์‹œ
JavaScript
๋ณต์‚ฌ
queryKey ์„ค๊ณ„ ์›์น™ ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ๋‹ฌ๋ผ์ง€๋ฉด ๋‹ค๋ฅธ ๋ฐ์ดํ„ฐ๊ฐ€ ๋‚˜์˜ค๋Š” ๊ฒฝ์šฐ ๋ฐ˜๋“œ์‹œ ํ‚ค์— ํฌํ•จํ•œ๋‹ค.

queryFn - ๋ฐ์ดํ„ฐ ์š”์ฒญ ํ•จ์ˆ˜

queryFn์€ Promise๋ฅผ ๋ฐ˜ํ™˜ํ•ด์•ผ ํ•œ๋‹ค.
// axios๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ - .then()์œผ๋กœ ์‹ค์ œ ๋ฐ์ดํ„ฐ๋งŒ ์ถ”์ถœ queryFn: () => boardsApi.list(page, size).then((res) => res.data) // fetch๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ queryFn: () => fetch('/api/boards').then(res => res.json()) // async/await ๋ฐฉ์‹ queryFn: async () => { const res = await boardsApi.list(page, size) return res.data }
JavaScript
๋ณต์‚ฌ

๋ฐ˜ํ™˜๊ฐ’

const { data, // queryFn์ด ๋ฐ˜ํ™˜ํ•œ ๋ฐ์ดํ„ฐ (์ดˆ๊ธฐ์—๋Š” undefined) isLoading, // ์ฒ˜์Œ ๋กœ๋”ฉ ์ค‘ (๋ฐ์ดํ„ฐ ์—†๊ณ  fetch ์ค‘) isFetching, // ๋ฐฑ๊ทธ๋ผ์šด๋“œ ํฌํ•จ ๋ชจ๋“  fetch ์ค‘ isError, // ์—๋Ÿฌ ๋ฐœ์ƒ ์—ฌ๋ถ€ error, // ์—๋Ÿฌ ๊ฐ์ฒด isSuccess, // ์„ฑ๊ณต ์—ฌ๋ถ€ refetch, // ์ˆ˜๋™์œผ๋กœ ๋‹ค์‹œ ๊ฐ€์ ธ์˜ค๊ธฐ status, // 'pending' | 'error' | 'success' } = useQuery({ queryKey, queryFn })
JavaScript
๋ณต์‚ฌ
isLoading vs isFetching
โ€ข
isLoading : ์บ์‹œ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†๊ณ  ์ฒ˜์Œ ์š”์ฒญ ์ค‘์ผ ๋•Œ๋งŒ true
โ€ข
isFetching : ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋ฆฌํŒจ์น˜ ํฌํ•จ ์–ด๋–ค fetch๋“  ์ง„ํ–‰ ์ค‘์ด๋ฉด true

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

useQuery({ queryKey: ['boards', page], queryFn: () => boardsApi.list(page), enabled: !!id, // false๋ฉด ์ฟผ๋ฆฌ ์‹คํ–‰ ์•ˆ ํ•จ (์กฐ๊ฑด๋ถ€ ์‹คํ–‰) staleTime: 1000 * 60, // 1๋ถ„๊ฐ„ ์‹ ์„  (์ „์—ญ ์„ค์ • ์˜ค๋ฒ„๋ผ์ด๋“œ) placeholderData: (prev) => prev, // ํ‚ค ๋ณ€๊ฒฝ ์‹œ ์ด์ „ ๋ฐ์ดํ„ฐ๋ฅผ ์ž„์‹œ๋กœ ํ‘œ์‹œ select: (data) => data.list, // ๋ฐ˜ํ™˜ ๋ฐ์ดํ„ฐ ๋ณ€ํ™˜/์„ ํƒ retry: false, // ์‹คํŒจ ์‹œ ์žฌ์‹œ๋„ ์•ˆ ํ•จ })
JavaScript
๋ณต์‚ฌ

enabled - ์กฐ๊ฑด๋ถ€ ์‹คํ–‰

// id๊ฐ€ ์žˆ์„ ๋•Œ๋งŒ ์š”์ฒญ (useBoard.js) const { data } = useQuery({ queryKey: ['board', id], queryFn: () => boardsApi.select(id).then(res => res.data), enabled: !!id, // id๊ฐ€ undefined/null์ด๋ฉด ์š”์ฒญ ์•ˆ ํ•จ })
JavaScript
๋ณต์‚ฌ

placeholderData - ํŽ˜์ด์ง€ ์ „ํ™˜ ์‹œ ๊นœ๋นก์ž„ ๋ฐฉ์ง€

// useBoards.js - ํŽ˜์ด์ง€ ์ด๋™ ์‹œ ์ด์ „ ๋ชฉ๋ก์„ ์œ ์ง€ํ•ด ํ™”๋ฉด ๊นœ๋นก์ž„ ๋ฐฉ์ง€ useQuery({ queryKey: ['boards', page, size], queryFn: () => boardsApi.list(page, size).then(res => res.data), placeholderData: (previousData) => previousData, // โ†‘ ์ƒˆ ๋ฐ์ดํ„ฐ ๋„์ฐฉ ์ „๊นŒ์ง€ ์ด์ „ ํŽ˜์ด์ง€ ๋ฐ์ดํ„ฐ๋ฅผ ์ž„์‹œ ํ‘œ์‹œ })
JavaScript
๋ณต์‚ฌ

6. useMutation - ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ

์„œ๋ฒ„ ๋ฐ์ดํ„ฐ๋ฅผ ์ƒ์„ฑ/์ˆ˜์ •/์‚ญ์ œํ•˜๋Š” ํ›…. POST, PUT, DELETE ์š”์ฒญ์— ์‚ฌ์šฉํ•œ๋‹ค.

๊ธฐ๋ณธ ๊ตฌ์กฐ

const mutation = useMutation({ mutationFn: (๋ณ€์ˆ˜) => ์„œ๋ฒ„์š”์ฒญํ•จ์ˆ˜(๋ณ€์ˆ˜), onSuccess: (data) => { /* ์„ฑ๊ณต ํ›„ ์ฒ˜๋ฆฌ */ }, onError: (error) => { /* ์‹คํŒจ ํ›„ ์ฒ˜๋ฆฌ */ }, }) // ํ˜ธ์ถœ mutation.mutate(์ „๋‹ฌํ• ๊ฐ’)
JavaScript
๋ณต์‚ฌ

mutationFn

// ๋‹จ์ˆœ ์ธ์ž mutationFn: (id) => boardsApi.remove(id) // ๊ฐ์ฒด ์ธ์ž (์—ฌ๋Ÿฌ ๊ฐ’ ์ „๋‹ฌ) mutationFn: ({ data, headers }) => boardsApi.insert(data, headers) // ๋ฐฐ์—ด ์ธ์ž mutationFn: (idList) => filesApi.removeFiles(idList)
JavaScript
๋ณต์‚ฌ

์ฝœ๋ฐฑ ์˜ต์…˜

useMutation({ mutationFn: (data) => boardsApi.insert(data), onSuccess: (responseData, variables, context) => { // ์š”์ฒญ ์„ฑ๊ณต ์‹œ ํ˜ธ์ถœ // responseData: ์„œ๋ฒ„ ์‘๋‹ต๊ฐ’ // variables: mutate()์— ์ „๋‹ฌํ•œ ๊ฐ’ }, onError: (error, variables, context) => { // ์š”์ฒญ ์‹คํŒจ ์‹œ ํ˜ธ์ถœ console.error(error.message) }, onSettled: (data, error, variables) => { // ์„ฑ๊ณต/์‹คํŒจ ๋ฌด๊ด€ํ•˜๊ฒŒ ํ•ญ์ƒ ํ˜ธ์ถœ (finally์™€ ์œ ์‚ฌ) }, onMutate: async (variables) => { // ์š”์ฒญ ์ง์ „์— ํ˜ธ์ถœ (๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ์— ํ™œ์šฉ) }, })
JavaScript
๋ณต์‚ฌ

๋ฐ˜ํ™˜๊ฐ’

const { mutate, // ๋ฎคํ…Œ์ด์…˜ ์‹คํ–‰ ํ•จ์ˆ˜ (๋น„๋™๊ธฐ ๊ฒฐ๊ณผ ๋ฌด์‹œ) mutateAsync, // ๋ฎคํ…Œ์ด์…˜ ์‹คํ–‰ ํ•จ์ˆ˜ (Promise ๋ฐ˜ํ™˜, await ๊ฐ€๋Šฅ) isPending, // ์š”์ฒญ ์ง„ํ–‰ ์ค‘ isSuccess, // ์š”์ฒญ ์„ฑ๊ณต isError, // ์š”์ฒญ ์‹คํŒจ error, // ์—๋Ÿฌ ๊ฐ์ฒด data, // ์„ฑ๊ณต ์‘๋‹ต ๋ฐ์ดํ„ฐ reset, // ์ƒํƒœ ์ดˆ๊ธฐํ™” } = useMutation({ mutationFn })
JavaScript
๋ณต์‚ฌ

mutate vs mutateAsync

// mutate - fire-and-forget, ์—๋Ÿฌ๋Š” onError๋กœ ์ฒ˜๋ฆฌ mutation.mutate(data) // mutateAsync - Promise ๋ฐ˜ํ™˜, try/catch ๊ฐ€๋Šฅ try { const result = await mutation.mutateAsync(data) console.log(result) } catch (error) { console.error(error) }
JavaScript
๋ณต์‚ฌ

isPending - ๋ฒ„ํŠผ ๋น„ํ™œ์„ฑํ™”

// ์ €์žฅ ์ค‘ ๋ฒ„ํŠผ ๋น„ํ™œ์„ฑํ™” (useBoardMutations.js) return { insertBoard: (data, headers) => insertMutation.mutate({ data, headers }), isInserting: insertMutation.isPending, // ์š”์ฒญ ์ค‘์ด๋ฉด true } // ์ปดํฌ๋„ŒํŠธ์—์„œ <button type="submit" disabled={isInserting}> {isInserting ? '์ €์žฅ ์ค‘...' : '์ €์žฅ'} </button>
JavaScript
๋ณต์‚ฌ

7. useQueryClient - ์บ์‹œ ์ง์ ‘ ์กฐ์ž‘

useQueryClient๋Š” QueryClient ์ธ์Šคํ„ด์Šค์— ์ง์ ‘ ์ ‘๊ทผํ•˜๋Š” ํ›…์ด๋‹ค. ์ฃผ๋กœ ๋ฎคํ…Œ์ด์…˜ ์„ฑ๊ณต ํ›„ ์บ์‹œ๋ฅผ ๋ฌดํšจํ™”ํ•˜๊ฑฐ๋‚˜ ์ง์ ‘ ์—…๋ฐ์ดํŠธํ•  ๋•Œ ์‚ฌ์šฉํ•œ๋‹ค.
import { useQueryClient } from '@tanstack/react-query' const queryClient = useQueryClient()
JavaScript
๋ณต์‚ฌ

invalidateQueries - ์บ์‹œ ๋ฌดํšจํ™” (๊ฐ€์žฅ ์ž์ฃผ ์‚ฌ์šฉ)

์บ์‹œ๋ฅผ "๋งŒ๋ฃŒ๋จ"์œผ๋กœ ํ‘œ์‹œํ•ด ๋‹ค์Œ ๋ Œ๋”๋ง ์‹œ ์„œ๋ฒ„์—์„œ ์ตœ์‹  ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๊ฒŒ ํ•œ๋‹ค.
// ํŠน์ • ํ‚ค ๋ฌดํšจํ™” queryClient.invalidateQueries({ queryKey: ['boards'] }) // ํŠน์ • ํ‚ค + ํŒŒ๋ผ๋ฏธํ„ฐ ๋ฌดํšจํ™” queryClient.invalidateQueries({ queryKey: ['board', id] }) // 'boards'๋กœ ์‹œ์ž‘ํ•˜๋Š” ๋ชจ๋“  ์บ์‹œ ๋ฌดํšจํ™” (๋ถ€๋ถ„ ๋งค์นญ) queryClient.invalidateQueries({ queryKey: ['boards'] }) // โ†’ ['boards'], ['boards', 1, 10], ['boards', 2, 10] ๋ชจ๋‘ ๋ฌดํšจํ™”
JavaScript
๋ณต์‚ฌ

getQueryData / setQueryData - ์บ์‹œ ์ฝ๊ธฐ/์“ฐ๊ธฐ

// ํ˜„์žฌ ์บ์‹œ ์ฝ๊ธฐ (์„œ๋ฒ„ ์š”์ฒญ ์—†์Œ) const boards = queryClient.getQueryData(['boards', 1, 10]) // ์บ์‹œ ์ง์ ‘ ์—…๋ฐ์ดํŠธ (์„œ๋ฒ„ ์š”์ฒญ ์—†์ด UI ์ฆ‰์‹œ ๋ฐ˜์˜ - ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ) queryClient.setQueryData(['board', id], (oldData) => ({ ...oldData, board: { ...oldData.board, title: newTitle } }))
JavaScript
๋ณต์‚ฌ

prefetchQuery - ๋ฏธ๋ฆฌ ์š”์ฒญ

// ๋‹ค์Œ ํŽ˜์ด์ง€๋ฅผ ๋ฏธ๋ฆฌ ๊ฐ€์ ธ์™€ ์บ์‹œ์— ์ €์žฅ await queryClient.prefetchQuery({ queryKey: ['boards', nextPage], queryFn: () => boardsApi.list(nextPage).then(res => res.data), })
JavaScript
๋ณต์‚ฌ

์ด ํ”„๋กœ์ ํŠธ์—์„œ์˜ ๋ฌดํšจํ™” ํŒจํ„ด

// useBoardMutations.js const queryClient = useQueryClient() // ๊ธ€ ๋“ฑ๋ก ์„ฑ๊ณต โ†’ ๋ชฉ๋ก ์บ์‹œ ๋ฌดํšจํ™” onSuccess: async () => { queryClient.invalidateQueries({ queryKey: ['boards'] }) // โ†’ ๋ชฉ๋ก ํŽ˜์ด์ง€ ์žฌ์ง„์ž… ์‹œ ์ตœ์‹  ๋ชฉ๋ก ๋กœ๋“œ } // ๊ธ€ ์ˆ˜์ • ์„ฑ๊ณต โ†’ ๋ชฉ๋ก + ๋‹จ๊ฑด ์บ์‹œ ๋ฌดํšจํ™” onSuccess: async () => { queryClient.invalidateQueries({ queryKey: ['boards'] }) // ๋ชฉ๋ก queryClient.invalidateQueries({ queryKey: ['board', id] }) // ์ƒ์„ธ } // ํŒŒ์ผ ์‚ญ์ œ ์„ฑ๊ณต โ†’ ํ•ด๋‹น ๊ฒŒ์‹œ๊ธ€ ์บ์‹œ ๋ฌดํšจํ™” onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['board', id] }) // โ†’ ํŒŒ์ผ ๋ชฉ๋ก์ด ์ฆ‰์‹œ ๊ฐฑ์‹ ๋จ }
JavaScript
๋ณต์‚ฌ

8. ReactQueryDevtools - ๋””๋ฒ„๊น… ๋„๊ตฌ

๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ ์บ์‹œ ์ƒํƒœ๋ฅผ ์‹œ๊ฐ์ ์œผ๋กœ ํ™•์ธํ•˜๋Š” ํ”Œ๋กœํŒ… ํŒจ๋„์ด๋‹ค. ํ”„๋กœ๋•์…˜ ๋นŒ๋“œ์—๋Š” ์ž๋™์œผ๋กœ ํฌํ•จ๋˜์ง€ ์•Š๋Š”๋‹ค.
// src/main.jsx import { ReactQueryDevtools } from '@tanstack/react-query-devtools' <QueryClientProvider client={queryClient}> <App /> <ReactQueryDevtools initialIsOpen={false} /> {/* โ†‘ ํ™”๋ฉด ์šฐํ•˜๋‹จ์— ํ”Œ๋กœํŒ… ๋ฒ„ํŠผ์œผ๋กœ ํ‘œ์‹œ */} </QueryClientProvider>
JavaScript
๋ณต์‚ฌ

ํ™•์ธ ๊ฐ€๋Šฅํ•œ ์ •๋ณด

ํ•ญ๋ชฉ
์„ค๋ช…
์ฟผ๋ฆฌ ๋ชฉ๋ก
ํ˜„์žฌ ์บ์‹œ์— ์ €์žฅ๋œ ๋ชจ๋“  ์ฟผ๋ฆฌ ํ‚ค
์ฟผ๋ฆฌ ์ƒํƒœ
fresh / stale / fetching / inactive
์บ์‹œ ๋ฐ์ดํ„ฐ
์ฟผ๋ฆฌ๋ณ„ ์‹ค์ œ ์ €์žฅ๋œ ๋ฐ์ดํ„ฐ
๋งˆ์ง€๋ง‰ ๊ฐฑ์‹  ์‹œ๊ฐ„
์–ธ์ œ ๋งˆ์ง€๋ง‰์œผ๋กœ ์„œ๋ฒ„์—์„œ ๊ฐ€์ ธ์™”๋Š”์ง€
์ˆ˜๋™ ๋ฆฌํŒจ์น˜
Devtools์—์„œ ์ง์ ‘ ๋ฆฌํŒจ์น˜ ์‹คํ–‰ ๊ฐ€๋Šฅ

์ฟผ๋ฆฌ ์ƒํƒœ ์ƒ‰์ƒ

์ƒ‰์ƒ
์ƒํƒœ
์˜๋ฏธ
์ดˆ๋ก
fresh
์‹ ์„ ํ•œ ๋ฐ์ดํ„ฐ (staleTime ์ด๋‚ด)
๋…ธ๋ž‘
stale
๋งŒ๋ฃŒ๋จ (๋‹ค์Œ ์ ‘๊ทผ ์‹œ ๋ฆฌํŒจ์น˜ ์˜ˆ์ •)
ํŒŒ๋ž‘
fetching
ํ˜„์žฌ ์„œ๋ฒ„์—์„œ ๊ฐ€์ ธ์˜ค๋Š” ์ค‘
ํšŒ์ƒ‰
inactive
์‚ฌ์šฉ ์ค‘์ธ ์ปดํฌ๋„ŒํŠธ ์—†์Œ (gcTime ํ›„ ์‚ญ์ œ)

9. ์บ์‹œ ๋ผ์ดํ”„์‚ฌ์ดํด

์ปดํฌ๋„ŒํŠธ ๋งˆ์šดํŠธ โ”‚ โ–ผ ์บ์‹œ์— ๋ฐ์ดํ„ฐ ์žˆ์Œ? โ”œโ”€ ์—†์Œ โ”€โ”€โ–บ ์„œ๋ฒ„ ์š”์ฒญ โ†’ ๋ฐ์ดํ„ฐ ์ €์žฅ โ†’ isLoading: true ๋™์•ˆ ๋กœ๋”ฉ ํ‘œ์‹œ โ”‚ โ””โ”€ ์žˆ์Œ โ”‚ โ”œโ”€ fresh? (staleTime ์ด๋‚ด) โ”‚ โ””โ”€ ์บ์‹œ ์ฆ‰์‹œ ๋ฐ˜ํ™˜ (์„œ๋ฒ„ ์š”์ฒญ ์•ˆ ํ•จ) โ”‚ โ””โ”€ stale? (staleTime ์ดˆ๊ณผ) โ””โ”€ ์บ์‹œ ์ฆ‰์‹œ ๋ฐ˜ํ™˜ + ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์„œ๋ฒ„ ์žฌ์š”์ฒญ (isFetching: true) โ”‚ โ””โ”€ ์ƒˆ ๋ฐ์ดํ„ฐ ๋„์ฐฉ โ†’ ์บ์‹œ ๊ต์ฒด โ†’ UI ๊ฐฑ์‹ 
Plain Text
๋ณต์‚ฌ

staleTime vs gcTime

๋ฐ์ดํ„ฐ ๋„์ฐฉ โ”‚ โ”œโ”€โ”€ staleTime ์ด๋‚ด โ†’ "fresh" (์žฌ์š”์ฒญ ์•ˆ ํ•จ) โ”‚ โ””โ”€โ”€ staleTime ์ดˆ๊ณผ โ†’ "stale" (๋‹ค์Œ ์ ‘๊ทผ ์‹œ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์žฌ์š”์ฒญ) ์ปดํฌ๋„ŒํŠธ ์–ธ๋งˆ์šดํŠธ โ”‚ โ””โ”€โ”€ gcTime ์ด๋‚ด โ†’ ์บ์‹œ ์œ ์ง€ (๋‹ค์‹œ ๋งˆ์šดํŠธ ์‹œ ์ฆ‰์‹œ ์‚ฌ์šฉ) gcTime ์ดˆ๊ณผ โ†’ ์บ์‹œ ์‚ญ์ œ (๋‹ค์‹œ ๋งˆ์šดํŠธ ์‹œ ์ƒˆ ์š”์ฒญ)
Plain Text
๋ณต์‚ฌ

10. ์ด ํ”„๋กœ์ ํŠธ์—์„œ์˜ ํ™œ์šฉ ํŒจํ„ด

์ „์ฒด ๊ตฌ์กฐ

main.jsx QueryClient (retry: 1, staleTime: 30s) QueryClientProvider App โ””โ”€ ๊ฐ ํŽ˜์ด์ง€ โ†’ ์ปดํฌ๋„ŒํŠธ โ†’ ์ปค์Šคํ…€ ํ›… โ”œโ”€ useBoards(page, size) โ†’ useQuery(['boards', page, size]) โ”œโ”€ useBoard(id) โ†’ useQuery(['board', id]) โ””โ”€ useBoardMutations(id) โ†’ useMutation + useQueryClient
Plain Text
๋ณต์‚ฌ

๋ฐ์ดํ„ฐ ํ๋ฆ„ - ๋ชฉ๋ก ์กฐํšŒ

ListPage ๋งˆ์šดํŠธ โ””โ”€ useBoards(1, 10) ํ˜ธ์ถœ โ””โ”€ useQuery({ queryKey: ['boards', 1, 10], ... }) โ”œโ”€ ์บ์‹œ ์—†์Œ โ†’ boardsApi.list(1, 10) ํ˜ธ์ถœ โ†’ GET /api/boards?page=1&size=10 โ”‚ isLoading: true โ†’ ๋กœ๋”ฉ ํ‘œ์‹œ โ”‚ ์‘๋‹ต ๋„์ฐฉ โ†’ ์บ์‹œ ์ €์žฅ โ†’ UI ํ‘œ์‹œ โ”‚ โ””โ”€ ์บ์‹œ ์žˆ์Œ (30์ดˆ ์ด๋‚ด) โ†’ ์ฆ‰์‹œ ๋ฐ˜ํ™˜, ์„œ๋ฒ„ ์š”์ฒญ ์—†์Œ
Plain Text
๋ณต์‚ฌ

๋ฐ์ดํ„ฐ ํ๋ฆ„ - ๊ธ€ ๋“ฑ๋ก ํ›„ ๋ชฉ๋ก ๊ฐฑ์‹ 

Insert ์ปดํฌ๋„ŒํŠธ โ””โ”€ onSubmit โ†’ insertBoard(formData) ํ˜ธ์ถœ โ””โ”€ insertMutation.mutate({ data: formData, headers }) โ””โ”€ boardsApi.insert() โ†’ POST /api/boards โ””โ”€ onSuccess: โ”œโ”€ queryClient.invalidateQueries(['boards']) โ”‚ โ†“ โ”‚ ['boards', 1, 10] ์บ์‹œ "๋งŒ๋ฃŒ"๋กœ ํ‘œ์‹œ โ”‚ โ”œโ”€ Swal ์„ฑ๊ณต ์•Œ๋ฆผ โ””โ”€ navigate('/boards') โ†“ ListPage ๋งˆ์šดํŠธ โ””โ”€ useBoards() ์žฌ์‹คํ–‰ โ””โ”€ ์บ์‹œ ๋งŒ๋ฃŒ โ†’ ์„œ๋ฒ„ ์žฌ์š”์ฒญ โ†’ ์ตœ์‹  ๋ชฉ๋ก ํ‘œ์‹œ
Plain Text
๋ณต์‚ฌ

์ปค์Šคํ…€ ํ›…์œผ๋กœ ๋ถ„๋ฆฌํ•˜๋Š” ์ด์œ 

โŒ ์ปดํฌ๋„ŒํŠธ ์•ˆ์— ์ง์ ‘ ์ž‘์„ฑ List.jsx โ†’ useQuery ๋กœ์ง ํฌํ•จ Update.jsx โ†’ useMutation ๋กœ์ง ํฌํ•จ โ†’ ๋กœ์ง์ด ์ปดํฌ๋„ŒํŠธ์— ์„ž์—ฌ ์žฌ์‚ฌ์šฉ ๋ถˆ๊ฐ€ โœ… ์ปค์Šคํ…€ ํ›…์œผ๋กœ ๋ถ„๋ฆฌ useBoards.js โ†’ ๋ชฉ๋ก ์กฐํšŒ ๋กœ์ง ์บก์Аํ™” useBoard.js โ†’ ๋‹จ๊ฑด ์กฐํšŒ ๋กœ์ง ์บก์Аํ™” useBoardMutations.js โ†’ CRUD ๋กœ์ง ์บก์Аํ™” โ†’ ์—ฌ๋Ÿฌ ์ปดํฌ๋„ŒํŠธ์—์„œ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅ โ†’ ํ…Œ์ŠคํŠธ ์šฉ์ด โ†’ ์ปดํฌ๋„ŒํŠธ๋Š” UI์—๋งŒ ์ง‘์ค‘
Plain Text
๋ณต์‚ฌ

์‹ค์ œ ์ฝ”๋“œ ์˜ˆ์‹œ ๋น„๊ต

// useBoards.js export const useBoards = (page = 1, size = 10) => { const { data, isLoading, isError } = useQuery({ queryKey: ['boards', page, size], // ํŽ˜์ด์ง€๋งˆ๋‹ค ๋ณ„๋„ ์บ์‹œ queryFn: () => boardsApi.list(page, size).then(res => res.data), placeholderData: (prev) => prev, // ํŽ˜์ด์ง€ ์ด๋™ ์‹œ ์ด์ „ ๋ฐ์ดํ„ฐ ์œ ์ง€ }) return { list: data?.list ?? [], pagination: data?.pagination ?? {}, isLoading, isError } } // useBoard.js export const useBoard = (id) => { const { data, isLoading, isError, refetch } = useQuery({ queryKey: ['board', id], // id๋งˆ๋‹ค ๋ณ„๋„ ์บ์‹œ queryFn: () => boardsApi.select(id).then(res => res.data), enabled: !!id, // id ์—†์œผ๋ฉด ์š”์ฒญ ์•ˆ ํ•จ }) return { board: data?.board ?? null, fileList: data?.fileList ?? [], isLoading, isError, refetch } } // useBoardMutations.js export const useBoardMutations = (id) => { const queryClient = useQueryClient() const insertMutation = useMutation({ mutationFn: ({ data, headers }) => boardsApi.insert(data, headers), onSuccess: async () => { queryClient.invalidateQueries({ queryKey: ['boards'] }) // ๋ชฉ๋ก ๊ฐฑ์‹  await successAlert('๋“ฑ๋ก ์„ฑ๊ณต', '๊ฒŒ์‹œ๊ธ€ ๋“ฑ๋ก์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.') navigate('/boards') }, }) return { insertBoard: (data, headers) => insertMutation.mutate({ data, headers }), isInserting: insertMutation.isPending, } }
JavaScript
๋ณต์‚ฌ

๋น ๋ฅธ ์ฐธ์กฐ

@tanstack/react-query โ”œโ”€ QueryClient โ†’ ์บ์‹œ ์ €์žฅ์†Œ, ์ „์—ญ ์„ค์ • โ”œโ”€ QueryClientProvider โ†’ Context๋กœ ์•ฑ ์ „์ฒด์— ๊ณต๊ธ‰ โ”œโ”€ useQuery โ†’ ๋ฐ์ดํ„ฐ ์ฝ๊ธฐ (GET) โ”‚ โ”œโ”€ queryKey โ†’ ์บ์‹œ ์‹๋ณ„์ž (๋ฐฐ์—ด) โ”‚ โ”œโ”€ queryFn โ†’ ์„œ๋ฒ„ ์š”์ฒญ ํ•จ์ˆ˜ โ”‚ โ”œโ”€ enabled โ†’ ์กฐ๊ฑด๋ถ€ ์‹คํ–‰ โ”‚ โ””โ”€ placeholderData โ†’ ์ด์ „ ๋ฐ์ดํ„ฐ ์ž„์‹œ ํ‘œ์‹œ โ”œโ”€ useMutation โ†’ ๋ฐ์ดํ„ฐ ์“ฐ๊ธฐ (POST/PUT/DELETE) โ”‚ โ”œโ”€ mutationFn โ†’ ์„œ๋ฒ„ ์š”์ฒญ ํ•จ์ˆ˜ โ”‚ โ”œโ”€ onSuccess โ†’ ์„ฑ๊ณต ์ฝœ๋ฐฑ โ”‚ โ”œโ”€ onError โ†’ ์‹คํŒจ ์ฝœ๋ฐฑ โ”‚ โ””โ”€ isPending โ†’ ์š”์ฒญ ์ง„ํ–‰ ์ค‘ ์—ฌ๋ถ€ โ”œโ”€ useQueryClient โ†’ QueryClient ์ธ์Šคํ„ด์Šค ์ ‘๊ทผ โ”‚ โ”œโ”€ invalidateQueries โ†’ ์บ์‹œ ๋ฌดํšจํ™” (๊ฐ€์žฅ ์ž์ฃผ ์‚ฌ์šฉ) โ”‚ โ”œโ”€ setQueryData โ†’ ์บ์‹œ ์ง์ ‘ ์ˆ˜์ • โ”‚ โ””โ”€ getQueryData โ†’ ์บ์‹œ ์ฝ๊ธฐ โ””โ”€ ReactQueryDevtools โ†’ ๊ฐœ๋ฐœ์šฉ ์บ์‹œ ์ƒํƒœ ์‹œ๊ฐํ™”
Plain Text
๋ณต์‚ฌ