@kood/claude-code 0.3.8 → 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.
Files changed (37) hide show
  1. package/dist/index.js +1 -1
  2. package/package.json +1 -1
  3. package/templates/.claude/agents/code-reviewer.md +16 -1
  4. package/templates/.claude/agents/dependency-manager.md +16 -1
  5. package/templates/.claude/agents/deployment-validator.md +16 -1
  6. package/templates/.claude/agents/git-operator.md +16 -1
  7. package/templates/.claude/agents/implementation-executor.md +16 -1
  8. package/templates/.claude/agents/lint-fixer.md +16 -1
  9. package/templates/.claude/agents/refactor-advisor.md +16 -1
  10. package/templates/.claude/commands/agent-creator.md +16 -1
  11. package/templates/.claude/commands/bug-fix.md +16 -1
  12. package/templates/.claude/commands/command-creator.md +17 -1
  13. package/templates/.claude/commands/docs-creator.md +17 -1
  14. package/templates/.claude/commands/docs-refactor.md +17 -1
  15. package/templates/.claude/commands/execute.md +17 -1
  16. package/templates/.claude/commands/git-all.md +16 -1
  17. package/templates/.claude/commands/git-session.md +17 -1
  18. package/templates/.claude/commands/git.md +17 -1
  19. package/templates/.claude/commands/lint-fix.md +17 -1
  20. package/templates/.claude/commands/lint-init.md +17 -1
  21. package/templates/.claude/commands/plan.md +17 -1
  22. package/templates/.claude/commands/prd.md +17 -1
  23. package/templates/.claude/commands/pre-deploy.md +17 -1
  24. package/templates/.claude/commands/refactor.md +17 -1
  25. package/templates/.claude/commands/version-update.md +17 -1
  26. package/templates/hono/CLAUDE.md +1 -0
  27. package/templates/nextjs/CLAUDE.md +12 -9
  28. package/templates/nextjs/docs/architecture.md +812 -0
  29. package/templates/npx/CLAUDE.md +1 -0
  30. package/templates/tanstack-start/CLAUDE.md +1 -0
  31. package/templates/tanstack-start/docs/library/better-auth/index.md +225 -185
  32. package/templates/tanstack-start/docs/library/prisma/index.md +1025 -41
  33. package/templates/tanstack-start/docs/library/t3-env/index.md +207 -40
  34. package/templates/tanstack-start/docs/library/tanstack-query/index.md +878 -42
  35. package/templates/tanstack-start/docs/library/tanstack-router/index.md +602 -54
  36. package/templates/tanstack-start/docs/library/tanstack-start/index.md +1334 -33
  37. package/templates/tanstack-start/docs/library/zod/index.md +674 -31
@@ -2,55 +2,698 @@
2
2
 
3
3
  > v4 | TypeScript Schema Validation
4
4
 
