@kood/claude-code 0.1.0
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.d.ts +2 -0
- package/dist/index.js +297 -0
- package/package.json +47 -0
- package/templates/hono/CLAUDE.md +376 -0
- package/templates/hono/docs/deployment/cloudflare.md +328 -0
- package/templates/hono/docs/deployment/index.md +291 -0
- package/templates/hono/docs/git/index.md +180 -0
- package/templates/hono/docs/library/hono/error-handling.md +400 -0
- package/templates/hono/docs/library/hono/index.md +241 -0
- package/templates/hono/docs/library/hono/middleware.md +334 -0
- package/templates/hono/docs/library/hono/rpc.md +454 -0
- package/templates/hono/docs/library/hono/validation.md +328 -0
- package/templates/hono/docs/library/prisma/index.md +427 -0
- package/templates/hono/docs/library/zod/index.md +413 -0
- package/templates/hono/docs/mcp/context7.md +106 -0
- package/templates/hono/docs/mcp/index.md +94 -0
- package/templates/hono/docs/mcp/sequential-thinking.md +101 -0
- package/templates/hono/docs/mcp/sgrep.md +105 -0
- package/templates/hono/docs/skills/gemini-review/SKILL.md +220 -0
- package/templates/hono/docs/skills/gemini-review/references/checklists.md +136 -0
- package/templates/hono/docs/skills/gemini-review/references/prompt-templates.md +303 -0
- package/templates/tanstack-start/CLAUDE.md +279 -0
- package/templates/tanstack-start/docs/architecture/architecture.md +547 -0
- package/templates/tanstack-start/docs/deployment/cloudflare.md +346 -0
- package/templates/tanstack-start/docs/deployment/index.md +102 -0
- package/templates/tanstack-start/docs/deployment/nitro.md +211 -0
- package/templates/tanstack-start/docs/deployment/railway.md +364 -0
- package/templates/tanstack-start/docs/deployment/vercel.md +287 -0
- package/templates/tanstack-start/docs/design/accessibility.md +433 -0
- package/templates/tanstack-start/docs/design/color.md +235 -0
- package/templates/tanstack-start/docs/design/components.md +409 -0
- package/templates/tanstack-start/docs/design/index.md +107 -0
- package/templates/tanstack-start/docs/design/safe-area.md +317 -0
- package/templates/tanstack-start/docs/design/spacing.md +341 -0
- package/templates/tanstack-start/docs/design/tailwind-setup.md +470 -0
- package/templates/tanstack-start/docs/design/typography.md +324 -0
- package/templates/tanstack-start/docs/git/index.md +203 -0
- package/templates/tanstack-start/docs/guides/best-practices.md +753 -0
- package/templates/tanstack-start/docs/guides/getting-started.md +304 -0
- package/templates/tanstack-start/docs/guides/husky-lint-staged.md +303 -0
- package/templates/tanstack-start/docs/guides/prettier.md +189 -0
- package/templates/tanstack-start/docs/guides/project-templates.md +710 -0
- package/templates/tanstack-start/docs/library/better-auth/2fa.md +136 -0
- package/templates/tanstack-start/docs/library/better-auth/advanced.md +138 -0
- package/templates/tanstack-start/docs/library/better-auth/index.md +83 -0
- package/templates/tanstack-start/docs/library/better-auth/plugins.md +111 -0
- package/templates/tanstack-start/docs/library/better-auth/session.md +127 -0
- package/templates/tanstack-start/docs/library/better-auth/setup.md +123 -0
- package/templates/tanstack-start/docs/library/prisma/crud.md +218 -0
- package/templates/tanstack-start/docs/library/prisma/index.md +165 -0
- package/templates/tanstack-start/docs/library/prisma/relations.md +191 -0
- package/templates/tanstack-start/docs/library/prisma/schema.md +177 -0
- package/templates/tanstack-start/docs/library/prisma/setup.md +156 -0
- package/templates/tanstack-start/docs/library/prisma/transactions.md +140 -0
- package/templates/tanstack-start/docs/library/tanstack-query/index.md +146 -0
- package/templates/tanstack-start/docs/library/tanstack-query/invalidation.md +146 -0
- package/templates/tanstack-start/docs/library/tanstack-query/optimistic-updates.md +196 -0
- package/templates/tanstack-start/docs/library/tanstack-query/setup.md +110 -0
- package/templates/tanstack-start/docs/library/tanstack-query/use-mutation.md +170 -0
- package/templates/tanstack-start/docs/library/tanstack-query/use-query.md +173 -0
- package/templates/tanstack-start/docs/library/tanstack-start/auth-patterns.md +171 -0
- package/templates/tanstack-start/docs/library/tanstack-start/index.md +114 -0
- package/templates/tanstack-start/docs/library/tanstack-start/middleware.md +142 -0
- package/templates/tanstack-start/docs/library/tanstack-start/routing.md +163 -0
- package/templates/tanstack-start/docs/library/tanstack-start/server-functions.md +128 -0
- package/templates/tanstack-start/docs/library/tanstack-start/setup.md +85 -0
- package/templates/tanstack-start/docs/library/zod/basic-types.md +186 -0
- package/templates/tanstack-start/docs/library/zod/complex-types.md +204 -0
- package/templates/tanstack-start/docs/library/zod/index.md +186 -0
- package/templates/tanstack-start/docs/library/zod/transforms.md +174 -0
- package/templates/tanstack-start/docs/library/zod/validation.md +208 -0
- package/templates/tanstack-start/docs/mcp/context7.md +204 -0
- package/templates/tanstack-start/docs/mcp/index.md +116 -0
- package/templates/tanstack-start/docs/mcp/sequential-thinking.md +180 -0
- package/templates/tanstack-start/docs/mcp/sgrep.md +174 -0
- package/templates/tanstack-start/docs/skills/gemini-review/SKILL.md +220 -0
- package/templates/tanstack-start/docs/skills/gemini-review/references/checklists.md +150 -0
- package/templates/tanstack-start/docs/skills/gemini-review/references/prompt-templates.md +293 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# TanStack Query - Query 무효화
|
|
2
|
+
|
|
3
|
+
> **상위 문서**: [TanStack Query](./index.md)
|
|
4
|
+
|
|
5
|
+
## 기본 무효화
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
const queryClient = useQueryClient()
|
|
9
|
+
|
|
10
|
+
// 단일 쿼리 무효화
|
|
11
|
+
await queryClient.invalidateQueries({ queryKey: ['todos'] })
|
|
12
|
+
|
|
13
|
+
// 다중 쿼리 무효화
|
|
14
|
+
await Promise.all([
|
|
15
|
+
queryClient.invalidateQueries({ queryKey: ['todos'] }),
|
|
16
|
+
queryClient.invalidateQueries({ queryKey: ['reminders'] }),
|
|
17
|
+
])
|
|
18
|
+
|
|
19
|
+
// 모든 쿼리 무효화
|
|
20
|
+
queryClient.invalidateQueries()
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## 무효화 옵션
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
await queryClient.invalidateQueries(
|
|
27
|
+
{
|
|
28
|
+
queryKey: ['posts'],
|
|
29
|
+
exact: true, // 정확한 키 매칭만
|
|
30
|
+
refetchType: 'active', // 'active' | 'inactive' | 'all' | 'none'
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
throwOnError: false,
|
|
34
|
+
cancelRefetch: true,
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## refetchType 옵션
|
|
40
|
+
|
|
41
|
+
- `active`: 현재 렌더링 중인 쿼리만 재조회 (기본값)
|
|
42
|
+
- `inactive`: 비활성 쿼리만 재조회
|
|
43
|
+
- `all`: 모든 매칭 쿼리 재조회
|
|
44
|
+
- `none`: 무효화만 하고 재조회 안 함
|
|
45
|
+
|
|
46
|
+
## Query Key로 무효화
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
// 'todos'로 시작하는 모든 쿼리 무효화
|
|
50
|
+
queryClient.invalidateQueries({ queryKey: ['todos'] })
|
|
51
|
+
|
|
52
|
+
// 정확히 ['todos', 'list']인 쿼리만 무효화
|
|
53
|
+
queryClient.invalidateQueries({
|
|
54
|
+
queryKey: ['todos', 'list'],
|
|
55
|
+
exact: true,
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
// ['todos', { type: 'done' }] 무효화
|
|
59
|
+
queryClient.invalidateQueries({
|
|
60
|
+
queryKey: ['todos', { type: 'done' }],
|
|
61
|
+
})
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Mutation과 함께 사용
|
|
65
|
+
|
|
66
|
+
### onSuccess에서 무효화
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
70
|
+
|
|
71
|
+
function TodosComponent() {
|
|
72
|
+
const queryClient = useQueryClient()
|
|
73
|
+
|
|
74
|
+
const mutation = useMutation({
|
|
75
|
+
mutationFn: addTodo,
|
|
76
|
+
onSuccess: async () => {
|
|
77
|
+
// 단일 쿼리 무효화
|
|
78
|
+
await queryClient.invalidateQueries({ queryKey: ['todos'] })
|
|
79
|
+
|
|
80
|
+
// 여러 쿼리 무효화
|
|
81
|
+
await Promise.all([
|
|
82
|
+
queryClient.invalidateQueries({ queryKey: ['todos'] }),
|
|
83
|
+
queryClient.invalidateQueries({ queryKey: ['reminders'] }),
|
|
84
|
+
])
|
|
85
|
+
},
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<button onClick={() => mutation.mutate(newTodo)}>
|
|
90
|
+
Add Todo
|
|
91
|
+
</button>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### onSettled에서 무효화
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
const mutation = useMutation({
|
|
100
|
+
mutationFn: updateTodo,
|
|
101
|
+
onSettled: () => {
|
|
102
|
+
// 성공/실패 관계없이 무효화
|
|
103
|
+
queryClient.invalidateQueries({ queryKey: ['todos'] })
|
|
104
|
+
},
|
|
105
|
+
})
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## 캐시 직접 업데이트 vs 무효화
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
// 방법 1: 캐시 직접 업데이트 (더 빠름)
|
|
112
|
+
queryClient.setQueryData(['todos'], (old) => [...old, newTodo])
|
|
113
|
+
|
|
114
|
+
// 방법 2: 무효화 후 재조회 (서버 데이터 보장)
|
|
115
|
+
queryClient.invalidateQueries({ queryKey: ['todos'] })
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## 조건부 무효화
|
|
119
|
+
|
|
120
|
+
```tsx
|
|
121
|
+
const mutation = useMutation({
|
|
122
|
+
mutationFn: updateTodo,
|
|
123
|
+
onSuccess: (data, variables) => {
|
|
124
|
+
// 특정 조건에서만 무효화
|
|
125
|
+
if (variables.category === 'important') {
|
|
126
|
+
queryClient.invalidateQueries({ queryKey: ['todos', 'important'] })
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 항상 개별 항목은 무효화
|
|
130
|
+
queryClient.invalidateQueries({ queryKey: ['todo', variables.id] })
|
|
131
|
+
},
|
|
132
|
+
})
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## prefetch와 조합
|
|
136
|
+
|
|
137
|
+
```tsx
|
|
138
|
+
// 무효화 전에 새 데이터 프리페치
|
|
139
|
+
await queryClient.prefetchQuery({
|
|
140
|
+
queryKey: ['todos'],
|
|
141
|
+
queryFn: fetchTodos,
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
// 이후 무효화 시 이미 새 데이터가 있음
|
|
145
|
+
queryClient.invalidateQueries({ queryKey: ['todos'] })
|
|
146
|
+
```
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# TanStack Query - Optimistic Updates
|
|
2
|
+
|
|
3
|
+
> **상위 문서**: [TanStack Query](./index.md)
|
|
4
|
+
|
|
5
|
+
낙관적 업데이트는 서버 응답을 기다리지 않고 UI를 즉시 업데이트하는 패턴입니다.
|
|
6
|
+
|
|
7
|
+
## 기본 패턴
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
const addTodoMutation = useMutation({
|
|
11
|
+
mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }),
|
|
12
|
+
// mutation 완료 후 쿼리 무효화
|
|
13
|
+
onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
const { isPending, submittedAt, variables, mutate, isError } = addTodoMutation
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## 완전한 Optimistic Update 패턴
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
type Todo = { id: string; text: string }
|
|
23
|
+
type TodosResponse = { items: Todo[]; ts: number }
|
|
24
|
+
|
|
25
|
+
function TodoList() {
|
|
26
|
+
const queryClient = useQueryClient()
|
|
27
|
+
const [text, setText] = React.useState('')
|
|
28
|
+
|
|
29
|
+
const addTodoMutation = useMutation({
|
|
30
|
+
mutationFn: async (newTodo: string): Promise<Todo> => {
|
|
31
|
+
const response = await fetch('/api/todos', {
|
|
32
|
+
method: 'POST',
|
|
33
|
+
headers: { 'Content-Type': 'application/json' },
|
|
34
|
+
body: JSON.stringify({ text: newTodo }),
|
|
35
|
+
})
|
|
36
|
+
return response.json()
|
|
37
|
+
},
|
|
38
|
+
onMutate: async (newTodo) => {
|
|
39
|
+
// 1. 진행 중인 refetch 취소 (optimistic update 덮어쓰기 방지)
|
|
40
|
+
await queryClient.cancelQueries({ queryKey: ['todos'] })
|
|
41
|
+
|
|
42
|
+
// 2. 롤백을 위한 이전 값 스냅샷
|
|
43
|
+
const previousTodos = queryClient.getQueryData<TodosResponse>(['todos'])
|
|
44
|
+
|
|
45
|
+
// 3. 캐시 optimistic 업데이트
|
|
46
|
+
if (previousTodos) {
|
|
47
|
+
queryClient.setQueryData<TodosResponse>(['todos'], {
|
|
48
|
+
...previousTodos,
|
|
49
|
+
items: [
|
|
50
|
+
...previousTodos.items,
|
|
51
|
+
{ id: Math.random().toString(), text: newTodo },
|
|
52
|
+
],
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 4. context로 이전 값 반환
|
|
57
|
+
return { previousTodos }
|
|
58
|
+
},
|
|
59
|
+
onError: (err, variables, context) => {
|
|
60
|
+
// 5. 에러 시 롤백
|
|
61
|
+
if (context?.previousTodos) {
|
|
62
|
+
queryClient.setQueryData(['todos'], context.previousTodos)
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
onSettled: () => {
|
|
66
|
+
// 6. 서버와 캐시 동기화를 위해 refetch
|
|
67
|
+
queryClient.invalidateQueries({ queryKey: ['todos'] })
|
|
68
|
+
},
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div>
|
|
73
|
+
<form onSubmit={(e) => {
|
|
74
|
+
e.preventDefault()
|
|
75
|
+
addTodoMutation.mutate(text)
|
|
76
|
+
setText('')
|
|
77
|
+
}}>
|
|
78
|
+
<input value={text} onChange={(e) => setText(e.target.value)} />
|
|
79
|
+
<button type="submit" disabled={addTodoMutation.isPending}>
|
|
80
|
+
{addTodoMutation.isPending ? 'Adding...' : 'Add Todo'}
|
|
81
|
+
</button>
|
|
82
|
+
</form>
|
|
83
|
+
{addTodoMutation.isError && <div>Error: {addTodoMutation.error.message}</div>}
|
|
84
|
+
</div>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## 단일 항목 Optimistic Update
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
useMutation({
|
|
93
|
+
mutationFn: updateTodo,
|
|
94
|
+
onMutate: async (newTodo) => {
|
|
95
|
+
// 진행 중인 refetch 취소
|
|
96
|
+
await queryClient.cancelQueries({ queryKey: ['todos', newTodo.id] })
|
|
97
|
+
|
|
98
|
+
// 이전 값 스냅샷
|
|
99
|
+
const previousTodo = queryClient.getQueryData(['todos', newTodo.id])
|
|
100
|
+
|
|
101
|
+
// Optimistic 업데이트
|
|
102
|
+
queryClient.setQueryData(['todos', newTodo.id], newTodo)
|
|
103
|
+
|
|
104
|
+
return { previousTodo, newTodo }
|
|
105
|
+
},
|
|
106
|
+
onError: (err, newTodo, context) => {
|
|
107
|
+
// 에러 시 롤백
|
|
108
|
+
queryClient.setQueryData(
|
|
109
|
+
['todos', context.newTodo.id],
|
|
110
|
+
context.previousTodo,
|
|
111
|
+
)
|
|
112
|
+
},
|
|
113
|
+
onSettled: (newTodo) => {
|
|
114
|
+
queryClient.invalidateQueries({ queryKey: ['todos', newTodo.id] })
|
|
115
|
+
},
|
|
116
|
+
})
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## 삭제 Optimistic Update
|
|
120
|
+
|
|
121
|
+
```tsx
|
|
122
|
+
const deleteMutation = useMutation({
|
|
123
|
+
mutationFn: (todoId: string) => deleteTodo(todoId),
|
|
124
|
+
onMutate: async (todoId) => {
|
|
125
|
+
await queryClient.cancelQueries({ queryKey: ['todos'] })
|
|
126
|
+
|
|
127
|
+
const previousTodos = queryClient.getQueryData<Todo[]>(['todos'])
|
|
128
|
+
|
|
129
|
+
// 삭제된 항목 제외
|
|
130
|
+
queryClient.setQueryData<Todo[]>(['todos'], (old) =>
|
|
131
|
+
old?.filter((todo) => todo.id !== todoId)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
return { previousTodos }
|
|
135
|
+
},
|
|
136
|
+
onError: (err, todoId, context) => {
|
|
137
|
+
queryClient.setQueryData(['todos'], context?.previousTodos)
|
|
138
|
+
},
|
|
139
|
+
onSettled: () => {
|
|
140
|
+
queryClient.invalidateQueries({ queryKey: ['todos'] })
|
|
141
|
+
},
|
|
142
|
+
})
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## 토글 Optimistic Update
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
const toggleMutation = useMutation({
|
|
149
|
+
mutationFn: (todoId: string) => toggleTodo(todoId),
|
|
150
|
+
onMutate: async (todoId) => {
|
|
151
|
+
await queryClient.cancelQueries({ queryKey: ['todos'] })
|
|
152
|
+
|
|
153
|
+
const previousTodos = queryClient.getQueryData<Todo[]>(['todos'])
|
|
154
|
+
|
|
155
|
+
// 토글 상태 변경
|
|
156
|
+
queryClient.setQueryData<Todo[]>(['todos'], (old) =>
|
|
157
|
+
old?.map((todo) =>
|
|
158
|
+
todo.id === todoId
|
|
159
|
+
? { ...todo, completed: !todo.completed }
|
|
160
|
+
: todo
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
return { previousTodos }
|
|
165
|
+
},
|
|
166
|
+
onError: (err, todoId, context) => {
|
|
167
|
+
queryClient.setQueryData(['todos'], context?.previousTodos)
|
|
168
|
+
},
|
|
169
|
+
onSettled: () => {
|
|
170
|
+
queryClient.invalidateQueries({ queryKey: ['todos'] })
|
|
171
|
+
},
|
|
172
|
+
})
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Persist (영속성)
|
|
176
|
+
|
|
177
|
+
### Optimistic Update 즉시 저장
|
|
178
|
+
|
|
179
|
+
```tsx
|
|
180
|
+
const persister = experimental_createQueryPersister({
|
|
181
|
+
storage: AsyncStorage,
|
|
182
|
+
maxAge: 1000 * 60 * 60 * 12, // 12시간
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
const queryClient = useQueryClient()
|
|
186
|
+
|
|
187
|
+
useMutation({
|
|
188
|
+
mutationFn: updateTodo,
|
|
189
|
+
onMutate: async (newTodo) => {
|
|
190
|
+
// Optimistic 업데이트
|
|
191
|
+
queryClient.setQueryData(['todos'], (old) => [...old, newTodo])
|
|
192
|
+
// 즉시 스토리지에 저장
|
|
193
|
+
persister.persistQueryByKey(['todos'], queryClient)
|
|
194
|
+
},
|
|
195
|
+
})
|
|
196
|
+
```
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# TanStack Query - 설치 및 설정
|
|
2
|
+
|
|
3
|
+
> **상위 문서**: [TanStack Query](./index.md)
|
|
4
|
+
|
|
5
|
+
## 설치
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
yarn add @tanstack/react-query
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## QueryClient 설정
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import {
|
|
15
|
+
QueryClient,
|
|
16
|
+
QueryClientProvider,
|
|
17
|
+
} from '@tanstack/react-query'
|
|
18
|
+
|
|
19
|
+
// QueryClient 생성
|
|
20
|
+
const queryClient = new QueryClient()
|
|
21
|
+
|
|
22
|
+
function App() {
|
|
23
|
+
return (
|
|
24
|
+
<QueryClientProvider client={queryClient}>
|
|
25
|
+
<YourApp />
|
|
26
|
+
</QueryClientProvider>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## QueryClient 옵션
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
const queryClient = new QueryClient({
|
|
35
|
+
defaultOptions: {
|
|
36
|
+
queries: {
|
|
37
|
+
staleTime: 1000 * 60 * 5, // 5분
|
|
38
|
+
gcTime: 1000 * 60 * 30, // 30분 (이전의 cacheTime)
|
|
39
|
+
retry: 3,
|
|
40
|
+
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
|
|
41
|
+
refetchOnWindowFocus: true,
|
|
42
|
+
refetchOnReconnect: true,
|
|
43
|
+
},
|
|
44
|
+
mutations: {
|
|
45
|
+
retry: 1,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## TanStack Start와 함께 사용
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
import { useQuery } from '@tanstack/react-query'
|
|
55
|
+
import { useServerFn } from '@tanstack/react-start'
|
|
56
|
+
import { getServerPosts } from '~/lib/server-functions'
|
|
57
|
+
|
|
58
|
+
function PostList() {
|
|
59
|
+
const getPosts = useServerFn(getServerPosts)
|
|
60
|
+
|
|
61
|
+
const { data, isLoading, error } = useQuery({
|
|
62
|
+
queryKey: ['posts'],
|
|
63
|
+
queryFn: () => getPosts(),
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
if (isLoading) return <div>Loading...</div>
|
|
67
|
+
if (error) return <div>Error: {error.message}</div>
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<ul>
|
|
71
|
+
{data?.map((post) => (
|
|
72
|
+
<li key={post.id}>{post.title}</li>
|
|
73
|
+
))}
|
|
74
|
+
</ul>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Query Keys 패턴
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
// 간단한 키
|
|
83
|
+
['todos']
|
|
84
|
+
|
|
85
|
+
// 파라미터가 있는 키
|
|
86
|
+
['todo', { id: 5 }]
|
|
87
|
+
|
|
88
|
+
// 계층적 키
|
|
89
|
+
['todos', 'list', { filters: 'all' }]
|
|
90
|
+
['todos', 'detail', todoId]
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## DevTools (개발용)
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
yarn add @tanstack/react-query-devtools
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
|
|
101
|
+
|
|
102
|
+
function App() {
|
|
103
|
+
return (
|
|
104
|
+
<QueryClientProvider client={queryClient}>
|
|
105
|
+
<YourApp />
|
|
106
|
+
<ReactQueryDevtools initialIsOpen={false} />
|
|
107
|
+
</QueryClientProvider>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
```
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# TanStack Query - useMutation
|
|
2
|
+
|
|
3
|
+
> **상위 문서**: [TanStack Query](./index.md)
|
|
4
|
+
|
|
5
|
+
## 기본 사용법
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
9
|
+
|
|
10
|
+
function AddTodo() {
|
|
11
|
+
const queryClient = useQueryClient()
|
|
12
|
+
|
|
13
|
+
const mutation = useMutation({
|
|
14
|
+
mutationFn: postTodo,
|
|
15
|
+
onSuccess: () => {
|
|
16
|
+
// 성공 시 todos 쿼리 무효화하여 재조회
|
|
17
|
+
queryClient.invalidateQueries({ queryKey: ['todos'] })
|
|
18
|
+
},
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<button
|
|
23
|
+
onClick={() => {
|
|
24
|
+
mutation.mutate({
|
|
25
|
+
id: Date.now(),
|
|
26
|
+
title: 'Do Laundry',
|
|
27
|
+
})
|
|
28
|
+
}}
|
|
29
|
+
>
|
|
30
|
+
Add Todo
|
|
31
|
+
</button>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## useMutation 반환 값
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
const {
|
|
40
|
+
data, // mutation 결과 데이터
|
|
41
|
+
error, // 에러 객체
|
|
42
|
+
isError, // 에러 상태
|
|
43
|
+
isIdle, // 아직 실행 안 됨
|
|
44
|
+
isPending, // 실행 중
|
|
45
|
+
isPaused, // 일시 정지됨
|
|
46
|
+
isSuccess, // 성공 상태
|
|
47
|
+
failureCount, // 실패 횟수
|
|
48
|
+
failureReason, // 실패 이유
|
|
49
|
+
mutate, // mutation 실행 (비동기)
|
|
50
|
+
mutateAsync, // mutation 실행 (Promise 반환)
|
|
51
|
+
reset, // 상태 초기화
|
|
52
|
+
status, // 'idle' | 'pending' | 'success' | 'error'
|
|
53
|
+
submittedAt, // 제출 시간
|
|
54
|
+
variables, // 전달된 변수
|
|
55
|
+
} = useMutation({ mutationFn })
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## useMutation 옵션
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
const mutation = useMutation({
|
|
62
|
+
mutationFn,
|
|
63
|
+
gcTime, // 가비지 컬렉션 시간
|
|
64
|
+
meta, // 메타데이터
|
|
65
|
+
mutationKey, // mutation 키
|
|
66
|
+
networkMode, // 네트워크 모드
|
|
67
|
+
onError, // 에러 콜백
|
|
68
|
+
onMutate, // mutation 시작 전 콜백
|
|
69
|
+
onSettled, // 완료 후 콜백 (성공/실패 무관)
|
|
70
|
+
onSuccess, // 성공 콜백
|
|
71
|
+
retry, // 재시도 횟수
|
|
72
|
+
retryDelay, // 재시도 딜레이
|
|
73
|
+
scope, // 범위
|
|
74
|
+
throwOnError, // 에러 throw 여부
|
|
75
|
+
})
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## 콜백 함수들
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
const mutation = useMutation({
|
|
82
|
+
mutationFn: updateTodo,
|
|
83
|
+
onMutate: async (newTodo) => {
|
|
84
|
+
// mutation 시작 전 실행
|
|
85
|
+
console.log('Starting mutation with:', newTodo)
|
|
86
|
+
return { previousData: 'something' } // context로 전달
|
|
87
|
+
},
|
|
88
|
+
onSuccess: (data, variables, context) => {
|
|
89
|
+
// 성공 시 실행
|
|
90
|
+
console.log('Success:', data)
|
|
91
|
+
console.log('Variables:', variables)
|
|
92
|
+
console.log('Context:', context)
|
|
93
|
+
},
|
|
94
|
+
onError: (error, variables, context) => {
|
|
95
|
+
// 에러 시 실행
|
|
96
|
+
console.log('Error:', error)
|
|
97
|
+
// context로 롤백 가능
|
|
98
|
+
},
|
|
99
|
+
onSettled: (data, error, variables, context) => {
|
|
100
|
+
// 성공/실패 무관하게 완료 시 실행
|
|
101
|
+
queryClient.invalidateQueries({ queryKey: ['todos'] })
|
|
102
|
+
},
|
|
103
|
+
})
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## mutate vs mutateAsync
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
// mutate - 콜백 기반
|
|
110
|
+
mutation.mutate(data, {
|
|
111
|
+
onSuccess: (result) => {
|
|
112
|
+
console.log('Success:', result)
|
|
113
|
+
},
|
|
114
|
+
onError: (error) => {
|
|
115
|
+
console.log('Error:', error)
|
|
116
|
+
},
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
// mutateAsync - Promise 기반
|
|
120
|
+
try {
|
|
121
|
+
const result = await mutation.mutateAsync(data)
|
|
122
|
+
console.log('Success:', result)
|
|
123
|
+
} catch (error) {
|
|
124
|
+
console.log('Error:', error)
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Error 상태 Reset
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
const CreateTodo = () => {
|
|
132
|
+
const [title, setTitle] = useState('')
|
|
133
|
+
const mutation = useMutation({ mutationFn: createTodo })
|
|
134
|
+
|
|
135
|
+
const onCreateTodo = (e) => {
|
|
136
|
+
e.preventDefault()
|
|
137
|
+
mutation.mutate({ title })
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<form onSubmit={onCreateTodo}>
|
|
142
|
+
{mutation.error && (
|
|
143
|
+
<h5 onClick={() => mutation.reset()}>{mutation.error}</h5>
|
|
144
|
+
)}
|
|
145
|
+
<input
|
|
146
|
+
type="text"
|
|
147
|
+
value={title}
|
|
148
|
+
onChange={(e) => setTitle(e.target.value)}
|
|
149
|
+
/>
|
|
150
|
+
<br />
|
|
151
|
+
<button type="submit">Create Todo</button>
|
|
152
|
+
</form>
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## 캐시 직접 업데이트
|
|
158
|
+
|
|
159
|
+
```tsx
|
|
160
|
+
const saveMutation = useMutation({
|
|
161
|
+
mutationFn: patchTodo,
|
|
162
|
+
onSuccess: (data) => {
|
|
163
|
+
// 목록 쿼리 무효화
|
|
164
|
+
queryClient.invalidateQueries({ queryKey: ['todos'] })
|
|
165
|
+
|
|
166
|
+
// 개별 항목 캐시 직접 업데이트
|
|
167
|
+
queryClient.setQueryData(['todo', { id: data.id }], data)
|
|
168
|
+
},
|
|
169
|
+
})
|
|
170
|
+
```
|