@kood/claude-code 0.1.6 → 0.1.9
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 +109 -216
- package/package.json +8 -2
- package/templates/hono/CLAUDE.md +59 -328
- package/templates/hono/docs/architecture/architecture.md +93 -747
- package/templates/hono/docs/deployment/cloudflare.md +59 -513
- package/templates/hono/docs/deployment/docker.md +41 -356
- package/templates/hono/docs/deployment/index.md +54 -190
- package/templates/hono/docs/deployment/railway.md +36 -306
- package/templates/hono/docs/deployment/vercel.md +49 -434
- package/templates/hono/docs/library/ai-sdk/index.md +53 -290
- package/templates/hono/docs/library/ai-sdk/openrouter.md +19 -387
- package/templates/hono/docs/library/ai-sdk/providers.md +28 -394
- package/templates/hono/docs/library/ai-sdk/streaming.md +52 -353
- package/templates/hono/docs/library/ai-sdk/structured-output.md +63 -395
- package/templates/hono/docs/library/ai-sdk/tools.md +62 -431
- package/templates/hono/docs/library/hono/env-setup.md +24 -313
- package/templates/hono/docs/library/hono/error-handling.md +34 -295
- package/templates/hono/docs/library/hono/index.md +29 -121
- package/templates/hono/docs/library/hono/middleware.md +21 -188
- package/templates/hono/docs/library/hono/rpc.md +40 -341
- package/templates/hono/docs/library/hono/validation.md +35 -195
- package/templates/hono/docs/library/pino/index.md +42 -333
- package/templates/hono/docs/library/prisma/cloudflare-d1.md +64 -367
- package/templates/hono/docs/library/prisma/config.md +19 -260
- package/templates/hono/docs/library/prisma/index.md +67 -320
- package/templates/hono/docs/library/zod/index.md +53 -257
- package/templates/npx/CLAUDE.md +62 -274
- package/templates/npx/docs/references/patterns.md +160 -0
- package/templates/tanstack-start/CLAUDE.md +100 -256
- package/templates/tanstack-start/docs/architecture/architecture.md +44 -589
- package/templates/tanstack-start/docs/deployment/cloudflare.md +37 -424
- package/templates/tanstack-start/docs/deployment/index.md +57 -286
- package/templates/tanstack-start/docs/deployment/nitro.md +36 -318
- package/templates/tanstack-start/docs/deployment/railway.md +40 -409
- package/templates/tanstack-start/docs/deployment/vercel.md +43 -465
- package/templates/tanstack-start/docs/design/components.md +77 -311
- package/templates/tanstack-start/docs/design/index.md +113 -69
- package/templates/tanstack-start/docs/design/safe-area.md +51 -250
- package/templates/tanstack-start/docs/design/tailwind-setup.md +45 -359
- package/templates/tanstack-start/docs/guides/conventions.md +103 -0
- package/templates/tanstack-start/docs/guides/env-setup.md +34 -340
- package/templates/tanstack-start/docs/guides/getting-started.md +22 -209
- package/templates/tanstack-start/docs/guides/hooks.md +166 -0
- package/templates/tanstack-start/docs/guides/routes.md +166 -0
- package/templates/tanstack-start/docs/guides/services.md +143 -0
- package/templates/tanstack-start/docs/library/better-auth/2fa.md +27 -115
- package/templates/tanstack-start/docs/library/better-auth/advanced.md +22 -105
- package/templates/tanstack-start/docs/library/better-auth/index.md +17 -66
- package/templates/tanstack-start/docs/library/better-auth/plugins.md +11 -88
- package/templates/tanstack-start/docs/library/better-auth/session.md +12 -92
- package/templates/tanstack-start/docs/library/better-auth/setup.md +9 -91
- package/templates/tanstack-start/docs/library/prisma/cloudflare-d1.md +30 -358
- package/templates/tanstack-start/docs/library/prisma/config.md +27 -327
- package/templates/tanstack-start/docs/library/prisma/crud.md +46 -174
- package/templates/tanstack-start/docs/library/prisma/index.md +23 -113
- package/templates/tanstack-start/docs/library/prisma/relations.md +31 -153
- package/templates/tanstack-start/docs/library/prisma/schema.md +40 -217
- package/templates/tanstack-start/docs/library/prisma/setup.md +12 -112
- package/templates/tanstack-start/docs/library/prisma/transactions.md +20 -110
- package/templates/tanstack-start/docs/library/tanstack-query/index.md +26 -97
- package/templates/tanstack-start/docs/library/tanstack-query/invalidation.md +28 -107
- package/templates/tanstack-start/docs/library/tanstack-query/optimistic-updates.md +44 -146
- package/templates/tanstack-start/docs/library/tanstack-query/use-mutation.md +33 -127
- package/templates/tanstack-start/docs/library/tanstack-query/use-query.md +49 -149
- package/templates/tanstack-start/docs/library/tanstack-start/auth-patterns.md +19 -112
- package/templates/tanstack-start/docs/library/tanstack-start/index.md +33 -80
- package/templates/tanstack-start/docs/library/tanstack-start/middleware.md +28 -106
- package/templates/tanstack-start/docs/library/tanstack-start/routing.md +21 -118
- package/templates/tanstack-start/docs/library/tanstack-start/server-functions.md +34 -246
- package/templates/tanstack-start/docs/library/tanstack-start/setup.md +6 -39
- package/templates/tanstack-start/docs/library/zod/complex-types.md +32 -156
- package/templates/tanstack-start/docs/library/zod/index.md +31 -144
- package/templates/tanstack-start/docs/library/zod/transforms.md +20 -129
- package/templates/tanstack-start/docs/library/zod/validation.md +39 -155
- package/templates/hono/docs/commands/git.md +0 -145
- package/templates/hono/docs/mcp/context7.md +0 -106
- package/templates/hono/docs/mcp/index.md +0 -176
- package/templates/hono/docs/mcp/sequential-thinking.md +0 -101
- package/templates/hono/docs/mcp/serena.md +0 -269
- package/templates/hono/docs/mcp/sgrep.md +0 -105
- package/templates/hono/docs/skills/gemini-review/SKILL.md +0 -220
- package/templates/hono/docs/skills/gemini-review/references/checklists.md +0 -136
- package/templates/hono/docs/skills/gemini-review/references/prompt-templates.md +0 -303
- package/templates/npx/docs/commands/git.md +0 -145
- package/templates/npx/docs/mcp/index.md +0 -60
- package/templates/npx/docs/skills/gemini-review/SKILL.md +0 -220
- package/templates/npx/docs/skills/gemini-review/references/checklists.md +0 -134
- package/templates/npx/docs/skills/gemini-review/references/prompt-templates.md +0 -301
- package/templates/tanstack-start/docs/commands/git.md +0 -145
- package/templates/tanstack-start/docs/design/accessibility.md +0 -433
- package/templates/tanstack-start/docs/design/color.md +0 -235
- package/templates/tanstack-start/docs/design/spacing.md +0 -341
- package/templates/tanstack-start/docs/design/typography.md +0 -324
- package/templates/tanstack-start/docs/guides/best-practices.md +0 -950
- package/templates/tanstack-start/docs/guides/husky-lint-staged.md +0 -303
- package/templates/tanstack-start/docs/guides/prettier.md +0 -189
- package/templates/tanstack-start/docs/guides/project-templates.md +0 -710
- package/templates/tanstack-start/docs/library/tanstack-query/setup.md +0 -107
- package/templates/tanstack-start/docs/library/zod/basic-types.md +0 -186
- package/templates/tanstack-start/docs/mcp/context7.md +0 -204
- package/templates/tanstack-start/docs/mcp/index.md +0 -177
- package/templates/tanstack-start/docs/mcp/sequential-thinking.md +0 -180
- package/templates/tanstack-start/docs/mcp/serena.md +0 -269
- package/templates/tanstack-start/docs/mcp/sgrep.md +0 -174
- package/templates/tanstack-start/docs/skills/gemini-review/SKILL.md +0 -220
- package/templates/tanstack-start/docs/skills/gemini-review/references/checklists.md +0 -144
- package/templates/tanstack-start/docs/skills/gemini-review/references/prompt-templates.md +0 -292
|
@@ -1,287 +1,75 @@
|
|
|
1
1
|
# TanStack Start - Server Functions
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
Server Functions는 서버에서만 실행되는 타입 안전한 함수입니다.
|
|
3
|
+
서버에서만 실행되는 타입 안전한 함수.
|
|
6
4
|
|
|
7
5
|
## ⚠️ 필수: TanStack Query 사용
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
```
|
|
12
|
-
❌ 금지: Server Function 직접 호출
|
|
13
|
-
✅ 필수: useQuery/useMutation과 함께 사용
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
**이유**:
|
|
17
|
-
- 자동 캐싱 및 중복 요청 제거
|
|
18
|
-
- 로딩/에러 상태 관리
|
|
19
|
-
- 자동 재시도 및 백그라운드 갱신
|
|
20
|
-
- invalidateQueries로 일관된 데이터 동기화
|
|
7
|
+
클라이언트 호출 시 반드시 useQuery/useMutation 사용.
|
|
8
|
+
- 자동 캐싱, 중복 요청 제거, 로딩/에러 상태 관리, invalidateQueries 동기화
|
|
21
9
|
|
|
22
|
-
## 기본
|
|
10
|
+
## 기본 패턴
|
|
23
11
|
|
|
24
12
|
```typescript
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
// GET 요청 (데이터 조회)
|
|
13
|
+
// GET
|
|
28
14
|
export const getUsers = createServerFn({ method: 'GET' })
|
|
29
|
-
.handler(async () =>
|
|
30
|
-
return prisma.user.findMany()
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
// POST 요청 (데이터 생성/수정)
|
|
34
|
-
export const createUser = createServerFn({ method: 'POST' })
|
|
35
|
-
.handler(async () => {
|
|
36
|
-
return { success: true }
|
|
37
|
-
})
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
## Input Validation
|
|
41
|
-
|
|
42
|
-
### 기본 Validator
|
|
43
|
-
|
|
44
|
-
```typescript
|
|
45
|
-
import { createServerFn } from '@tanstack/react-start'
|
|
46
|
-
|
|
47
|
-
export const createUser = createServerFn({ method: 'POST' })
|
|
48
|
-
.inputValidator((data: { email: string; name: string }) => data)
|
|
49
|
-
.handler(async ({ data }) => {
|
|
50
|
-
// data는 타입 안전함
|
|
51
|
-
return prisma.user.create({ data })
|
|
52
|
-
})
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
### Zod Validation 사용
|
|
56
|
-
|
|
57
|
-
```typescript
|
|
58
|
-
import { createServerFn } from '@tanstack/react-start'
|
|
59
|
-
import { zodValidator } from '@tanstack/react-start/validators'
|
|
60
|
-
import { z } from 'zod'
|
|
15
|
+
.handler(async () => prisma.user.findMany())
|
|
61
16
|
|
|
17
|
+
// POST + Zod Validation
|
|
62
18
|
const createUserSchema = z.object({
|
|
63
|
-
email: z.
|
|
19
|
+
email: z.email(),
|
|
64
20
|
name: z.string().min(1).max(100),
|
|
65
|
-
bio: z.string().max(500).optional(),
|
|
66
21
|
})
|
|
67
22
|
|
|
68
23
|
export const createUser = createServerFn({ method: 'POST' })
|
|
69
24
|
.inputValidator(zodValidator(createUserSchema))
|
|
70
|
-
.handler(async ({ data }) => {
|
|
71
|
-
return prisma.user.create({ data })
|
|
72
|
-
})
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
## FormData 처리
|
|
76
|
-
|
|
77
|
-
```typescript
|
|
78
|
-
import { createServerFn } from '@tanstack/react-start'
|
|
79
|
-
|
|
80
|
-
export const submitForm = createServerFn({ method: 'POST' })
|
|
81
|
-
.inputValidator((formData: FormData) => {
|
|
82
|
-
const name = formData.get('name') as string
|
|
83
|
-
const email = formData.get('email') as string
|
|
84
|
-
return { name, email }
|
|
85
|
-
})
|
|
86
|
-
.handler(async ({ data }) => {
|
|
87
|
-
return prisma.user.create({ data })
|
|
88
|
-
})
|
|
25
|
+
.handler(async ({ data }) => prisma.user.create({ data }))
|
|
89
26
|
```
|
|
90
27
|
|
|
91
|
-
## 컴포넌트에서 호출
|
|
92
|
-
|
|
93
|
-
### ✅ 올바른 패턴: useQuery 사용 (데이터 조회)
|
|
28
|
+
## 컴포넌트에서 호출
|
|
94
29
|
|
|
95
30
|
```tsx
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
queryKey: ['posts'],
|
|
102
|
-
queryFn: () => getServerPosts(),
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
if (isLoading) return <div>Loading...</div>
|
|
106
|
-
if (error) return <div>Error: {error.message}</div>
|
|
107
|
-
|
|
108
|
-
return (
|
|
109
|
-
<ul>
|
|
110
|
-
{data?.map((post) => (
|
|
111
|
-
<li key={post.id}>{post.title}</li>
|
|
112
|
-
))}
|
|
113
|
-
</ul>
|
|
114
|
-
)
|
|
115
|
-
}
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
### ✅ 올바른 패턴: useMutation 사용 (데이터 변경)
|
|
119
|
-
|
|
120
|
-
```tsx
|
|
121
|
-
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
122
|
-
import { createPost, deletePost } from '@/lib/server-functions'
|
|
123
|
-
|
|
124
|
-
function PostForm() {
|
|
125
|
-
const queryClient = useQueryClient()
|
|
126
|
-
|
|
127
|
-
const mutation = useMutation({
|
|
128
|
-
mutationFn: (data: { title: string; content: string }) => createPost({ data }),
|
|
129
|
-
onSuccess: () => {
|
|
130
|
-
// 관련 쿼리 무효화로 데이터 동기화
|
|
131
|
-
queryClient.invalidateQueries({ queryKey: ['posts'] })
|
|
132
|
-
},
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
|
136
|
-
e.preventDefault()
|
|
137
|
-
const formData = new FormData(e.currentTarget)
|
|
138
|
-
mutation.mutate({
|
|
139
|
-
title: formData.get('title') as string,
|
|
140
|
-
content: formData.get('content') as string,
|
|
141
|
-
})
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
return (
|
|
145
|
-
<form onSubmit={handleSubmit}>
|
|
146
|
-
<input name="title" required />
|
|
147
|
-
<textarea name="content" required />
|
|
148
|
-
<button type="submit" disabled={mutation.isPending}>
|
|
149
|
-
{mutation.isPending ? '저장 중...' : '저장'}
|
|
150
|
-
</button>
|
|
151
|
-
</form>
|
|
152
|
-
)
|
|
153
|
-
}
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
### ❌ 금지: Server Function 직접 호출
|
|
157
|
-
|
|
158
|
-
```tsx
|
|
159
|
-
// ❌ 이렇게 하지 마세요!
|
|
160
|
-
function BadExample() {
|
|
161
|
-
const [posts, setPosts] = useState([])
|
|
162
|
-
const [loading, setLoading] = useState(false)
|
|
163
|
-
|
|
164
|
-
useEffect(() => {
|
|
165
|
-
setLoading(true)
|
|
166
|
-
getPosts()
|
|
167
|
-
.then(setPosts)
|
|
168
|
-
.finally(() => setLoading(false))
|
|
169
|
-
}, [])
|
|
170
|
-
|
|
171
|
-
// 문제점:
|
|
172
|
-
// - 중복 요청 발생 가능
|
|
173
|
-
// - 캐싱 없음
|
|
174
|
-
// - 에러 처리 수동
|
|
175
|
-
// - 다른 컴포넌트와 데이터 동기화 안됨
|
|
176
|
-
}
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
## ⚠️ 함수 분리 시 유의사항
|
|
180
|
-
|
|
181
|
-
Server Function 내부 로직을 별도 함수로 분리할 때 반드시 아래 규칙을 따르세요.
|
|
31
|
+
// ✅ useQuery (조회)
|
|
32
|
+
const { data, isLoading } = useQuery({
|
|
33
|
+
queryKey: ['posts'],
|
|
34
|
+
queryFn: () => getServerPosts(),
|
|
35
|
+
})
|
|
182
36
|
|
|
183
|
-
|
|
37
|
+
// ✅ useMutation (변경)
|
|
38
|
+
const mutation = useMutation({
|
|
39
|
+
mutationFn: createPost,
|
|
40
|
+
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['posts'] }),
|
|
41
|
+
})
|
|
184
42
|
|
|
185
|
-
|
|
186
|
-
1. 분리한 함수는 createServerFn 내부에서만 호출
|
|
187
|
-
2. 분리한 함수는 createServerFn으로 감싸지 않음
|
|
188
|
-
3. 분리한 함수는 index.ts에서 export 금지 (프론트엔드 import 방지)
|
|
43
|
+
// ❌ 직접 호출 금지 (캐싱 없음, 동기화 안됨)
|
|
189
44
|
```
|
|
190
45
|
|
|
191
|
-
|
|
46
|
+
## 함수 분리 규칙
|
|
192
47
|
|
|
193
48
|
```typescript
|
|
194
|
-
//
|
|
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
|
-
}
|
|
49
|
+
// 내부 헬퍼 (export 금지!)
|
|
50
|
+
const validateUserData = async (email: string) => { ... }
|
|
204
51
|
|
|
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
52
|
// Server Function (export 가능)
|
|
215
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
216
53
|
export const createUser = createServerFn({ method: 'POST' })
|
|
217
54
|
.inputValidator(createUserSchema)
|
|
218
55
|
.handler(async ({ data }) => {
|
|
219
|
-
// 내부 헬퍼 함수 호출
|
|
220
56
|
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
|
-
// ...
|
|
57
|
+
return prisma.user.create({ data })
|
|
249
58
|
})
|
|
250
59
|
|
|
251
|
-
//
|
|
252
|
-
export
|
|
253
|
-
|
|
254
|
-
}
|
|
60
|
+
// index.ts: Server Function만 export
|
|
61
|
+
export { createUser } from './mutations'
|
|
62
|
+
// ❌ export { validateUserData } 금지
|
|
255
63
|
```
|
|
256
64
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
- **보안**: 내부 로직이 프론트엔드 번들에 포함되지 않음
|
|
260
|
-
- **명확한 API**: Server Function만 외부에 노출되어 API 경계가 명확함
|
|
261
|
-
- **트리 쉐이킹**: export하지 않은 함수는 번들에서 제거됨
|
|
262
|
-
|
|
263
|
-
---
|
|
264
|
-
|
|
265
|
-
## 보안 패턴
|
|
266
|
-
|
|
267
|
-
### 서버 전용 데이터 보호
|
|
65
|
+
## 보안
|
|
268
66
|
|
|
269
67
|
```tsx
|
|
270
|
-
// ❌
|
|
271
|
-
|
|
272
|
-
loader: () => {
|
|
273
|
-
const secret = process.env.SECRET // 클라이언트에 노출!
|
|
274
|
-
return fetch(`/api/users?key=${secret}`)
|
|
275
|
-
},
|
|
276
|
-
})
|
|
277
|
-
|
|
278
|
-
// ✅ 올바른 방법 - 서버 함수 사용
|
|
279
|
-
const getUsersSecurely = createServerFn().handler(() => {
|
|
280
|
-
const secret = process.env.SECRET // 서버에서만 접근
|
|
281
|
-
return fetch(`/api/users?key=${secret}`)
|
|
282
|
-
})
|
|
68
|
+
// ❌ loader에서 환경변수 직접 사용 (노출됨)
|
|
69
|
+
loader: () => { const secret = process.env.SECRET }
|
|
283
70
|
|
|
284
|
-
|
|
285
|
-
|
|
71
|
+
// ✅ Server Function 사용
|
|
72
|
+
const fn = createServerFn().handler(() => {
|
|
73
|
+
const secret = process.env.SECRET // 서버에서만
|
|
286
74
|
})
|
|
287
75
|
```
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
# TanStack Start - 설치 및 설정
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
## 패키지 설치
|
|
3
|
+
## 설치
|
|
6
4
|
|
|
7
5
|
```bash
|
|
8
6
|
yarn add @tanstack/react-start @tanstack/react-router vinxi
|
|
9
7
|
yarn add -D vite @vitejs/plugin-react vite-tsconfig-paths
|
|
10
8
|
```
|
|
11
9
|
|
|
12
|
-
##
|
|
10
|
+
## 설정
|
|
13
11
|
|
|
14
12
|
```typescript
|
|
15
13
|
// vite.config.ts
|
|
@@ -19,19 +17,11 @@ import { tanstackStart } from '@tanstack/react-start/plugin/vite'
|
|
|
19
17
|
import viteReact from '@vitejs/plugin-react'
|
|
20
18
|
|
|
21
19
|
export default defineConfig({
|
|
22
|
-
server: {
|
|
23
|
-
|
|
24
|
-
},
|
|
25
|
-
plugins: [
|
|
26
|
-
tsConfigPaths(),
|
|
27
|
-
tanstackStart(),
|
|
28
|
-
viteReact(),
|
|
29
|
-
],
|
|
20
|
+
server: { port: 3000 },
|
|
21
|
+
plugins: [tsConfigPaths(), tanstackStart(), viteReact()],
|
|
30
22
|
})
|
|
31
23
|
```
|
|
32
24
|
|
|
33
|
-
## TypeScript 설정
|
|
34
|
-
|
|
35
25
|
```json
|
|
36
26
|
// tsconfig.json
|
|
37
27
|
{
|
|
@@ -40,35 +30,12 @@ export default defineConfig({
|
|
|
40
30
|
"module": "ESNext",
|
|
41
31
|
"moduleResolution": "bundler",
|
|
42
32
|
"strict": true,
|
|
43
|
-
"esModuleInterop": true,
|
|
44
|
-
"skipLibCheck": true,
|
|
45
33
|
"jsx": "react-jsx",
|
|
46
|
-
"
|
|
47
|
-
|
|
48
|
-
"@/*": ["./src/*"]
|
|
49
|
-
}
|
|
50
|
-
},
|
|
51
|
-
"include": ["src/**/*"]
|
|
34
|
+
"paths": { "@/*": ["./src/*"] }
|
|
35
|
+
}
|
|
52
36
|
}
|
|
53
37
|
```
|
|
54
38
|
|
|
55
|
-
## 프로젝트 구조
|
|
56
|
-
|
|
57
|
-
```
|
|
58
|
-
project/
|
|
59
|
-
├── src/
|
|
60
|
-
│ ├── routes/
|
|
61
|
-
│ │ ├── __root.tsx
|
|
62
|
-
│ │ ├── index.tsx
|
|
63
|
-
│ │ └── about.tsx
|
|
64
|
-
│ ├── lib/
|
|
65
|
-
│ │ └── server-functions.ts
|
|
66
|
-
│ └── start.ts
|
|
67
|
-
├── vite.config.ts
|
|
68
|
-
├── tsconfig.json
|
|
69
|
-
└── package.json
|
|
70
|
-
```
|
|
71
|
-
|
|
72
39
|
## 환경 변수 검증
|
|
73
40
|
|
|
74
41
|
```typescript
|
|
@@ -1,204 +1,80 @@
|
|
|
1
1
|
# Zod - 복합 타입
|
|
2
2
|
|
|
3
|
-
> **상위 문서**: [Zod](./index.md)
|
|
4
|
-
|
|
5
3
|
## 객체
|
|
6
4
|
|
|
7
5
|
```typescript
|
|
8
6
|
const UserSchema = z.object({
|
|
9
7
|
name: z.string(),
|
|
10
|
-
email: z.email(),
|
|
11
|
-
age: z.number().optional(),
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
// 검증
|
|
15
|
-
const result = UserSchema.parse({
|
|
16
|
-
name: 'john',
|
|
17
|
-
email: 'john@example.com',
|
|
18
|
-
age: 25,
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
// 안전한 검증
|
|
22
|
-
const safeResult = UserSchema.safeParse(data)
|
|
23
|
-
if (safeResult.success) {
|
|
24
|
-
console.log(safeResult.data)
|
|
25
|
-
} else {
|
|
26
|
-
console.log(safeResult.error)
|
|
27
|
-
}
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
### 객체 메서드
|
|
31
|
-
|
|
32
|
-
```typescript
|
|
33
|
-
// 부분 객체 (모든 필드가 optional)
|
|
34
|
-
const PartialUser = UserSchema.partial()
|
|
35
|
-
|
|
36
|
-
// 필수 객체 (모든 필드가 required)
|
|
37
|
-
const RequiredUser = UserSchema.required()
|
|
38
|
-
|
|
39
|
-
// 필드 선택
|
|
40
|
-
const UserName = UserSchema.pick({ name: true })
|
|
41
|
-
|
|
42
|
-
// 필드 제외
|
|
43
|
-
const UserWithoutEmail = UserSchema.omit({ email: true })
|
|
44
|
-
|
|
45
|
-
// 확장
|
|
46
|
-
const ExtendedUser = UserSchema.extend({
|
|
47
|
-
role: z.enum(['admin', 'user']),
|
|
8
|
+
email: z.email(),
|
|
9
|
+
age: z.number().optional(),
|
|
48
10
|
})
|
|
49
11
|
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
12
|
+
// 메서드
|
|
13
|
+
UserSchema.partial() // 모든 필드 optional
|
|
14
|
+
UserSchema.required() // 모든 필드 required
|
|
15
|
+
UserSchema.pick({ name: true }) // 특정 필드만
|
|
16
|
+
UserSchema.omit({ email: true }) // 특정 필드 제외
|
|
17
|
+
UserSchema.extend({ role: z.enum(['admin', 'user']) })
|
|
18
|
+
UserSchema.merge(AnotherSchema)
|
|
55
19
|
|
|
56
|
-
|
|
57
|
-
// v3
|
|
58
|
-
z.object({ name: z.string() }).strict() // 추가 키 에러
|
|
59
|
-
z.object({ name: z.string() }).passthrough() // 추가 키 통과
|
|
60
|
-
|
|
61
|
-
// v4 - 새로운 API
|
|
20
|
+
// v4 Strict/Loose
|
|
62
21
|
z.strictObject({ name: z.string() }) // 추가 키 에러
|
|
63
22
|
z.looseObject({ name: z.string() }) // 추가 키 통과
|
|
64
23
|
```
|
|
65
24
|
|
|
66
|
-
##
|
|
25
|
+
## 배열/튜플
|
|
67
26
|
|
|
68
27
|
```typescript
|
|
69
28
|
z.array(z.string())
|
|
70
|
-
z.array(z.number()).min(1)
|
|
71
|
-
z.array(z.number()).max(10) // 최대 10개
|
|
72
|
-
z.array(z.number()).length(5) // 정확히 5개
|
|
73
|
-
z.array(z.number()).nonempty() // 비어있지 않음
|
|
74
|
-
```
|
|
29
|
+
z.array(z.number()).min(1).max(10).length(5).nonempty()
|
|
75
30
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
```typescript
|
|
79
|
-
const tuple = z.tuple([
|
|
80
|
-
z.string(), // 첫 번째 요소
|
|
81
|
-
z.number(), // 두 번째 요소
|
|
82
|
-
])
|
|
83
|
-
|
|
84
|
-
type Tuple = z.infer<typeof tuple>
|
|
85
|
-
// [string, number]
|
|
31
|
+
z.tuple([z.string(), z.number()]) // [string, number]
|
|
86
32
|
```
|
|
87
33
|
|
|
88
34
|
## 유니온
|
|
89
35
|
|
|
90
36
|
```typescript
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const StringOrNumber = z.string().or(z.number())
|
|
94
|
-
|
|
95
|
-
type StringOrNumber = z.infer<typeof StringOrNumber>
|
|
96
|
-
// string | number
|
|
97
|
-
```
|
|
37
|
+
z.union([z.string(), z.number()])
|
|
38
|
+
z.string().or(z.number())
|
|
98
39
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
```typescript
|
|
102
|
-
const Shape = z.discriminatedUnion('type', [
|
|
40
|
+
// Discriminated Union
|
|
41
|
+
z.discriminatedUnion('type', [
|
|
103
42
|
z.object({ type: z.literal('circle'), radius: z.number() }),
|
|
104
43
|
z.object({ type: z.literal('rectangle'), width: z.number(), height: z.number() }),
|
|
105
44
|
])
|
|
106
|
-
|
|
107
|
-
type Shape = z.infer<typeof Shape>
|
|
108
|
-
// { type: 'circle'; radius: number } | { type: 'rectangle'; width: number; height: number }
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
### v4 향상된 Discriminated Union
|
|
112
|
-
|
|
113
|
-
```typescript
|
|
114
|
-
// 유니온 및 파이프 discriminator 지원
|
|
115
|
-
const MyResult = z.discriminatedUnion("status", [
|
|
116
|
-
// 단순 리터럴
|
|
117
|
-
z.object({ status: z.literal("aaa"), data: z.string() }),
|
|
118
|
-
// 유니온 discriminator
|
|
119
|
-
z.object({ status: z.union([z.literal("bbb"), z.literal("ccc")]) }),
|
|
120
|
-
// 파이프 discriminator
|
|
121
|
-
z.object({ status: z.literal("fail").transform(val => val.toUpperCase()) }),
|
|
122
|
-
])
|
|
123
|
-
|
|
124
|
-
// 중첩 discriminated union
|
|
125
|
-
const BaseError = z.object({ status: z.literal("failed"), message: z.string() })
|
|
126
|
-
|
|
127
|
-
const MyResult2 = z.discriminatedUnion("status", [
|
|
128
|
-
z.object({ status: z.literal("success"), data: z.string() }),
|
|
129
|
-
z.discriminatedUnion("code", [
|
|
130
|
-
BaseError.extend({ code: z.literal(400) }),
|
|
131
|
-
BaseError.extend({ code: z.literal(401) }),
|
|
132
|
-
BaseError.extend({ code: z.literal(500) })
|
|
133
|
-
])
|
|
134
|
-
])
|
|
135
45
|
```
|
|
136
46
|
|
|
137
47
|
## Enum
|
|
138
48
|
|
|
139
49
|
```typescript
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
FishEnum.parse('Salmon') // => "Salmon"
|
|
144
|
-
FishEnum.parse('Swordfish') // => ❌ throws
|
|
145
|
-
|
|
146
|
-
type Fish = z.infer<typeof FishEnum>
|
|
147
|
-
// 'Salmon' | 'Tuna' | 'Trout'
|
|
50
|
+
const Status = z.enum(['pending', 'done', 'cancelled'])
|
|
51
|
+
type Status = z.infer<typeof Status> // 'pending' | 'done' | 'cancelled'
|
|
148
52
|
|
|
149
53
|
// Native enum
|
|
150
|
-
enum Fruits {
|
|
151
|
-
|
|
152
|
-
Banana,
|
|
153
|
-
}
|
|
154
|
-
const FruitSchema = z.nativeEnum(Fruits)
|
|
54
|
+
enum Fruits { Apple, Banana }
|
|
55
|
+
z.nativeEnum(Fruits)
|
|
155
56
|
```
|
|
156
57
|
|
|
157
|
-
## Record
|
|
58
|
+
## Record/Map/Set
|
|
158
59
|
|
|
159
60
|
```typescript
|
|
160
|
-
|
|
161
|
-
name: z.string(),
|
|
162
|
-
}))
|
|
163
|
-
|
|
164
|
-
type UserStore = z.infer<typeof UserStore>
|
|
61
|
+
z.record(z.string(), z.object({ name: z.string() }))
|
|
165
62
|
// { [key: string]: { name: string } }
|
|
166
63
|
|
|
167
|
-
//
|
|
168
|
-
|
|
169
|
-
userStore['77d2586b-9e8e-4ecf-8b21-ea7e0530eadd'] = {
|
|
170
|
-
name: 'Carlotta',
|
|
171
|
-
} // passes
|
|
172
|
-
|
|
173
|
-
userStore['77d2586b-9e8e-4ecf-8b21-ea7e0530eadd'] = {
|
|
174
|
-
whatever: 'Ice cream sundae',
|
|
175
|
-
} // TypeError
|
|
64
|
+
z.map(z.string(), z.number()) // Map<string, number>
|
|
65
|
+
z.set(z.number()) // Set<number>
|
|
176
66
|
```
|
|
177
67
|
|
|
178
|
-
##
|
|
68
|
+
## 재귀 스키마
|
|
179
69
|
|
|
180
70
|
```typescript
|
|
181
|
-
|
|
182
|
-
const stringNumberMap = z.map(z.string(), z.number())
|
|
183
|
-
type StringNumberMap = z.infer<typeof stringNumberMap>
|
|
184
|
-
// Map<string, number>
|
|
185
|
-
|
|
186
|
-
// Set
|
|
187
|
-
const numberSet = z.set(z.number())
|
|
188
|
-
type NumberSet = z.infer<typeof numberSet>
|
|
189
|
-
// Set<number>
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
## 재귀 스키마 (JSON)
|
|
193
|
-
|
|
194
|
-
```typescript
|
|
195
|
-
const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()])
|
|
196
|
-
type Literal = z.infer<typeof literalSchema>
|
|
197
|
-
type Json = Literal | { [key: string]: Json } | Json[]
|
|
71
|
+
type Json = string | number | boolean | null | { [key: string]: Json } | Json[]
|
|
198
72
|
|
|
199
73
|
const jsonSchema: z.ZodType<Json> = z.lazy(() =>
|
|
200
|
-
z.union([
|
|
74
|
+
z.union([
|
|
75
|
+
z.string(), z.number(), z.boolean(), z.null(),
|
|
76
|
+
z.array(jsonSchema),
|
|
77
|
+
z.record(jsonSchema)
|
|
78
|
+
])
|
|
201
79
|
)
|
|
202
|
-
|
|
203
|
-
jsonSchema.parse({ foo: [1, 2, { bar: 'baz' }] })
|
|
204
80
|
```
|