5
- @complex-types.md
6
- @transforms.md
7
- @validation.md
5
+ <context>
6
+
7
+ **Purpose:** Type-safe schema validation with TypeScript inference
8
+ **Requirements:** TypeScript v5.5+, strict mode enabled
9
+ **Bundle Size:** 2kb core (gzipped), zero dependencies
10
+
11
+ </context>
12
+
13
+ ---
14
+
15
+ <forbidden>
16
+
17
+ | 분류 | 금지 |
18
+ |------|------|
19
+ | **Deprecated API** | `z.string().email()`, `z.string().url()`, `z.string().uuid()` |
20
+ | **Error Messages** | `message`, `invalid_type_error`, `required_error` (v3) |
21
+ | **타입** | any 타입 (unknown 사용) |
22
+
23
+ </forbidden>
24
+
25
+ ---
26
+
27
+ <required>
28
+
29
+ | 분류 | 필수 |
30
+ |------|------|
31
+ | **v4 API** | `z.email()`, `z.url()`, `z.uuid()` (top-level) |
32
+ | **Error Param** | `{ error: "message" }` (v4 통합 파라미터) |
33
+ | **TanStack Start** | `.inputValidator(schema)` 사용 |
34
+ | **타입 추론** | `z.infer<typeof schema>` |
35
+
36
+ </required>
37
+
38
+ ---
39
+
40
+ <installation>
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ npm install zod@^4.0.0
46
+ ```
47
+
48
+ ## TypeScript Config
49
+
50
+ ```json
51
+ {
52
+ "compilerOptions": {
53
+ "strict": true
54
+ }
55
+ }
56
+ ```
57
+
58
+ </installation>
8
59
 
9
60
  ---
10
61
 
11
- <quick_reference>
62
+ <v4_breaking_changes>
63
+
64
+ ## v3 → v4 Breaking Changes
65
+
66
+ | v3 (Deprecated) | v4 (Required) |
67
+ |-----------------|---------------|
68
+ | `z.string().email()` | `z.email()` |
69
+ | `z.string().url()` | `z.url()` |
70
+ | `z.string().uuid()` | `z.uuid()` |
71
+ | `z.string().min(5, { message: "..." })` | `z.string().min(5, { error: "..." })` |
72
+ | `z.string().email({ message: "..." })` | `z.email({ error: "..." })` |
73
+
74
+ ## Refinement Chaining
12
75
 
13
76
  ```typescript
