@kood/claude-code 0.3.9 → 0.3.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/templates/tanstack-start/docs/library/better-auth/index.md +225 -185
- package/templates/tanstack-start/docs/library/prisma/index.md +1025 -41
- package/templates/tanstack-start/docs/library/t3-env/index.md +207 -40
- package/templates/tanstack-start/docs/library/tanstack-query/index.md +878 -42
- package/templates/tanstack-start/docs/library/tanstack-router/index.md +602 -54
- package/templates/tanstack-start/docs/library/tanstack-start/index.md +1334 -33
- package/templates/tanstack-start/docs/library/zod/index.md +674 -31
|
@@ -1,66 +1,902 @@
|
|
|
1
1
|
# TanStack Query
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> v5 | React Data Fetching & State Management
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<context>
|
|
8
|
+
|
|
9
|
+
**Purpose:** TanStack Query v5를 사용한 서버 상태 관리
|
|
10
|
+
|
|
11
|
+
**Scope:** useQuery, useMutation, 캐시 무효화, Optimistic Updates
|
|
12
|
+
|
|
13
|
+
**Key Features:**
|
|
14
|
+
- Automatic caching & background updates
|
|
15
|
+
- Request deduplication
|
|
16
|
+
- Optimistic updates
|
|
17
|
+
- Pagination & infinite queries
|
|
18
|
+
- Server state synchronization
|
|
19
|
+
- TanStack Start Server Functions 통합
|
|
20
|
+
|
|
21
|
+
**Version Highlights (v5):**
|
|
22
|
+
- `staleTime`: 데이터가 fresh 상태로 유지되는 시간
|
|
23
|
+
- `gcTime`: 캐시에서 가비지 컬렉션되기 전 대기 시간 (v4의 `cacheTime`)
|
|
24
|
+
- Improved TypeScript inference
|
|
25
|
+
- Better DevTools
|
|
26
|
+
|
|
27
|
+
</context>
|
|
9
28
|
|
|
10
29
|
---
|
|
11
30
|
|
|
12
|
-
<
|
|
31
|
+
<forbidden>
|
|
13
32
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
33
|
+
| 분류 | 금지 |
|
|
34
|
+
|------|------|
|
|
35
|
+
| **Server Function 호출** | ❌ 클라이언트에서 Server Function 직접 호출 (반드시 useQuery/useMutation 사용) |
|
|
36
|
+
| **Query Key** | ❌ 동적 키 없이 파라미터 쿼리, ❌ 객체/배열 직접 비교 |
|
|
37
|
+
| **무효화** | ❌ 과도한 전역 무효화 (`invalidateQueries()` 남발) |
|
|
38
|
+
| **Optimistic Update** | ❌ `onMutate`에서 `cancelQueries` 누락, ❌ `onError`에서 롤백 누락 |
|
|
39
|
+
| **Mutation** | ❌ `mutateAsync`에서 에러 처리 누락 |
|
|
20
40
|
|
|
21
|
-
|
|
22
|
-
const queryClient = useQueryClient()
|
|
23
|
-
const mutation = useMutation({
|
|
24
|
-
mutationFn: createUser,
|
|
25
|
-
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['users'] }),
|
|
26
|
-
})
|
|
27
|
-
mutation.mutate({ email, name })
|
|
41
|
+
</forbidden>
|
|
28
42
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
<required>
|
|
46
|
+
|
|
47
|
+
| 작업 | 필수 |
|
|
48
|
+
|------|------|
|
|
49
|
+
| **Server Function 호출** | ✅ `useQuery`로 GET, `useMutation`로 POST/PUT/PATCH/DELETE |
|
|
50
|
+
| **Query Key** | ✅ 계층적 구조 (`['todos', 'list', { filters }]`), ✅ 파라미터 포함 |
|
|
51
|
+
| **무효화** | ✅ 관련 쿼리만 무효화 (`{ queryKey: ['todos'] }`), ✅ `onSuccess`/`onSettled`에서 실행 |
|
|
52
|
+
| **Optimistic Update** | ✅ `cancelQueries` → `getQueryData` → `setQueryData` → `onError` 롤백 순서 |
|
|
53
|
+
| **설정** | ✅ `staleTime`, `gcTime` 적절히 설정 (기본값은 즉시 stale) |
|
|
54
|
+
|
|
55
|
+
</required>
|
|
42
56
|
|
|
43
|
-
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
<setup>
|
|
60
|
+
|
|
61
|
+
## QueryClient 설정
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
65
|
+
|
|
66
|
+
// QueryClient 생성
|
|
44
67
|
const queryClient = new QueryClient({
|
|
45
68
|
defaultOptions: {
|
|
46
69
|
queries: {
|
|
47
|
-
staleTime: 1000 * 60 * 5, // 5
|
|
48
|
-
gcTime: 1000 * 60 * 30, // 30
|
|
49
|
-
retry: 3,
|
|
70
|
+
staleTime: 1000 * 60 * 5, // 5분 동안 fresh 유지
|
|
71
|
+
gcTime: 1000 * 60 * 30, // 30분 후 가비지 컬렉션
|
|
72
|
+
retry: 3, // 실패 시 3회 재시도
|
|
73
|
+
refetchOnWindowFocus: true, // 포커스 시 리페치
|
|
74
|
+
},
|
|
75
|
+
mutations: {
|
|
76
|
+
retry: 1, // Mutation은 1회만 재시도
|
|
50
77
|
},
|
|
51
78
|
},
|
|
52
79
|
})
|
|
53
80
|
|
|
54
|
-
|
|
81
|
+
// App Provider
|
|
82
|
+
export const App = () => (
|
|
55
83
|
<QueryClientProvider client={queryClient}>
|
|
56
84
|
<YourApp />
|
|
57
85
|
</QueryClientProvider>
|
|
58
86
|
)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### 설정 옵션
|
|
90
|
+
|
|
91
|
+
| 옵션 | 설명 | 기본값 |
|
|
92
|
+
|------|------|--------|
|
|
93
|
+
| `staleTime` | 데이터가 fresh 상태 유지 시간 (ms) | 0 (즉시 stale) |
|
|
94
|
+
| `gcTime` | 비활성 캐시 가비지 컬렉션 대기 시간 (ms) | 5분 |
|
|
95
|
+
| `retry` | 실패 시 재시도 횟수 | 3 |
|
|
96
|
+
| `refetchOnWindowFocus` | 윈도우 포커스 시 리페치 | true |
|
|
97
|
+
| `refetchInterval` | 자동 리페치 간격 (ms) | false |
|
|
98
|
+
|
|
99
|
+
</setup>
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
<use_query>
|
|
104
|
+
|
|
105
|
+
## useQuery - 데이터 조회
|
|
106
|
+
|
|
107
|
+
### 기본 패턴
|
|
59
108
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
109
|
+
```tsx
|
|
110
|
+
import { useQuery } from '@tanstack/react-query'
|
|
111
|
+
import { getUsers } from '@/functions/user'
|
|
112
|
+
|
|
113
|
+
const UsersPage = () => {
|
|
114
|
+
const { data, isLoading, error } = useQuery({
|
|
115
|
+
queryKey: ['users'],
|
|
116
|
+
queryFn: getUsers,
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
if (isLoading) return <div>Loading...</div>
|
|
120
|
+
if (error) return <div>Error: {error.message}</div>
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<ul>
|
|
124
|
+
{data.map((user) => (
|
|
125
|
+
<li key={user.id}>{user.name}</li>
|
|
126
|
+
))}
|
|
127
|
+
</ul>
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### 옵션 활용
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
useQuery({
|
|
136
|
+
queryKey: ['todos'],
|
|
137
|
+
queryFn: fetchTodos,
|
|
138
|
+
staleTime: 1000 * 60 * 5, // 5분 동안 fresh 유지
|
|
139
|
+
gcTime: 1000 * 60 * 30, // 30분 후 가비지 컬렉션
|
|
140
|
+
refetchOnWindowFocus: true, // 포커스 시 리페치
|
|
141
|
+
refetchInterval: 1000 * 60, // 1분마다 자동 리페치
|
|
142
|
+
retry: 3, // 재시도 횟수
|
|
143
|
+
enabled: !!userId, // 조건부 실행
|
|
144
|
+
initialData: [], // 초기 데이터
|
|
145
|
+
select: (data) => data.filter(t => t.done), // 데이터 변환
|
|
146
|
+
})
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### 파라미터 쿼리
|
|
150
|
+
|
|
151
|
+
```tsx
|
|
152
|
+
// Query Key에 파라미터 포함
|
|
153
|
+
const TodoDetailPage = () => {
|
|
154
|
+
const { todoId } = useParams()
|
|
155
|
+
|
|
156
|
+
const { data: todo } = useQuery({
|
|
157
|
+
queryKey: ['todo', todoId], // 파라미터 포함
|
|
158
|
+
queryFn: () => getTodoById(todoId),
|
|
159
|
+
enabled: !!todoId, // todoId 있을 때만 실행
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
return <div>{todo?.title}</div>
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### 의존적 쿼리
|
|
167
|
+
|
|
168
|
+
```tsx
|
|
169
|
+
// 첫 번째 쿼리 결과를 두 번째 쿼리에 사용
|
|
170
|
+
const UserPostsPage = () => {
|
|
171
|
+
const { data: user } = useQuery({
|
|
172
|
+
queryKey: ['user', userId],
|
|
173
|
+
queryFn: () => getUser(userId),
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
const { data: posts } = useQuery({
|
|
177
|
+
queryKey: ['posts', user?.id],
|
|
178
|
+
queryFn: () => getPostsByUserId(user!.id),
|
|
179
|
+
enabled: !!user?.id, // user가 로드된 후에만 실행
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
return <PostList posts={posts} />
|
|
183
|
+
}
|
|
64
184
|
```
|
|
65
185
|
|
|
66
|
-
|
|
186
|
+
### 병렬 쿼리
|
|
187
|
+
|
|
188
|
+
```tsx
|
|
189
|
+
// 여러 쿼리 동시 실행
|
|
190
|
+
const DashboardPage = () => {
|
|
191
|
+
const usersQuery = useQuery({ queryKey: ['users'], queryFn: getUsers })
|
|
192
|
+
const postsQuery = useQuery({ queryKey: ['posts'], queryFn: getPosts })
|
|
193
|
+
const statsQuery = useQuery({ queryKey: ['stats'], queryFn: getStats })
|
|
194
|
+
|
|
195
|
+
if (usersQuery.isLoading || postsQuery.isLoading || statsQuery.isLoading) {
|
|
196
|
+
return <div>Loading...</div>
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return (
|
|
200
|
+
<div>
|
|
201
|
+
<UserStats users={usersQuery.data} />
|
|
202
|
+
<PostList posts={postsQuery.data} />
|
|
203
|
+
<Analytics stats={statsQuery.data} />
|
|
204
|
+
</div>
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### 동적 병렬 쿼리
|
|
210
|
+
|
|
211
|
+
```tsx
|
|
212
|
+
import { useQueries } from '@tanstack/react-query'
|
|
213
|
+
|
|
214
|
+
// 여러 ID에 대해 동시 조회
|
|
215
|
+
const UserListPage = () => {
|
|
216
|
+
const userIds = [1, 2, 3, 4, 5]
|
|
217
|
+
|
|
218
|
+
const userQueries = useQueries({
|
|
219
|
+
queries: userIds.map((id) => ({
|
|
220
|
+
queryKey: ['user', id],
|
|
221
|
+
queryFn: () => getUserById(id),
|
|
222
|
+
})),
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
const allLoaded = userQueries.every((query) => query.isSuccess)
|
|
226
|
+
if (!allLoaded) return <div>Loading...</div>
|
|
227
|
+
|
|
228
|
+
return (
|
|
229
|
+
<ul>
|
|
230
|
+
{userQueries.map((query, index) => (
|
|
231
|
+
<li key={userIds[index]}>{query.data?.name}</li>
|
|
232
|
+
))}
|
|
233
|
+
</ul>
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### useQuery 반환값
|
|
239
|
+
|
|
240
|
+
| 속성 | 타입 | 설명 |
|
|
241
|
+
|------|------|------|
|
|
242
|
+
| `data` | `TData \| undefined` | 쿼리 결과 데이터 |
|
|
243
|
+
| `error` | `TError \| null` | 에러 객체 |
|
|
244
|
+
| `isLoading` | `boolean` | 첫 로딩 중 (데이터 없음 + 페칭 중) |
|
|
245
|
+
| `isFetching` | `boolean` | 백그라운드 페칭 중 |
|
|
246
|
+
| `isError` | `boolean` | 에러 상태 |
|
|
247
|
+
| `isSuccess` | `boolean` | 성공 상태 |
|
|
248
|
+
| `status` | `'pending' \| 'error' \| 'success'` | 쿼리 상태 |
|
|
249
|
+
| `refetch` | `() => Promise<...>` | 수동 리페치 |
|
|
250
|
+
|
|
251
|
+
</use_query>
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
<use_mutation>
|
|
256
|
+
|
|
257
|
+
## useMutation - 데이터 변경
|
|
258
|
+
|
|
259
|
+
### 기본 패턴
|
|
260
|
+
|
|
261
|
+
```tsx
|
|
262
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
263
|
+
import { createUser } from '@/functions/user'
|
|
264
|
+
|
|
265
|
+
const CreateUserForm = () => {
|
|
266
|
+
const queryClient = useQueryClient()
|
|
267
|
+
|
|
268
|
+
const mutation = useMutation({
|
|
269
|
+
mutationFn: createUser,
|
|
270
|
+
onSuccess: () => {
|
|
271
|
+
// 성공 시 users 쿼리 무효화 (리페치 유도)
|
|
272
|
+
queryClient.invalidateQueries({ queryKey: ['users'] })
|
|
273
|
+
},
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
const handleSubmit = (formData) => {
|
|
277
|
+
mutation.mutate(formData)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return (
|
|
281
|
+
<form onSubmit={handleSubmit}>
|
|
282
|
+
<input name="email" />
|
|
283
|
+
<button disabled={mutation.isPending}>
|
|
284
|
+
{mutation.isPending ? 'Creating...' : 'Create User'}
|
|
285
|
+
</button>
|
|
286
|
+
{mutation.isError && <div>Error: {mutation.error.message}</div>}
|
|
287
|
+
</form>
|
|
288
|
+
)
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### 콜백 활용
|
|
293
|
+
|
|
294
|
+
```tsx
|
|
295
|
+
useMutation({
|
|
296
|
+
mutationFn: updateTodo,
|
|
297
|
+
onMutate: async (newTodo) => {
|
|
298
|
+
// Mutation 시작 전 (Optimistic Update용)
|
|
299
|
+
await queryClient.cancelQueries({ queryKey: ['todos'] })
|
|
300
|
+
const previousTodos = queryClient.getQueryData(['todos'])
|
|
301
|
+
queryClient.setQueryData(['todos'], (old) => [...old, newTodo])
|
|
302
|
+
return { previousTodos } // context로 전달
|
|
303
|
+
},
|
|
304
|
+
onSuccess: (data, variables, context) => {
|
|
305
|
+
// 성공 시 실행
|
|
306
|
+
console.log('Created:', data)
|
|
307
|
+
},
|
|
308
|
+
onError: (error, variables, context) => {
|
|
309
|
+
// 실패 시 롤백
|
|
310
|
+
queryClient.setQueryData(['todos'], context.previousTodos)
|
|
311
|
+
},
|
|
312
|
+
onSettled: (data, error, variables, context) => {
|
|
313
|
+
// 성공/실패 무관하게 실행
|
|
314
|
+
queryClient.invalidateQueries({ queryKey: ['todos'] })
|
|
315
|
+
},
|
|
316
|
+
})
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### mutate vs mutateAsync
|
|
320
|
+
|
|
321
|
+
```tsx
|
|
322
|
+
// mutate: 콜백 방식
|
|
323
|
+
mutation.mutate(data, {
|
|
324
|
+
onSuccess: (result) => {
|
|
325
|
+
console.log('Success:', result)
|
|
326
|
+
},
|
|
327
|
+
onError: (error) => {
|
|
328
|
+
console.error('Error:', error)
|
|
329
|
+
},
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
// mutateAsync: Promise 방식
|
|
333
|
+
try {
|
|
334
|
+
const result = await mutation.mutateAsync(data)
|
|
335
|
+
console.log('Success:', result)
|
|
336
|
+
} catch (error) {
|
|
337
|
+
console.error('Error:', error)
|
|
338
|
+
}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### 캐시 직접 업데이트
|
|
342
|
+
|
|
343
|
+
```tsx
|
|
344
|
+
useMutation({
|
|
345
|
+
mutationFn: updateTodo,
|
|
346
|
+
onSuccess: (data) => {
|
|
347
|
+
// 전체 목록 무효화
|
|
348
|
+
queryClient.invalidateQueries({ queryKey: ['todos'] })
|
|
349
|
+
|
|
350
|
+
// 개별 항목 캐시 직접 업데이트 (리페치 불필요)
|
|
351
|
+
queryClient.setQueryData(['todo', data.id], data)
|
|
352
|
+
},
|
|
353
|
+
})
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
### useMutation 반환값
|
|
357
|
+
|
|
358
|
+
| 속성 | 타입 | 설명 |
|
|
359
|
+
|------|------|------|
|
|
360
|
+
| `data` | `TData \| undefined` | Mutation 결과 데이터 |
|
|
361
|
+
| `error` | `TError \| null` | 에러 객체 |
|
|
362
|
+
| `isPending` | `boolean` | 실행 중 |
|
|
363
|
+
| `isSuccess` | `boolean` | 성공 상태 |
|
|
364
|
+
| `isError` | `boolean` | 에러 상태 |
|
|
365
|
+
| `mutate` | `(variables, options?) => void` | Mutation 실행 (콜백 방식) |
|
|
366
|
+
| `mutateAsync` | `(variables) => Promise<TData>` | Mutation 실행 (Promise 방식) |
|
|
367
|
+
| `reset` | `() => void` | 상태 초기화 |
|
|
368
|
+
| `variables` | `TVariables \| undefined` | 전달된 변수 |
|
|
369
|
+
|
|
370
|
+
</use_mutation>
|
|
371
|
+
|
|
372
|
+
---
|
|
373
|
+
|
|
374
|
+
<invalidation>
|
|
375
|
+
|
|
376
|
+
## 캐시 무효화
|
|
377
|
+
|
|
378
|
+
### 기본 무효화
|
|
379
|
+
|
|
380
|
+
```typescript
|
|
381
|
+
import { useQueryClient } from '@tanstack/react-query'
|
|
382
|
+
|
|
383
|
+
const Component = () => {
|
|
384
|
+
const queryClient = useQueryClient()
|
|
385
|
+
|
|
386
|
+
// 단일 쿼리 무효화
|
|
387
|
+
queryClient.invalidateQueries({ queryKey: ['todos'] })
|
|
388
|
+
|
|
389
|
+
// 다중 쿼리 무효화
|
|
390
|
+
await Promise.all([
|
|
391
|
+
queryClient.invalidateQueries({ queryKey: ['todos'] }),
|
|
392
|
+
queryClient.invalidateQueries({ queryKey: ['reminders'] }),
|
|
393
|
+
])
|
|
394
|
+
|
|
395
|
+
// 전체 무효화 (주의: 과도한 리페치 발생 가능)
|
|
396
|
+
queryClient.invalidateQueries()
|
|
397
|
+
}
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### 무효화 옵션
|
|
401
|
+
|
|
402
|
+
```typescript
|
|
403
|
+
queryClient.invalidateQueries({
|
|
404
|
+
queryKey: ['posts'],
|
|
405
|
+
exact: true, // 정확한 키 매칭만
|
|
406
|
+
refetchType: 'active', // 'active' | 'inactive' | 'all' | 'none'
|
|
407
|
+
})
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
| refetchType | 설명 |
|
|
411
|
+
|-------------|------|
|
|
412
|
+
| `'active'` | 현재 렌더링 중인 쿼리만 재조회 (기본값) |
|
|
413
|
+
| `'inactive'` | 비활성 쿼리만 재조회 |
|
|
414
|
+
| `'all'` | 모든 매칭 쿼리 재조회 |
|
|
415
|
+
| `'none'` | 무효화만, 재조회 안함 |
|
|
416
|
+
|
|
417
|
+
### Query Key 매칭
|
|
418
|
+
|
|
419
|
+
```typescript
|
|
420
|
+
// Prefix 매칭 (기본)
|
|
421
|
+
queryClient.invalidateQueries({ queryKey: ['todos'] })
|
|
422
|
+
// 매칭: ['todos'], ['todos', 'list'], ['todos', 1]
|
|
423
|
+
|
|
424
|
+
// 정확한 매칭
|
|
425
|
+
queryClient.invalidateQueries({ queryKey: ['todos', 'list'], exact: true })
|
|
426
|
+
// 매칭: ['todos', 'list']만
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### Mutation과 함께 사용
|
|
430
|
+
|
|
431
|
+
```tsx
|
|
432
|
+
useMutation({
|
|
433
|
+
mutationFn: createTodo,
|
|
434
|
+
onSuccess: () => {
|
|
435
|
+
// 성공 시 무효화
|
|
436
|
+
queryClient.invalidateQueries({ queryKey: ['todos'] })
|
|
437
|
+
},
|
|
438
|
+
onSettled: () => {
|
|
439
|
+
// 성공/실패 무관하게 무효화
|
|
440
|
+
queryClient.invalidateQueries({ queryKey: ['todos'] })
|
|
441
|
+
},
|
|
442
|
+
})
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
### 직접 업데이트 vs 무효화
|
|
446
|
+
|
|
447
|
+
| 방법 | 장점 | 단점 | 사용 시점 |
|
|
448
|
+
|------|------|------|----------|
|
|
449
|
+
| **setQueryData** | 빠름, 서버 요청 없음 | 서버 동기화 안됨 | 클라이언트 로직으로 결과 예측 가능 |
|
|
450
|
+
| **invalidateQueries** | 서버 데이터 보장 | 네트워크 요청 발생 | 서버 데이터 정확성 중요 |
|
|
451
|
+
|
|
452
|
+
```typescript
|
|
453
|
+
// 직접 업데이트 (더 빠름)
|
|
454
|
+
queryClient.setQueryData(['todos'], (old) => [...old, newTodo])
|
|
455
|
+
|
|
456
|
+
// 무효화 (서버 데이터 보장)
|
|
457
|
+
queryClient.invalidateQueries({ queryKey: ['todos'] })
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
</invalidation>
|
|
461
|
+
|
|
462
|
+
---
|
|
463
|
+
|
|
464
|
+
<optimistic_updates>
|
|
465
|
+
|
|
466
|
+
## Optimistic Updates
|
|
467
|
+
|
|
468
|
+
### 추가 패턴
|
|
469
|
+
|
|
470
|
+
```tsx
|
|
471
|
+
useMutation({
|
|
472
|
+
mutationFn: addTodo,
|
|
473
|
+
onMutate: async (newTodo) => {
|
|
474
|
+
// 1. 진행 중인 쿼리 취소 (충돌 방지)
|
|
475
|
+
await queryClient.cancelQueries({ queryKey: ['todos'] })
|
|
476
|
+
|
|
477
|
+
// 2. 이전 데이터 백업
|
|
478
|
+
const previousTodos = queryClient.getQueryData(['todos'])
|
|
479
|
+
|
|
480
|
+
// 3. 낙관적 업데이트
|
|
481
|
+
queryClient.setQueryData(['todos'], (old) => [...old, newTodo])
|
|
482
|
+
|
|
483
|
+
// 4. context로 반환 (롤백용)
|
|
484
|
+
return { previousTodos }
|
|
485
|
+
},
|
|
486
|
+
onError: (err, newTodo, context) => {
|
|
487
|
+
// 실패 시 롤백
|
|
488
|
+
queryClient.setQueryData(['todos'], context.previousTodos)
|
|
489
|
+
},
|
|
490
|
+
onSettled: () => {
|
|
491
|
+
// 성공/실패 무관하게 서버 데이터로 동기화
|
|
492
|
+
queryClient.invalidateQueries({ queryKey: ['todos'] })
|
|
493
|
+
},
|
|
494
|
+
})
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
### 삭제 패턴
|
|
498
|
+
|
|
499
|
+
```tsx
|
|
500
|
+
useMutation({
|
|
501
|
+
mutationFn: deleteTodo,
|
|
502
|
+
onMutate: async (todoId) => {
|
|
503
|
+
await queryClient.cancelQueries({ queryKey: ['todos'] })
|
|
504
|
+
const previousTodos = queryClient.getQueryData(['todos'])
|
|
505
|
+
|
|
506
|
+
// 즉시 UI에서 제거
|
|
507
|
+
queryClient.setQueryData(['todos'], (old) =>
|
|
508
|
+
old.filter((todo) => todo.id !== todoId)
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
return { previousTodos }
|
|
512
|
+
},
|
|
513
|
+
onError: (err, todoId, context) => {
|
|
514
|
+
queryClient.setQueryData(['todos'], context.previousTodos)
|
|
515
|
+
},
|
|
516
|
+
onSettled: () => {
|
|
517
|
+
queryClient.invalidateQueries({ queryKey: ['todos'] })
|
|
518
|
+
},
|
|
519
|
+
})
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
### 토글 패턴
|
|
523
|
+
|
|
524
|
+
```tsx
|
|
525
|
+
useMutation({
|
|
526
|
+
mutationFn: toggleTodo,
|
|
527
|
+
onMutate: async (todoId) => {
|
|
528
|
+
await queryClient.cancelQueries({ queryKey: ['todos'] })
|
|
529
|
+
const previousTodos = queryClient.getQueryData(['todos'])
|
|
530
|
+
|
|
531
|
+
// 즉시 토글
|
|
532
|
+
queryClient.setQueryData(['todos'], (old) =>
|
|
533
|
+
old.map((todo) =>
|
|
534
|
+
todo.id === todoId ? { ...todo, completed: !todo.completed } : todo
|
|
535
|
+
)
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
return { previousTodos }
|
|
539
|
+
},
|
|
540
|
+
onError: (err, todoId, context) => {
|
|
541
|
+
queryClient.setQueryData(['todos'], context.previousTodos)
|
|
542
|
+
},
|
|
543
|
+
onSettled: () => {
|
|
544
|
+
queryClient.invalidateQueries({ queryKey: ['todos'] })
|
|
545
|
+
},
|
|
546
|
+
})
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
### 단일 항목 업데이트
|
|
550
|
+
|
|
551
|
+
```tsx
|
|
552
|
+
useMutation({
|
|
553
|
+
mutationFn: updateTodo,
|
|
554
|
+
onMutate: async (newTodo) => {
|
|
555
|
+
// 목록과 개별 항목 모두 취소
|
|
556
|
+
await queryClient.cancelQueries({ queryKey: ['todos'] })
|
|
557
|
+
await queryClient.cancelQueries({ queryKey: ['todo', newTodo.id] })
|
|
558
|
+
|
|
559
|
+
const previousTodo = queryClient.getQueryData(['todo', newTodo.id])
|
|
560
|
+
const previousTodos = queryClient.getQueryData(['todos'])
|
|
561
|
+
|
|
562
|
+
// 개별 항목 업데이트
|
|
563
|
+
queryClient.setQueryData(['todo', newTodo.id], newTodo)
|
|
564
|
+
|
|
565
|
+
// 목록에서도 업데이트
|
|
566
|
+
queryClient.setQueryData(['todos'], (old) =>
|
|
567
|
+
old.map((todo) => (todo.id === newTodo.id ? newTodo : todo))
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
return { previousTodo, previousTodos, newTodo }
|
|
571
|
+
},
|
|
572
|
+
onError: (err, newTodo, context) => {
|
|
573
|
+
queryClient.setQueryData(['todo', context.newTodo.id], context.previousTodo)
|
|
574
|
+
queryClient.setQueryData(['todos'], context.previousTodos)
|
|
575
|
+
},
|
|
576
|
+
onSettled: (newTodo) => {
|
|
577
|
+
queryClient.invalidateQueries({ queryKey: ['todo', newTodo.id] })
|
|
578
|
+
queryClient.invalidateQueries({ queryKey: ['todos'] })
|
|
579
|
+
},
|
|
580
|
+
})
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
### Optimistic Updates 체크리스트
|
|
584
|
+
|
|
585
|
+
| 단계 | 필수 작업 |
|
|
586
|
+
|------|----------|
|
|
587
|
+
| 1. **cancelQueries** | ✅ 진행 중인 쿼리 취소 (충돌 방지) |
|
|
588
|
+
| 2. **getQueryData** | ✅ 이전 데이터 백업 (롤백용) |
|
|
589
|
+
| 3. **setQueryData** | ✅ 낙관적 업데이트 |
|
|
590
|
+
| 4. **return context** | ✅ context로 이전 데이터 반환 |
|
|
591
|
+
| 5. **onError 롤백** | ✅ 실패 시 이전 데이터로 복원 |
|
|
592
|
+
| 6. **onSettled 동기화** | ✅ 서버 데이터로 최종 동기화 |
|
|
593
|
+
|
|
594
|
+
</optimistic_updates>
|
|
595
|
+
|
|
596
|
+
---
|
|
597
|
+
|
|
598
|
+
<query_keys>
|
|
599
|
+
|
|
600
|
+
## Query Keys
|
|
601
|
+
|
|
602
|
+
### 계층적 구조
|
|
603
|
+
|
|
604
|
+
```typescript
|
|
605
|
+
// 단순
|
|
606
|
+
['todos']
|
|
607
|
+
|
|
608
|
+
// 파라미터 포함
|
|
609
|
+
['todo', { id: 5 }]
|
|
610
|
+
['todo', todoId]
|
|
611
|
+
|
|
612
|
+
// 계층적
|
|
613
|
+
['todos', 'list']
|
|
614
|
+
['todos', 'list', { filters: 'completed' }]
|
|
615
|
+
['todos', 'detail', { id: 5 }]
|
|
616
|
+
|
|
617
|
+
// 복잡한 필터
|
|
618
|
+
['projects', 'list', { type: 'owned', filters: { status: 'active' } }]
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
### Query Key 패턴
|
|
622
|
+
|
|
623
|
+
| 패턴 | 예시 | 사용 시점 |
|
|
624
|
+
|------|------|----------|
|
|
625
|
+
| **단일 리소스 목록** | `['users']` | 전체 목록 |
|
|
626
|
+
| **단일 리소스 상세** | `['user', userId]` | 개별 항목 |
|
|
627
|
+
| **계층적 목록** | `['users', 'list']` | 명시적 목록 구분 |
|
|
628
|
+
| **필터링** | `['users', { role: 'admin' }]` | 필터 조건 포함 |
|
|
629
|
+
| **관계형** | `['user', userId, 'posts']` | 관계 데이터 |
|
|
630
|
+
| **복합** | `['posts', 'list', { page, filters }]` | 페이지네이션 + 필터 |
|
|
631
|
+
|
|
632
|
+
### Query Key 무효화 전략
|
|
633
|
+
|
|
634
|
+
```typescript
|
|
635
|
+
// 모든 users 관련 쿼리 무효화
|
|
636
|
+
queryClient.invalidateQueries({ queryKey: ['users'] })
|
|
637
|
+
// 매칭: ['users'], ['users', 'list'], ['user', 1]
|
|
638
|
+
|
|
639
|
+
// 특정 user만 무효화
|
|
640
|
+
queryClient.invalidateQueries({ queryKey: ['user', userId] })
|
|
641
|
+
// 매칭: ['user', userId], ['user', userId, 'posts']
|
|
642
|
+
|
|
643
|
+
// 정확한 키만 무효화
|
|
644
|
+
queryClient.invalidateQueries({ queryKey: ['users', 'list'], exact: true })
|
|
645
|
+
// 매칭: ['users', 'list']만
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
</query_keys>
|
|
649
|
+
|
|
650
|
+
---
|
|
651
|
+
|
|
652
|
+
<tanstack_start_integration>
|
|
653
|
+
|
|
654
|
+
## TanStack Start 통합
|
|
655
|
+
|
|
656
|
+
### Server Function과 함께 사용
|
|
657
|
+
|
|
658
|
+
```typescript
|
|
659
|
+
// src/functions/user.ts
|
|
660
|
+
import { createServerFn } from '@tanstack/start'
|
|
661
|
+
import { authMiddleware } from '@/middleware/auth'
|
|
662
|
+
import { createUserSchema } from '@/lib/schemas/user'
|
|
663
|
+
|
|
664
|
+
// GET - 인증 필요
|
|
665
|
+
export const getUsers = createServerFn({ method: 'GET' })
|
|
666
|
+
.middleware([authMiddleware])
|
|
667
|
+
.handler(async () => {
|
|
668
|
+
return prisma.user.findMany()
|
|
669
|
+
})
|
|
670
|
+
|
|
671
|
+
// GET - 파라미터
|
|
672
|
+
export const getUserById = createServerFn({ method: 'GET' })
|
|
673
|
+
.inputValidator((data: unknown) => {
|
|
674
|
+
const id = Number(data)
|
|
675
|
+
if (isNaN(id)) throw new Error('Invalid ID')
|
|
676
|
+
return id
|
|
677
|
+
})
|
|
678
|
+
.handler(async ({ data: userId }) => {
|
|
679
|
+
return prisma.user.findUnique({ where: { id: userId } })
|
|
680
|
+
})
|
|
681
|
+
|
|
682
|
+
// POST - 검증 + 인증
|
|
683
|
+
export const createUser = createServerFn({ method: 'POST' })
|
|
684
|
+
.middleware([authMiddleware])
|
|
685
|
+
.inputValidator(createUserSchema)
|
|
686
|
+
.handler(async ({ data }) => {
|
|
687
|
+
return prisma.user.create({ data })
|
|
688
|
+
})
|
|
689
|
+
|
|
690
|
+
// PUT - 수정
|
|
691
|
+
export const updateUser = createServerFn({ method: 'PUT' })
|
|
692
|
+
.middleware([authMiddleware])
|
|
693
|
+
.inputValidator(updateUserSchema)
|
|
694
|
+
.handler(async ({ data }) => {
|
|
695
|
+
return prisma.user.update({
|
|
696
|
+
where: { id: data.id },
|
|
697
|
+
data,
|
|
698
|
+
})
|
|
699
|
+
})
|
|
700
|
+
|
|
701
|
+
// DELETE - 삭제
|
|
702
|
+
export const deleteUser = createServerFn({ method: 'DELETE' })
|
|
703
|
+
.middleware([authMiddleware])
|
|
704
|
+
.inputValidator((data: unknown) => {
|
|
705
|
+
const id = Number(data)
|
|
706
|
+
if (isNaN(id)) throw new Error('Invalid ID')
|
|
707
|
+
return id
|
|
708
|
+
})
|
|
709
|
+
.handler(async ({ data: userId }) => {
|
|
710
|
+
return prisma.user.delete({ where: { id: userId } })
|
|
711
|
+
})
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
### 클라이언트에서 사용
|
|
715
|
+
|
|
716
|
+
```tsx
|
|
717
|
+
// src/routes/users/index.tsx
|
|
718
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
719
|
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
720
|
+
import { getUsers, createUser, deleteUser } from '@/functions/user'
|
|
721
|
+
|
|
722
|
+
export const Route = createFileRoute('/users')({
|
|
723
|
+
component: UsersPage,
|
|
724
|
+
})
|
|
725
|
+
|
|
726
|
+
function UsersPage() {
|
|
727
|
+
const queryClient = useQueryClient()
|
|
728
|
+
|
|
729
|
+
// GET - useQuery
|
|
730
|
+
const { data: users, isLoading } = useQuery({
|
|
731
|
+
queryKey: ['users'],
|
|
732
|
+
queryFn: getUsers,
|
|
733
|
+
})
|
|
734
|
+
|
|
735
|
+
// POST - useMutation
|
|
736
|
+
const createMutation = useMutation({
|
|
737
|
+
mutationFn: createUser,
|
|
738
|
+
onSuccess: () => {
|
|
739
|
+
queryClient.invalidateQueries({ queryKey: ['users'] })
|
|
740
|
+
},
|
|
741
|
+
})
|
|
742
|
+
|
|
743
|
+
// DELETE - useMutation
|
|
744
|
+
const deleteMutation = useMutation({
|
|
745
|
+
mutationFn: deleteUser,
|
|
746
|
+
onMutate: async (userId) => {
|
|
747
|
+
// Optimistic Update
|
|
748
|
+
await queryClient.cancelQueries({ queryKey: ['users'] })
|
|
749
|
+
const previousUsers = queryClient.getQueryData(['users'])
|
|
750
|
+
queryClient.setQueryData(['users'], (old) =>
|
|
751
|
+
old.filter((user) => user.id !== userId)
|
|
752
|
+
)
|
|
753
|
+
return { previousUsers }
|
|
754
|
+
},
|
|
755
|
+
onError: (err, userId, context) => {
|
|
756
|
+
queryClient.setQueryData(['users'], context.previousUsers)
|
|
757
|
+
},
|
|
758
|
+
onSettled: () => {
|
|
759
|
+
queryClient.invalidateQueries({ queryKey: ['users'] })
|
|
760
|
+
},
|
|
761
|
+
})
|
|
762
|
+
|
|
763
|
+
if (isLoading) return <div>Loading...</div>
|
|
764
|
+
|
|
765
|
+
return (
|
|
766
|
+
<div>
|
|
767
|
+
<button onClick={() => createMutation.mutate({ name: 'New User', email: 'new@example.com' })}>
|
|
768
|
+
Create User
|
|
769
|
+
</button>
|
|
770
|
+
<ul>
|
|
771
|
+
{users?.map((user) => (
|
|
772
|
+
<li key={user.id}>
|
|
773
|
+
{user.name}
|
|
774
|
+
<button onClick={() => deleteMutation.mutate(user.id)}>Delete</button>
|
|
775
|
+
</li>
|
|
776
|
+
))}
|
|
777
|
+
</ul>
|
|
778
|
+
</div>
|
|
779
|
+
)
|
|
780
|
+
}
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
### Route Loader와 함께 사용
|
|
784
|
+
|
|
785
|
+
```tsx
|
|
786
|
+
// src/routes/users/$userId.tsx
|
|
787
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
788
|
+
import { useQuery } from '@tanstack/react-query'
|
|
789
|
+
import { getUserById } from '@/functions/user'
|
|
790
|
+
|
|
791
|
+
export const Route = createFileRoute('/users/$userId')({
|
|
792
|
+
component: UserDetailPage,
|
|
793
|
+
loader: async ({ params }) => {
|
|
794
|
+
// SSR에서 prefetch
|
|
795
|
+
const user = await getUserById(Number(params.userId))
|
|
796
|
+
return { user }
|
|
797
|
+
},
|
|
798
|
+
})
|
|
799
|
+
|
|
800
|
+
function UserDetailPage() {
|
|
801
|
+
const { userId } = Route.useParams()
|
|
802
|
+
|
|
803
|
+
// 클라이언트에서는 캐시된 데이터 사용, 없으면 재조회
|
|
804
|
+
const { data: user } = useQuery({
|
|
805
|
+
queryKey: ['user', userId],
|
|
806
|
+
queryFn: () => getUserById(Number(userId)),
|
|
807
|
+
})
|
|
808
|
+
|
|
809
|
+
return <div>{user?.name}</div>
|
|
810
|
+
}
|
|
811
|
+
```
|
|
812
|
+
|
|
813
|
+
### TanStack Start 패턴
|
|
814
|
+
|
|
815
|
+
| 패턴 | Server Function | TanStack Query |
|
|
816
|
+
|------|----------------|----------------|
|
|
817
|
+
| **조회** | `createServerFn({ method: 'GET' })` | `useQuery({ queryFn: serverFn })` |
|
|
818
|
+
| **생성** | `createServerFn({ method: 'POST' })` | `useMutation({ mutationFn: serverFn })` |
|
|
819
|
+
| **수정** | `createServerFn({ method: 'PUT' })` | `useMutation({ mutationFn: serverFn })` |
|
|
820
|
+
| **삭제** | `createServerFn({ method: 'DELETE' })` | `useMutation({ mutationFn: serverFn })` |
|
|
821
|
+
| **SSR Prefetch** | `loader: async () => await serverFn()` | 캐시된 데이터 활용 |
|
|
822
|
+
|
|
823
|
+
</tanstack_start_integration>
|
|
824
|
+
|
|
825
|
+
---
|
|
826
|
+
|
|
827
|
+
<dos_and_donts>
|
|
828
|
+
|
|
829
|
+
## Do's & Don'ts
|
|
830
|
+
|
|
831
|
+
| ✅ Do | ❌ Don't |
|
|
832
|
+
|-------|----------|
|
|
833
|
+
| `useQuery`로 Server Function GET 호출 | 클라이언트에서 Server Function 직접 호출 |
|
|
834
|
+
| `useMutation`로 POST/PUT/PATCH/DELETE | `mutateAsync` 사용 시 try-catch 누락 |
|
|
835
|
+
| Query Key에 파라미터 포함 (`['user', userId]`) | 동적 키 없이 파라미터 쿼리 |
|
|
836
|
+
| `onSuccess`/`onSettled`에서 관련 쿼리만 무효화 | 전역 `invalidateQueries()` 남발 |
|
|
837
|
+
| Optimistic Update 시 `cancelQueries` 먼저 | `cancelQueries` 누락하고 `setQueryData` |
|
|
838
|
+
| `onError`에서 이전 데이터로 롤백 | Optimistic Update 후 롤백 로직 누락 |
|
|
839
|
+
| `staleTime`, `gcTime` 적절히 설정 | 기본값(0)으로 즉시 stale 허용 |
|
|
840
|
+
| 계층적 Query Key (`['todos', 'list', { filters }]`) | 평면적 Query Key (`['todos-list-completed']`) |
|
|
841
|
+
| `refetchType: 'active'`로 현재 화면만 리페치 | 모든 쿼리 리페치 (`refetchType: 'all'`) |
|
|
842
|
+
| `enabled: !!dependency`로 조건부 실행 | 의존성 없이 쿼리 실행 후 에러 처리 |
|
|
843
|
+
|
|
844
|
+
</dos_and_donts>
|
|
845
|
+
|
|
846
|
+
---
|
|
847
|
+
|
|
848
|
+
<reference>
|
|
849
|
+
|
|
850
|
+
## Quick Reference
|
|
851
|
+
|
|
852
|
+
### useQuery 옵션
|
|
853
|
+
|
|
854
|
+
| 옵션 | 타입 | 기본값 | 설명 |
|
|
855
|
+
|------|------|--------|------|
|
|
856
|
+
| `queryKey` | `unknown[]` | (필수) | 쿼리 식별 키 |
|
|
857
|
+
| `queryFn` | `() => Promise<TData>` | (필수) | 데이터 조회 함수 |
|
|
858
|
+
| `staleTime` | `number \| Infinity` | 0 | fresh 상태 유지 시간 (ms) |
|
|
859
|
+
| `gcTime` | `number \| Infinity` | 5분 | 가비지 컬렉션 대기 시간 (ms) |
|
|
860
|
+
| `enabled` | `boolean` | true | 쿼리 실행 여부 |
|
|
861
|
+
| `retry` | `boolean \| number \| (count, error) => boolean` | 3 | 재시도 횟수 |
|
|
862
|
+
| `refetchOnWindowFocus` | `boolean` | true | 포커스 시 리페치 |
|
|
863
|
+
| `refetchInterval` | `number \| false` | false | 자동 리페치 간격 (ms) |
|
|
864
|
+
| `select` | `(data) => TResult` | - | 데이터 변환 |
|
|
865
|
+
| `initialData` | `TData` | - | 초기 데이터 |
|
|
866
|
+
|
|
867
|
+
### useMutation 옵션
|
|
868
|
+
|
|
869
|
+
| 옵션 | 타입 | 설명 |
|
|
870
|
+
|------|------|------|
|
|
871
|
+
| `mutationFn` | `(variables: TVariables) => Promise<TData>` | Mutation 함수 (필수) |
|
|
872
|
+
| `onMutate` | `(variables) => Promise<TContext>` | Mutation 시작 전 |
|
|
873
|
+
| `onSuccess` | `(data, variables, context) => void` | 성공 시 |
|
|
874
|
+
| `onError` | `(error, variables, context) => void` | 실패 시 |
|
|
875
|
+
| `onSettled` | `(data, error, variables, context) => void` | 성공/실패 무관 |
|
|
876
|
+
| `retry` | `boolean \| number` | 재시도 횟수 |
|
|
877
|
+
|
|
878
|
+
### QueryClient 메서드
|
|
879
|
+
|
|
880
|
+
| 메서드 | 설명 |
|
|
881
|
+
|--------|------|
|
|
882
|
+
| `invalidateQueries({ queryKey })` | 쿼리 무효화 (리페치 유도) |
|
|
883
|
+
| `refetchQueries({ queryKey })` | 즉시 리페치 |
|
|
884
|
+
| `cancelQueries({ queryKey })` | 진행 중인 쿼리 취소 |
|
|
885
|
+
| `getQueryData(queryKey)` | 캐시 데이터 조회 |
|
|
886
|
+
| `setQueryData(queryKey, data)` | 캐시 데이터 직접 설정 |
|
|
887
|
+
| `removeQueries({ queryKey })` | 캐시에서 제거 |
|
|
888
|
+
| `resetQueries({ queryKey })` | 초기 상태로 리셋 |
|
|
889
|
+
|
|
890
|
+
### 상태 플래그
|
|
891
|
+
|
|
892
|
+
| 플래그 | useQuery | useMutation | 설명 |
|
|
893
|
+
|--------|----------|-------------|------|
|
|
894
|
+
| `isPending` | ✅ | ✅ | 대기 중 |
|
|
895
|
+
| `isLoading` | ✅ | - | 첫 로딩 중 (데이터 없음 + 페칭) |
|
|
896
|
+
| `isFetching` | ✅ | - | 백그라운드 페칭 중 |
|
|
897
|
+
| `isSuccess` | ✅ | ✅ | 성공 |
|
|
898
|
+
| `isError` | ✅ | ✅ | 에러 |
|
|
899
|
+
| `data` | ✅ | ✅ | 결과 데이터 |
|
|
900
|
+
| `error` | ✅ | ✅ | 에러 객체 |
|
|
901
|
+
|
|
902
|
+
</reference>
|