@kood/claude-code 0.1.5 → 0.1.6
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/package.json +1 -1
- package/templates/tanstack-start/CLAUDE.md +6 -8
- package/templates/tanstack-start/docs/guides/best-practices.md +3 -8
- package/templates/tanstack-start/docs/guides/env-setup.md +3 -3
- package/templates/tanstack-start/docs/library/better-auth/2fa.md +1 -1
- package/templates/tanstack-start/docs/library/better-auth/setup.md +1 -1
- package/templates/tanstack-start/docs/library/prisma/setup.md +1 -1
- package/templates/tanstack-start/docs/library/tanstack-query/setup.md +2 -5
- package/templates/tanstack-start/docs/library/tanstack-start/auth-patterns.md +1 -1
- package/templates/tanstack-start/docs/library/tanstack-start/server-functions.md +90 -9
- package/templates/tanstack-start/docs/library/tanstack-start/setup.md +1 -1
package/package.json
CHANGED
|
@@ -277,21 +277,19 @@ export const Route = createFileRoute('/users')({
|
|
|
277
277
|
### TanStack Query (서버 연동 시 필수)
|
|
278
278
|
```tsx
|
|
279
279
|
// ✅ 데이터 조회: useQuery 필수
|
|
280
|
-
const getPosts = useServerFn(getServerPosts)
|
|
281
280
|
const { data } = useQuery({
|
|
282
|
-
queryKey: ['
|
|
283
|
-
queryFn: () =>
|
|
281
|
+
queryKey: ['posts'],
|
|
282
|
+
queryFn: () => getServerPosts(),
|
|
284
283
|
})
|
|
285
284
|
|
|
286
285
|
// ✅ 데이터 변경: useMutation 필수
|
|
287
|
-
const createPostFn = useServerFn(createPost)
|
|
288
286
|
const mutation = useMutation({
|
|
289
|
-
mutationFn:
|
|
290
|
-
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['
|
|
287
|
+
mutationFn: createPost,
|
|
288
|
+
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['posts'] }),
|
|
291
289
|
})
|
|
292
290
|
|
|
293
|
-
// ❌ 금지: Server Function 직접 호출
|
|
294
|
-
// useEffect(() => {
|
|
291
|
+
// ❌ 금지: Server Function을 TanStack Query 없이 직접 호출
|
|
292
|
+
// useEffect(() => { getServerPosts().then(setData) }, [])
|
|
295
293
|
```
|
|
296
294
|
|
|
297
295
|
---
|
|
@@ -304,7 +304,6 @@ const UsersPage = (): JSX.Element => {
|
|
|
304
304
|
import { useState, useMemo, useEffect, useCallback } from 'react'
|
|
305
305
|
import { useParams, useNavigate } from '@tanstack/react-router'
|
|
306
306
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
307
|
-
import { useServerFn } from '@tanstack/react-start'
|
|
308
307
|
import { useAuthStore } from '@/stores/auth'
|
|
309
308
|
import { getUsers, createUser, deleteUser } from '@/services/user'
|
|
310
309
|
import type { User } from '@/types'
|
|
@@ -339,24 +338,20 @@ export const useUsers = (): UseUsersReturn => {
|
|
|
339
338
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
340
339
|
// 3. React Query (useQuery → useMutation)
|
|
341
340
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
342
|
-
const getUsersFn = useServerFn(getUsers)
|
|
343
|
-
const createUserFn = useServerFn(createUser)
|
|
344
|
-
const deleteUserFn = useServerFn(deleteUser)
|
|
345
|
-
|
|
346
341
|
const { data: users, isLoading, error } = useQuery({
|
|
347
342
|
queryKey: ['users'],
|
|
348
|
-
queryFn: () =>
|
|
343
|
+
queryFn: () => getUsers(),
|
|
349
344
|
})
|
|
350
345
|
|
|
351
346
|
const createMutation = useMutation({
|
|
352
|
-
mutationFn:
|
|
347
|
+
mutationFn: createUser,
|
|
353
348
|
onSuccess: () => {
|
|
354
349
|
queryClient.invalidateQueries({ queryKey: ['users'] })
|
|
355
350
|
},
|
|
356
351
|
})
|
|
357
352
|
|
|
358
353
|
const deleteMutation = useMutation({
|
|
359
|
-
mutationFn:
|
|
354
|
+
mutationFn: deleteUser,
|
|
360
355
|
onSuccess: () => {
|
|
361
356
|
queryClient.invalidateQueries({ queryKey: ['users'] })
|
|
362
357
|
},
|
|
@@ -226,7 +226,7 @@ export const clientEnv = parseClientEnv()
|
|
|
226
226
|
```typescript
|
|
227
227
|
// app/server-functions/users.ts
|
|
228
228
|
import { createServerFn } from '@tanstack/react-start'
|
|
229
|
-
import { getServerEnv } from '
|
|
229
|
+
import { getServerEnv } from '@/config/env'
|
|
230
230
|
|
|
231
231
|
export const getUsers = createServerFn({ method: 'GET' })
|
|
232
232
|
.handler(async () => {
|
|
@@ -248,7 +248,7 @@ export const getUsers = createServerFn({ method: 'GET' })
|
|
|
248
248
|
|
|
249
249
|
```tsx
|
|
250
250
|
// app/components/AppHeader.tsx
|
|
251
|
-
import { clientEnv } from '
|
|
251
|
+
import { clientEnv } from '@/config/env'
|
|
252
252
|
|
|
253
253
|
export const AppHeader = () => {
|
|
254
254
|
return (
|
|
@@ -282,7 +282,7 @@ const appName = import.meta.env.VITE_APP_NAME // ✅ VITE_ 접두사만
|
|
|
282
282
|
|
|
283
283
|
```typescript
|
|
284
284
|
// app/lib/auth.ts
|
|
285
|
-
import { getServerEnv } from '
|
|
285
|
+
import { getServerEnv } from '@/config/env'
|
|
286
286
|
|
|
287
287
|
export const getAuthConfig = () => {
|
|
288
288
|
const env = getServerEnv()
|
|
@@ -103,7 +103,7 @@ await authClient.twoFactor.disable({
|
|
|
103
103
|
```tsx
|
|
104
104
|
// pages/two-factor.tsx
|
|
105
105
|
import { useState } from 'react'
|
|
106
|
-
import { authClient } from '
|
|
106
|
+
import { authClient } from '@/lib/auth-client'
|
|
107
107
|
|
|
108
108
|
export default function TwoFactorPage() {
|
|
109
109
|
const [code, setCode] = useState('')
|
|
@@ -111,7 +111,7 @@ export const auth = betterAuth({
|
|
|
111
111
|
|
|
112
112
|
// Server Function에서 사용
|
|
113
113
|
import { createServerFn } from '@tanstack/react-start'
|
|
114
|
-
import { auth } from '
|
|
114
|
+
import { auth } from '@/lib/auth'
|
|
115
115
|
|
|
116
116
|
export const getSession = createServerFn({ method: 'GET' })
|
|
117
117
|
.handler(async ({ request }) => {
|
|
@@ -113,7 +113,7 @@ if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
|
|
|
113
113
|
|
|
114
114
|
// Server Function에서 사용
|
|
115
115
|
import { createServerFn } from '@tanstack/react-start'
|
|
116
|
-
import { prisma } from '
|
|
116
|
+
import { prisma } from '@/lib/prisma'
|
|
117
117
|
|
|
118
118
|
export const getUsers = createServerFn({ method: 'GET' })
|
|
119
119
|
.handler(async () => {
|
|
@@ -52,15 +52,12 @@ const queryClient = new QueryClient({
|
|
|
52
52
|
|
|
53
53
|
```tsx
|
|
54
54
|
import { useQuery } from '@tanstack/react-query'
|
|
55
|
-
import {
|
|
56
|
-
import { getServerPosts } from '~/lib/server-functions'
|
|
55
|
+
import { getServerPosts } from '@/lib/server-functions'
|
|
57
56
|
|
|
58
57
|
function PostList() {
|
|
59
|
-
const getPosts = useServerFn(getServerPosts)
|
|
60
|
-
|
|
61
58
|
const { data, isLoading, error } = useQuery({
|
|
62
59
|
queryKey: ['posts'],
|
|
63
|
-
queryFn: () =>
|
|
60
|
+
queryFn: () => getServerPosts(),
|
|
64
61
|
})
|
|
65
62
|
|
|
66
63
|
if (isLoading) return <div>Loading...</div>
|
|
@@ -159,7 +159,7 @@ export const auth = betterAuth({
|
|
|
159
159
|
|
|
160
160
|
// Server Function에서 사용
|
|
161
161
|
import { createServerFn } from '@tanstack/react-start'
|
|
162
|
-
import { auth } from '
|
|
162
|
+
import { auth } from '@/lib/auth'
|
|
163
163
|
|
|
164
164
|
export const getSession = createServerFn({ method: 'GET' })
|
|
165
165
|
.handler(async ({ request }) => {
|
|
@@ -93,16 +93,13 @@ export const submitForm = createServerFn({ method: 'POST' })
|
|
|
93
93
|
### ✅ 올바른 패턴: useQuery 사용 (데이터 조회)
|
|
94
94
|
|
|
95
95
|
```tsx
|
|
96
|
-
import { useServerFn } from '@tanstack/react-start'
|
|
97
96
|
import { useQuery } from '@tanstack/react-query'
|
|
98
|
-
import { getServerPosts } from '
|
|
97
|
+
import { getServerPosts } from '@/lib/server-functions'
|
|
99
98
|
|
|
100
99
|
function PostList() {
|
|
101
|
-
const getPosts = useServerFn(getServerPosts)
|
|
102
|
-
|
|
103
100
|
const { data, isLoading, error } = useQuery({
|
|
104
101
|
queryKey: ['posts'],
|
|
105
|
-
queryFn: () =>
|
|
102
|
+
queryFn: () => getServerPosts(),
|
|
106
103
|
})
|
|
107
104
|
|
|
108
105
|
if (isLoading) return <div>Loading...</div>
|
|
@@ -121,16 +118,14 @@ function PostList() {
|
|
|
121
118
|
### ✅ 올바른 패턴: useMutation 사용 (데이터 변경)
|
|
122
119
|
|
|
123
120
|
```tsx
|
|
124
|
-
import { useServerFn } from '@tanstack/react-start'
|
|
125
121
|
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
126
|
-
import { createPost, deletePost } from '
|
|
122
|
+
import { createPost, deletePost } from '@/lib/server-functions'
|
|
127
123
|
|
|
128
124
|
function PostForm() {
|
|
129
125
|
const queryClient = useQueryClient()
|
|
130
|
-
const createPostFn = useServerFn(createPost)
|
|
131
126
|
|
|
132
127
|
const mutation = useMutation({
|
|
133
|
-
mutationFn: (data: { title: string; content: string }) =>
|
|
128
|
+
mutationFn: (data: { title: string; content: string }) => createPost({ data }),
|
|
134
129
|
onSuccess: () => {
|
|
135
130
|
// 관련 쿼리 무효화로 데이터 동기화
|
|
136
131
|
queryClient.invalidateQueries({ queryKey: ['posts'] })
|
|
@@ -181,6 +176,92 @@ function BadExample() {
|
|
|
181
176
|
}
|
|
182
177
|
```
|
|
183
178
|
|
|
179
|
+
## ⚠️ 함수 분리 시 유의사항
|
|
180
|
+
|
|
181
|
+
Server Function 내부 로직을 별도 함수로 분리할 때 반드시 아래 규칙을 따르세요.
|
|
182
|
+
|
|
183
|
+
### 규칙
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
1. 분리한 함수는 createServerFn 내부에서만 호출
|
|
187
|
+
2. 분리한 함수는 createServerFn으로 감싸지 않음
|
|
188
|
+
3. 분리한 함수는 index.ts에서 export 금지 (프론트엔드 import 방지)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### ✅ 올바른 패턴
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
// services/user/mutations.ts
|
|
195
|
+
|
|
196
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
197
|
+
// 내부 헬퍼 함수 (export 금지!)
|
|
198
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
199
|
+
const validateUserData = async (email: string) => {
|
|
200
|
+
const existing = await prisma.user.findUnique({ where: { email } })
|
|
201
|
+
if (existing) throw new Error('Email already exists')
|
|
202
|
+
return true
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const sendWelcomeEmail = async (userId: string, email: string) => {
|
|
206
|
+
await emailService.send({
|
|
207
|
+
to: email,
|
|
208
|
+
template: 'welcome',
|
|
209
|
+
data: { userId },
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
214
|
+
// Server Function (export 가능)
|
|
215
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
216
|
+
export const createUser = createServerFn({ method: 'POST' })
|
|
217
|
+
.inputValidator(createUserSchema)
|
|
218
|
+
.handler(async ({ data }) => {
|
|
219
|
+
// 내부 헬퍼 함수 호출
|
|
220
|
+
await validateUserData(data.email)
|
|
221
|
+
|
|
222
|
+
const user = await prisma.user.create({ data })
|
|
223
|
+
|
|
224
|
+
// 내부 헬퍼 함수 호출
|
|
225
|
+
await sendWelcomeEmail(user.id, user.email)
|
|
226
|
+
|
|
227
|
+
return user
|
|
228
|
+
})
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
// services/user/index.ts
|
|
233
|
+
|
|
234
|
+
// ✅ Server Function만 export
|
|
235
|
+
export { createUser, updateUser, deleteUser } from './mutations'
|
|
236
|
+
export { getUsers, getUserById } from './queries'
|
|
237
|
+
|
|
238
|
+
// ❌ 내부 헬퍼 함수는 export 금지!
|
|
239
|
+
// export { validateUserData, sendWelcomeEmail } from './mutations'
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### ❌ 잘못된 패턴
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
// ❌ 분리한 함수를 createServerFn으로 감싸지 마세요
|
|
246
|
+
export const validateUserData = createServerFn({ method: 'POST' })
|
|
247
|
+
.handler(async ({ data }) => {
|
|
248
|
+
// ...
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
// ❌ 내부 헬퍼 함수를 export 하지 마세요
|
|
252
|
+
export const sendWelcomeEmail = async (userId: string) => {
|
|
253
|
+
// 프론트엔드에서 import 가능해져 보안 위험!
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### 이유
|
|
258
|
+
|
|
259
|
+
- **보안**: 내부 로직이 프론트엔드 번들에 포함되지 않음
|
|
260
|
+
- **명확한 API**: Server Function만 외부에 노출되어 API 경계가 명확함
|
|
261
|
+
- **트리 쉐이킹**: export하지 않은 함수는 번들에서 제거됨
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
184
265
|
## 보안 패턴
|
|
185
266
|
|
|
186
267
|
### 서버 전용 데이터 보호
|