14
- // Basic
15
- const schema = z.object({
16
- email: z.email(), // v4!
77
+ // ✅ v4: Refinement 체이닝 가능
78
+ z.string().refine(val => val.includes("@")).min(5)
79
+
80
+ // ❌ v3: ZodEffects로 래핑되어 체이닝 불가
81
+ z.string().refine(val => val.includes("@")).min(5) // Error
82
+ ```
83
+
84
+ ## Object Types
85
+
86
+ ```typescript
87
+ // ✅ v4: 새로운 객체 타입
88
+ z.strictObject({ name: z.string() }) // 추가 키 에러
89
+ z.looseObject({ name: z.string() }) // 추가 키 통과
90
+ z.object({ name: z.string() }) // 기본 동작 (strip)
91
+ ```
92
+
93
+ </v4_breaking_changes>
94
+
95
+ ---
96
+
97
+ <v4_new_apis>
98
+
99
+ ## String Formats
100
+
101
+ | API | 설명 |
102
+ |-----|------|
103
+ | `z.email()` | 이메일 검증 |
104
+ | `z.url()` | URL 검증 |
105
+ | `z.uuid()`, `z.uuidv1()`, `z.uuidv4()`, `z.uuidv7()` | UUID 검증 |
106
+ | `z.ipv4()`, `z.ipv6()` | IP 주소 |
107
+ | `z.cidr()`, `z.cidrvv4()`, `z.cidrv6()` | CIDR 표기법 |
108
+ | `z.base64()`, `z.base64url()` | Base64 인코딩 |
109
+ | `z.jwt()` | JWT 토큰 |
110
+
111
+ ## ISO Formats
112
+
113
+ | API | 설명 |
114
+ |-----|------|
115
+ | `z.iso.date()` | ISO 8601 날짜 (YYYY-MM-DD) |
116
+ | `z.iso.datetime()` | ISO 8601 날짜/시간 |
117
+ | `z.iso.time()` | ISO 8601 시간 |
118
+ | `z.iso.duration()` | ISO 8601 기간 |
119
+
120
+ ## Numeric Types
121
+
122
+ | API | 설명 |
123
+ |-----|------|
124
+ | `z.int()` | 정수 |
125
+ | `z.float32()`, `z.float64()` | 부동소수점 |
126
+ | `z.int32()`, `z.uint32()` | 32비트 정수 |
127
+ | `z.int64()`, `z.uint64()` | 64비트 정수 |
128
+
129
+ ## Special Types
130
+
131
+ | API | 설명 | 예시 |
132
+ |-----|------|------|
133
+ | `z.stringbool()` | 문자열 → 불리언 | `"true"`, `"1"`, `"yes"` → `true` |
134
+ | `z.templateLiteral()` | 템플릿 리터럴 타입 | `` `${number}px` `` |
135
+ | `z.file()` | 파일 검증 | `.min()`, `.max()`, `.mime()` |
136
+
137
+ ## Metadata & Tools
138
+
139
+ | API | 설명 |
140
+ |-----|------|
141
+ | `z.registry()` | 커스텀 메타데이터 |
142
+ | `z.globalRegistry` | JSON Schema 호환 메타데이터 |
143
+ | `.meta()` | 스키마 메타데이터 첨부 |
144
+ | `z.toJSONSchema()` | JSON Schema 변환 |
145
+ | `z.prettifyError()` | 사용자 친화적 에러 포맷 |
146
+ | `.overwrite()` | 타입 보존 변환 |
147
+
148
+ </v4_new_apis>
149
+
150
+ ---
151
+
152
+ <basic_usage>
153
+
154
+ ## Basic Schema
155
+
156
+ ```typescript
157
+ import { z } from 'zod'
158
+
159
+ // ✅ v4 API
160
+ const userSchema = z.object({
161
+ email: z.email(),
17
162
  name: z.string().min(1).trim(),
18
- website: z.url().optional(), // v4!
163
+ website: z.url().optional(),
19
164
  age: z.number().int().positive(),
20
165
  })
21
- type Input = z.infer<typeof schema>
22
166
 
23
- schema.parse(data) // Throw on failure
24
- schema.safeParse(data) // { success, data/error }
167
+ type User = z.infer<typeof userSchema>
168
+ // { email: string; name: string; website?: string; age: number }
169
+
170
+ // ✅ 검증
171
+ const result = userSchema.parse(data) // 실패 시 throw
172
+ const safe = userSchema.safeParse(data) // { success, data/error }
173
+
174
+ if (safe.success) {
175
+ console.log(safe.data.email)
176
+ } else {
177
+ console.error(safe.error.issues)
178
+ }
179
+ ```
180
+
181
+ ## TanStack Start Integration
182
+
183
+ ```typescript
184
+ import { createServerFn } from '@tanstack/start'
185
+ import { z } from 'zod'
186
+
187
+ // ✅ Server Function with Zod v4
188
+ const createUserSchema = z.object({
189
+ email: z.email(),
190
+ name: z.string().min(1).trim(),
191
+ website: z.url().optional(),
192
+ })
25
193
 
26
- // TanStack Start
27
194
  export const createUser = createServerFn({ method: 'POST' })
28
- .inputValidator(schema)
29
- .handler(async ({ data }) => prisma.user.create({ data }))
195
+ .inputValidator(createUserSchema)
196
+ .handler(async ({ data }) => {
197
+ // ^? { email: string; name: string; website?: string }
198
+ return prisma.user.create({ data })
199
+ })
30
200
  ```
31
201
 
32
- </quick_reference>
202
+ </basic_usage>
33
203
 
34
- <v4_changes>
204
+ ---
205
+
206
+ <primitives>
207
+
208
+ ## Primitive Types
209
+
210
+ | Type | Example |
211
+ |------|---------|
212
+ | `z.string()` | `z.string().min(5).max(100)` |
213
+ | `z.number()` | `z.number().int().positive()` |
214
+ | `z.boolean()` | `z.boolean()` |
215
+ | `z.date()` | `z.date().min(new Date('2020-01-01'))` |
216
+ | `z.null()` | `z.null()` |
217
+ | `z.undefined()` | `z.undefined()` |
218
+ | `z.void()` | `z.void()` |
219
+ | `z.any()` | `z.any()` (❌ 사용 지양) |
220
+ | `z.unknown()` | `z.unknown()` (✅ 권장) |
221
+ | `z.never()` | `z.never()` |
222
+
223
+ ## String Methods
224
+
225
+ ```typescript
226
+ z.string()
227
+ .min(5, { error: "Too short." })
228
+ .max(100, { error: "Too long." })
229
+ .length(10, { error: "Must be 10 chars." })
230
+ .trim()
231
+ .toLowerCase()
232
+ .toUpperCase()
233
+ .startsWith("https://")
234
+ .endsWith(".com")
235
+ .regex(/^\d+$/, { error: "Must be digits." })
236
+ ```
237
+
238
+ ## Number Methods
239
+
240
+ ```typescript
241
+ z.number()
242
+ .gt(5) // Greater than
243
+ .gte(5) // Greater than or equal
244
+ .lt(100) // Less than
245
+ .lte(100) // Less than or equal
246
+ .int() // Integer
247
+ .positive() // > 0
248
+ .nonnegative()// >= 0
249
+ .negative() // < 0
250
+ .nonpositive()// <= 0
251
+ .multipleOf(5)// 배수
252
+ .finite() // Not Infinity/-Infinity
253
+ .safe() // Number.MIN_SAFE_INTEGER ~ MAX_SAFE_INTEGER
254
+ ```
255
+
256
+ </primitives>
257
+
258
+ ---
259
+
260
+ <complex_types>
261
+
262
+ ## Objects
263
+
264
+ ```typescript
265
+ const schema = z.object({
266
+ name: z.string(),
267
+ age: z.number(),
268
+ })
269
+
270
+ // ✅ Methods
271
+ schema.shape.name // z.string()
272
+ schema.keyof() // z.enum(['name', 'age'])
273
+ schema.extend({ email: z.email() })
274
+ schema.merge(otherSchema)
275
+ schema.pick({ name: true })
276
+ schema.omit({ age: true })
277
+ schema.partial() // 모든 필드 optional
278
+ schema.deepPartial() // 재귀적 partial
279
+ schema.required() // 모든 필드 required
280
+ schema.passthrough() // 추가 키 허용
281
+ schema.strict() // 추가 키 에러
282
+ ```
283
+
284
+ ## Arrays
285
+
286
+ ```typescript
287
+ z.array(z.string())
288
+ .min(1, { error: "At least 1 item." })
289
+ .max(10)
290
+ .length(5)
291
+ .nonempty()
292
+
293
+ // ✅ Non-empty array
294
+ z.string().array().nonempty()
295
+ ```
296
+
297
+ ## Tuples
298
+
299
+ ```typescript
300
+ const tuple = z.tuple([
301
+ z.string(),
302
+ z.number(),
303
+ z.boolean(),
304
+ ])
305
+
306
+ type Tuple = z.infer<typeof tuple>
307
+ // → [string, number, boolean]
308
+
309
+ // ✅ Rest parameter
310
+ z.tuple([z.string()]).rest(z.number())
311
+ // → [string, ...number[]]
312
+ ```
313
+
314
+ ## Unions
315
+
316
+ ```typescript
317
+ const schema = z.union([z.string(), z.number()])
318
+ // 또는
319
+ const schema = z.string().or(z.number())
320
+
321
+ type Value = z.infer<typeof schema>
322
+ // → string | number
323
+ ```
324
+
325
+ ## Discriminated Unions
35
326
 
36
327
  ```typescript
37
- // v4 new API
38
- z.email() z.url() z.uuid()
39
- z.iso.date() z.iso.datetime() z.iso.duration()
40
- z.stringbool() // "true"/"yes"/"1" → true
328
+ const schema = z.discriminatedUnion('type', [
329
+ z.object({ type: z.literal('email'), email: z.email() }),
330
+ z.object({ type: z.literal('phone'), phone: z.string() }),
331
+ ])
332
+
333
+ type Contact = z.infer<typeof schema>
334
+ // → { type: 'email'; email: string } | { type: 'phone'; phone: string }
335
+ ```
336
+
337
+ ## Records
41
338
 
42
- // ❌ v3 deprecated
43
- z.string().email() z.string().url()
339
+ ```typescript
340
+ z.record(z.string()) // Record<string, string>
341
+ z.record(z.enum(['a', 'b']), z.number()) // Record<'a' | 'b', number>
342
+ ```
44
343
 
45
- // Changes
46
- z.string().min(5, { error: "Too short." }) // message → error
47
- z.strictObject({ name: z.string() }) // Error on extra keys
48
- z.looseObject({ name: z.string() }) // Allow extra keys
49
- z.string().refine(val => val.includes("@")).min(5) // Refinement chaining
344
+ ## Maps & Sets
50
345
 
51
- // Template literals
52
- const css = z.templateLiteral([z.number(), z.enum(["px", "em", "rem"])])
53
- // `${number}px` | `${number}em` | `${number}rem`
346
+ ```typescript
347
+ z.map(z.string(), z.number()) // Map<string, number>
348
+ z.set(z.string()) // Set<string>
349
+ ```
350
+
351
+ </complex_types>
352
+
353
+ ---
354
+
355
+ <validation>
356
+
357
+ ## Optional & Nullable
358
+
359
+ ```typescript
360
+ z.string().optional() // string | undefined
361
+ z.string().nullable() // string | null
362
+ z.string().nullish() // string | null | undefined
363
+ ```
364
+
365
+ ## Default Values
366
+
367
+ ```typescript
368
+ z.string().default("default")
369
+ z.number().default(0)
370
+ z.boolean().default(false)
371
+ ```
372
+
373
+ ## Enums
374
+
375
+ ```typescript
376
+ // ✅ Native enum
377
+ enum Fruits {
378
+ Apple,
379
+ Banana,
380
+ }
381
+ z.nativeEnum(Fruits)
382
+
383
+ // ✅ Zod enum
384
+ const fruits = z.enum(['apple', 'banana', 'orange'])
385
+ type Fruit = z.infer<typeof fruits> // 'apple' | 'banana' | 'orange'
386
+
387
+ // ✅ 값 추출
388
+ fruits.enum.apple // 'apple'
389
+ fruits.options // ['apple', 'banana', 'orange']
390
+ ```
391
+
392
+ ## Literals
393
+
394
+ ```typescript
395
+ z.literal('hello')
396
+ z.literal(42)
397
+ z.literal(true)
398
+ ```
399
+
400
+ </validation>
401
+
402
+ ---
403
+
404
+ <transforms>
405
+
406
+ ## Transform
407
+
408
+ ```typescript
409
+ const schema = z.string()
410
+ .transform((val) => val.length)
411
+ .pipe(z.number().positive())
412
+
413
+ schema.parse("hello") // → 5
414
+ schema.parse("") // Error: number must be positive
415
+ ```
416
+
417
+ ## Preprocess
418
+
419
+ ```typescript
420
+ const schema = z.preprocess(
421
+ (val) => (val === "" ? undefined : val),
422
+ z.string().optional()
423
+ )
424
+
425
+ schema.parse("") // → undefined
426
+ schema.parse("hello") // → "hello"
427
+ ```
428
+
429
+ ## Coerce
430
+
431
+ ```typescript
432
+ z.coerce.string() // String(val)
433
+ z.coerce.number() // Number(val)
434
+ z.coerce.boolean() // Boolean(val)
435
+ z.coerce.date() // new Date(val)
436
+
437
+ // ✅ 예시
438
+ z.coerce.number().parse("42") // → 42
439
+ ```
440
+
441
+ </transforms>
442
+
443
+ ---
444
+
445
+ <refinements>
446
+
447
+ ## Refine
448
+
449
+ ```typescript
450
+ const schema = z.string()
451
+ .refine((val) => val.length >= 5, {
452
+ error: "Must be at least 5 characters.",
453
+ })
454
+
455
+ // ✅ v4: 체이닝 가능
456
+ z.string()
457
+ .refine((val) => val.includes("@"))
458
+ .min(5)
459
+ ```
460
+
461
+ ## SuperRefine
462
+
463
+ ```typescript
464
+ const schema = z.object({
465
+ password: z.string(),
466
+ confirmPassword: z.string(),
467
+ }).superRefine((data, ctx) => {
468
+ if (data.password !== data.confirmPassword) {
469
+ ctx.addIssue({
470
+ code: z.ZodIssueCode.custom,
471
+ path: ['confirmPassword'],
472
+ message: "Passwords don't match.",
473
+ })
474
+ }
475
+ })
476
+ ```
477
+
478
+ </refinements>
479
+
480
+ ---
481
+
482
+ <error_handling>
483
+
484
+ ## Error Handling
485
+
486
+ ```typescript
487
+ try {
488
+ schema.parse(data)
489
+ } catch (error) {
490
+ if (error instanceof z.ZodError) {
491
+ console.log(error.issues)
492
+ // [{
493
+ // code: 'invalid_type',
494
+ // expected: 'string',
495
+ // received: 'number',
496
+ // path: ['name'],
497
+ // message: 'Expected string, received number'
498
+ // }]
499
+ }
500
+ }
501
+
502
+ // ✅ Safe parse
503
+ const result = schema.safeParse(data)
504
+ if (!result.success) {
505
+ console.log(result.error.format())
506
+ }
507
+ ```
508
+
509
+ ## Custom Errors
510
+
511
+ ```typescript
512
+ const schema = z.string().min(5, { error: "Too short." })
513
+ const schema = z.email({ error: "Invalid email." })
514
+
515
+ // ✅ v4: error 파라미터 통합
516
+ z.number().int({ error: "Must be integer." })
517
+ ```
518
+
519
+ ## Prettify Errors
520
+
521
+ ```typescript
522
+ import { z } from 'zod'
523
+
524
+ const error = schema.safeParse(data).error
525
+ const pretty = z.prettifyError(error)
526
+ console.log(pretty)
527
+ ```
528
+
529
+ </error_handling>
530
+
531
+ ---
532
+
533
+ <advanced>
534
+
535
+ ## Template Literals
536
+
537
+ ```typescript
538
+ const css = z.templateLiteral([
539
+ z.number(),
540
+ z.enum(["px", "em", "rem"]),
541
+ ])
542
+
543
+ type CSS = z.infer<typeof css>
544
+ // → `${number}px` | `${number}em` | `${number}rem`
545
+
546
+ css.parse("16px") // ✅
547
+ css.parse("16") // ❌
548
+ ```
549
+
550
+ ## String Bool
551
+
552
+ ```typescript
553
+ const schema = z.stringbool()
554
+
555
+ schema.parse("true") // → true
556
+ schema.parse("1") // → true
557
+ schema.parse("yes") // → true
558
+ schema.parse("on") // → true
559
+ schema.parse("false") // → false
560
+ schema.parse("0") // → false
561
+ schema.parse("no") // → false
562
+ schema.parse("off") // → false
563
+ ```
564
+
565
+ ## File Validation
566
+
567
+ ```typescript
568
+ const schema = z.file()
569
+ .min(1024) // 1KB 이상
570
+ .max(10 * 1024 * 1024) // 10MB 이하
571
+ .mime(['image/png', 'image/jpeg'])
572
+
573
+ // ✅ 사용
574
+ schema.parse(fileInput.files[0])
575
+ ```
576
+
577
+ ## JSON Schema
578
+
579
+ ```typescript
580
+ const schema = z.object({
581
+ name: z.string(),
582
+ age: z.number(),
583
+ })
584
+
585
+ const jsonSchema = z.toJSONSchema(schema)
586
+ // {
587
+ // type: 'object',
588
+ // properties: {
589
+ // name: { type: 'string' },
590
+ // age: { type: 'number' }
591
+ // },
592
+ // required: ['name', 'age']
593
+ // }
594
+ ```
595
+
596
+ ## Promise & Function Schemas
597
+
598
+ ```typescript
599
+ const promiseSchema = z.promise(z.string())
600
+ const fnSchema = z.function()
601
+ .args(z.string(), z.number())
602
+ .returns(z.boolean())
603
+ ```
604
+
605
+ </advanced>
606
+
607
+ ---
608
+
609
+ <patterns>
610
+
611
+ ## TanStack Start Patterns
612
+
613
+ | 용도 | 패턴 |
614
+ |------|------|
615
+ | **Server Function** | `.inputValidator(schema)` |
616
+ | **Search Params** | `validateSearch: schema` |
617
+ | **Form 검증** | `schema.safeParse(formData)` |
618
+
619
+ ```typescript
620
+ // ✅ POST Server Function
621
+ const createPostSchema = z.object({
622
+ title: z.string().min(1).max(100),
623
+ content: z.string().min(1),
624
+ tags: z.array(z.string()).max(5),
625
+ })
626
+
627
+ export const createPost = createServerFn({ method: 'POST' })
628
+ .inputValidator(createPostSchema)
629
+ .handler(async ({ data }) => prisma.post.create({ data }))
630
+
631
+ // ✅ Search Params
632
+ export const Route = createFileRoute('/posts')({
633
+ validateSearch: z.object({
634
+ page: z.number().catch(1),
635
+ sort: z.enum(['newest', 'oldest']).catch('newest'),
636
+ }),
637
+ })
638
+
639
+ // ✅ Form 검증
640
+ const formSchema = z.object({
641
+ email: z.email(),
642
+ password: z.string().min(8),
643
+ })
644
+
645
+ const handleSubmit = (formData: FormData) => {
646
+ const result = formSchema.safeParse({
647
+ email: formData.get('email'),
648
+ password: formData.get('password'),
649
+ })
650
+
651
+ if (!result.success) {
652
+ return { errors: result.error.flatten() }
653
+ }
654
+
655
+ // result.data is type-safe
656
+ }
657
+ ```
658
+
659
+ ## Do's & Don'ts
660
+
661
+ | ✅ Do | ❌ Don't |
662
+ |-------|----------|
663
+ | `z.email()` 사용 | `z.string().email()` (deprecated) |
664
+ | `{ error: "..." }` 파라미터 | `{ message: "..." }` (v3) |
665
+ | `z.unknown()` 사용 | `z.any()` 남용 |
666
+ | `.safeParse()` 사용자 입력 | `.parse()` 사용자 입력 (throw) |
667
+ | 타입 추론 활용 | 중복 타입 정의 |
668
+
669
+ </patterns>
670
+
671
+ ---
672
+
673
+ <migration>
674
+
675
+ ## v3 → v4 Migration Checklist
676
+
677
+ | 항목 | 작업 |
678
+ |------|------|
679
+ | **String formats** | `z.string().email()` → `z.email()` |
680
+ | **Error messages** | `{ message: "..." }` → `{ error: "..." }` |
681
+ | **Object types** | `.strict()` → `z.strictObject()` |
682
+ | **Refinements** | 체이닝 가능 확인 |
683
+ | **Bundle size** | `zod/mini` 고려 (85% 감소) |
684
+
685
+ ```typescript
686
+ // ❌ v3
687
+ const schema = z.object({
688
+ email: z.string().email({ message: "Invalid email." }),
689
+ age: z.number().min(18, { message: "Must be 18+." }),
690
+ })
691
+
692
+ // ✅ v4
693
+ const schema = z.object({
694
+ email: z.email({ error: "Invalid email." }),
695
+ age: z.number().min(18, { error: "Must be 18+." }),
696
+ })
54
697
  ```
55
698
 
56
- </v4_changes>
699
+ </migration>