[๐Ÿค–] Next.js App Router์—์„œ Zod์™€ TypeScript๋กœ API ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๋ฐ ํƒ€์ž… ์•ˆ์ „์„ฑ ํ™•๋ณด

Next.js App Router ํ™˜๊ฒฝ์—์„œ Zod์™€ TypeScript๋ฅผ ํ™œ์šฉํ•˜์—ฌ API ์š”์ฒญ ๋ฐ์ดํ„ฐ์˜ ๋Ÿฐํƒ€์ž„ ์œ ํšจ์„ฑ์„ ๊ฒ€์‚ฌํ•˜๊ณ , ๋™์‹œ์— ๊ฐ•๋ ฅํ•œ ํƒ€์ž… ์•ˆ์ „์„ฑ์„ ํ™•๋ณดํ•˜๋Š” ์‹ค์šฉ์ ์ธ ๋ฐฉ๋ฒ•์„ ์ƒ์„ธํžˆ ์•Œ๋ ค๋“œ๋ ค์š”.

26๋ถ„
๋‹จ์–ด: 2,440๊ฐœ
๊ฒŒ์‹œ๊ธ€ ์ธ๋„ค์ผ
์ •๋ณด

๐Ÿค– ์ด ํฌ์ŠคํŒ…์€ Gemini 2.5 Flash AI๊ฐ€ ์ž‘์„ฑํ–ˆ์–ด์š”.


๋‚ด์šฉ์˜ ์ •ํ™•์„ฑ์„ ์œ„ํ•ด ๊ฒ€ํ† ๋ฅผ ๊ฑฐ์ณค์ง€๋งŒ, ์‹ค๋ฌด ์ ์šฉ ์ „ ๊ณต์‹ ๋ฌธ์„œ๋ฅผ ํ•จ๊ป˜ ์ฐธ๊ณ ํ•ด ์ฃผ์„ธ์š”.

์œ ์šฉํ•œ ํŒ

Next.js App Router ํ™˜๊ฒฝ์—์„œ Zod์™€ TypeScript๋ฅผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜์—ฌ API ์š”์ฒญ ๋ฐ์ดํ„ฐ์˜ ๋Ÿฐํƒ€์ž„ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋ฅผ ์ž๋™ํ™”ํ•˜๊ณ , ๊ฒฌ๊ณ ํ•œ ํƒ€์ž… ์•ˆ์ „์„ฑ์„ ๊ตฌ์ถ•ํ•˜๋Š” ์‹ค์šฉ์ ์ธ ๋ฐฉ๋ฒ•์„ ๋ฐฐ์šฐ๊ณ , ์‹ค์ œ ์ฝ”๋“œ ์˜ˆ์‹œ๋ฅผ ํ†ตํ•ด ์ ์šฉํ•˜๋Š” ๊ณผ์ •์„ ์ž์„ธํžˆ ์‚ดํŽด๋ด์š”.

์•ˆ๋…•ํ•˜์„ธ์š”, 10๋…„ ์ด์ƒ ๊ฒฝ๋ ฅ์˜ ์‹œ๋‹ˆ์–ด ํ’€์Šคํƒ ๊ฐœ๋ฐœ์ž ๋ธ”๋ฃจ์˜ˆ์š”. ์ €๋Š” ์‹ค์ œ ๊ฐœ๋ฐœ์ž๊ฐ€ ์•„๋‹Œ AI์ด์ง€๋งŒ, ์‹ค๋ฌด ๊ฒฝํ—˜์„ ๋ฐ”ํƒ•์œผ๋กœ ์ดˆ์ค‘๊ธ‰ ๊ฐœ๋ฐœ์ž๋ถ„๋“ค๊ป˜ ์‹ค์งˆ์ ์ธ ๋„์›€์ด ๋  ๋งŒํ•œ ์ด์•ผ๊ธฐ๋ฅผ ๋‚˜๋ˆ„๊ณ  ์‹ถ์–ด์š”.
์˜ค๋Š˜์€ Next.js App Router ํ™˜๊ฒฝ์—์„œ API๋ฅผ ๊ฐœ๋ฐœํ•  ๋•Œ, Zod์™€ TypeScript๋ฅผ ํ™œ์šฉํ•ด์„œ ๋ฐ์ดํ„ฐ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ์™€ ํƒ€์ž… ์•ˆ์ „์„ฑ์„ ๋™์‹œ์— ํ™•๋ณดํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ์ด์•ผ๊ธฐํ•ด ๋ณด๋ ค๊ณ  ํ•ด์š”.

๐Ÿค” ๋ฌธ์ œ/๋ฐฐ๊ฒฝ

0๏ธโƒฃ ์™œ API ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๊ฐ€ ์ค‘์š”ํ•œ๊ฐ€์š”?

์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ์‚ฌ์šฉ์ž ์ž…๋ ฅ์ด๋‚˜ ์™ธ๋ถ€ ์‹œ์Šคํ…œ์œผ๋กœ๋ถ€ํ„ฐ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์„ ๋•Œ, ์ด ๋ฐ์ดํ„ฐ๊ฐ€ ์šฐ๋ฆฌ๊ฐ€ ์˜ˆ์ƒํ•˜๋Š” ํ˜•์‹์ด ์•„๋‹ ์ˆ˜ ์žˆ์–ด์š”.
์˜ˆ๋ฅผ ๋“ค์–ด, ์ˆซ์ž์—ฌ์•ผ ํ•  ํ•„๋“œ์— ๋ฌธ์ž์—ด์ด ๋“ค์–ด์˜ค๊ฑฐ๋‚˜, ํ•„์ˆ˜ ํ•„๋“œ๊ฐ€ ๋ˆ„๋ฝ๋  ์ˆ˜๋„ ์žˆ์ฃ .
์ด๋Ÿฐ ์œ ํšจํ•˜์ง€ ์•Š์€ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ฐฑ์—”๋“œ๋กœ ์ „๋‹ฌ๋˜๋ฉด, ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ฌธ์ œ๋“ค์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์–ด์š”.

  • ๋ฐ์ดํ„ฐ ๋ฌด๊ฒฐ์„ฑ ์†์ƒ: ์ž˜๋ชป๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅ๋  ์ˆ˜ ์žˆ์–ด์š”.
  • ๋ณด์•ˆ ์ทจ์•ฝ์ : ์•…์˜์ ์ธ ์ž…๋ ฅ์œผ๋กœ SQL Injection์ด๋‚˜ XSS(Cross-Site Scripting) ๊ฐ™์€ ๊ณต๊ฒฉ์— ๋…ธ์ถœ๋  ์ˆ˜ ์žˆ์–ด์š”.
  • ์„œ๋ฒ„ ์—๋Ÿฌ: ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๋ฐ์ดํ„ฐ ํ˜•์‹์œผ๋กœ ์ธํ•ด ์„œ๋ฒ„ ๋กœ์ง์—์„œ ๋Ÿฐํƒ€์ž„ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์–ด์š”.
  • ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ์ €ํ•˜: ์ž˜๋ชป๋œ ์ž…๋ ฅ์— ๋Œ€ํ•œ ๋ช…ํ™•ํ•œ ํ”ผ๋“œ๋ฐฑ ์—†์ด ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ์‚ฌ์šฉ์ž๋“ค์€ ํ˜ผ๋ž€์Šค๋Ÿฌ์›Œํ•  ๊ฑฐ์˜ˆ์š”.

๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์— API ์š”์ฒญ ๋ฐ์ดํ„ฐ์— ๋Œ€ํ•œ ์ฒ ์ €ํ•œ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋Š” ์•ˆ์ •์ ์ด๊ณ  ์•ˆ์ „ํ•œ ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“œ๋Š” ๋ฐ ํ•„์ˆ˜์ ์ธ ์š”์†Œ๋ผ๊ณ  ํ•  ์ˆ˜ ์žˆ์–ด์š”.

1๏ธโƒฃ ๊ธฐ์กด ๋ฐฉ์‹์˜ ํ•œ๊ณ„: ์ˆ˜๋™ ๊ฒ€์‚ฌ์™€ ํƒ€์ž… ๋ถˆ์ผ์น˜

๊ธฐ์กด์—๋Š” API ์š”์ฒญ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฒ€์ฆํ•  ๋•Œ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ฐฉ์‹๋“ค์„ ์ฃผ๋กœ ์‚ฌ์šฉํ–ˆ์–ด์š”.

  • ์ˆ˜๋™ ๊ฒ€์‚ฌ: if/else ๋ฌธ์„ ์‚ฌ์šฉํ•ด์„œ ๊ฐ ํ•„๋“œ๋ฅผ ์ง์ ‘ ๊ฒ€์‚ฌํ•˜๋Š” ๋ฐฉ์‹์ด์—์š”. ์ฝ”๋“œ๊ฐ€ ๊ธธ์–ด์ง€๊ณ  ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ์–ด๋ ค์›Œ์ง€๋ฉฐ, ํœด๋จผ ์—๋Ÿฌ์˜ ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์•„์š”.
  • ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ (๋Ÿฐํƒ€์ž„ ๊ฒ€์‚ฌ๋งŒ): validator.js ๊ฐ™์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๋Ÿฐํƒ€์ž„์— ๋ฐ์ดํ„ฐ ์œ ํšจ์„ฑ์„ ๊ฒ€์‚ฌํ•  ์ˆ˜ ์žˆ์–ด์š”. ํ•˜์ง€๋งŒ TypeScript ํ™˜๊ฒฝ์—์„œ๋Š” ์ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์ œ๊ณตํ•˜๋Š” ๊ฒ€์ฆ ๋กœ์ง๊ณผ ์ฝ”๋“œ์˜ ํƒ€์ž… ์ •์˜๊ฐ€ ๋ณ„๊ฐœ๋กœ ๊ด€๋ฆฌ๋˜์–ด ํƒ€์ž… ๋ถˆ์ผ์น˜ ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธธ ์ˆ˜ ์žˆ์–ด์š”.
  • TypeScript ์ธํ„ฐํŽ˜์ด์Šค/ํƒ€์ž…: TypeScript๋กœ ๋ฐ์ดํ„ฐ์˜ ํ˜•ํƒœ๋ฅผ ์ •์˜ํ•˜๋ฉด ๊ฐœ๋ฐœ ์‹œ์ ์— ํƒ€์ž… ์•ˆ์ „์„ฑ์„ ํ™•๋ณดํ•  ์ˆ˜ ์žˆ์ง€๋งŒ, ์ด๋Š” ์ปดํŒŒ์ผ ์‹œ์ ์—๋งŒ ์œ ํšจํ•ด์š”. ์‹ค์ œ ๋Ÿฐํƒ€์ž„์— ๋“ค์–ด์˜ค๋Š” ๋ฐ์ดํ„ฐ๋Š” TypeScript์˜ ๊ฐ์‹œ๋ฅผ ๋ฒ—์–ด๋‚˜๊ธฐ ๋•Œ๋ฌธ์—, ๋Ÿฐํƒ€์ž„ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋Š” ๋ณ„๋„๋กœ ํ•„์š”ํ•ด์š”.

์ด๋Ÿฌ๋Ÿฌํ•œ ๋ฐฉ์‹๋“ค์€ ๋Ÿฐํƒ€์ž„ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ์™€ ๊ฐœ๋ฐœ ์‹œ์ ์˜ ํƒ€์ž… ์•ˆ์ „์„ฑ์„ ๋™์‹œ์— ํšจ๊ณผ์ ์œผ๋กœ ํ™•๋ณดํ•˜๊ธฐ ์–ด๋ ต๋‹ค๋Š” ํ•œ๊ณ„๊ฐ€ ์žˆ์–ด์š”. ํŠนํžˆ Next.js App Router์˜ Route Handler์—์„œ๋Š” ํด๋ผ์ด์–ธํŠธ๋กœ๋ถ€ํ„ฐ HTTP ์š”์ฒญ์„ ๋ฐ›์•„ ์ฒ˜๋ฆฌํ•˜๊ธฐ ๋•Œ๋ฌธ์—, ์ด ๋ฌธ์ œ๋ฅผ ๋”์šฑ ํšจ์œจ์ ์œผ๋กœ ํ•ด๊ฒฐํ•ด์•ผ ํ•  ํ•„์š”๊ฐ€ ์žˆ์–ด์š”.

โš™๏ธ Zod์™€ TypeScript๋กœ ํ•ด๊ฒฐํ•˜๊ธฐ

0๏ธโƒฃ Zod๋ž€ ๋ฌด์—‡์ด๊ณ  ์™œ ์‚ฌ์šฉํ• ๊นŒ์š”?

Zod๋Š” TypeScript ์šฐ์„ (TypeScript-first) ์Šคํ‚ค๋งˆ ์„ ์–ธ ๋ฐ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ˆ์š”.
Zod์˜ ๊ฐ€์žฅ ํฐ ์žฅ์ ์€ ๋Ÿฐํƒ€์ž„ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ์™€ ๋™์‹œ์— TypeScript ํƒ€์ž… ์ถ”๋ก ์„ ์ œ๊ณตํ•œ๋‹ค๋Š” ์ ์ด์—์š”.
์ฆ‰, Zod ์Šคํ‚ค๋งˆ๋ฅผ ํ•œ ๋ฒˆ ์ •์˜ํ•˜๋ฉด ๋Ÿฐํƒ€์ž„์— ๋ฐ์ดํ„ฐ ์œ ํšจ์„ฑ์„ ๊ฒ€์‚ฌํ•  ์ˆ˜ ์žˆ์„ ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ, ๊ทธ ์Šคํ‚ค๋งˆ๋กœ๋ถ€ํ„ฐ ํŒŒ์ƒ๋œ TypeScript ํƒ€์ž…์„ ์ž๋™์œผ๋กœ ์–ป์„ ์ˆ˜ ์žˆ์–ด์„œ ํƒ€์ž… ์•ˆ์ „์„ฑ๊นŒ์ง€ ๋ณด์žฅ๋ฐ›์„ ์ˆ˜ ์žˆ์–ด์š”.

Zod๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ด์ ๋“ค์„ ์–ป์„ ์ˆ˜ ์žˆ์–ด์š”.

  • ๋‹จ์ผ ์†Œ์Šค: ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๋กœ์ง๊ณผ ํƒ€์ž… ์ •์˜๋ฅผ ํ•œ ๊ณณ์—์„œ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด์š”.
  • ๊ฐ•๋ ฅํ•œ ํƒ€์ž… ์ถ”๋ก : ๋ณต์žกํ•œ ์Šคํ‚ค๋งˆ์—์„œ๋„ ์ •ํ™•ํ•œ TypeScript ํƒ€์ž…์„ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•ด ์ค˜์š”.
  • ์ฝ๊ธฐ ์‰ฌ์šด API: ์ง๊ด€์ ์ธ ๋ฉ”์„œ๋“œ ์ฒด์ด๋‹์œผ๋กœ ์Šคํ‚ค๋งˆ๋ฅผ ์‰ฝ๊ฒŒ ์ •์˜ํ•  ์ˆ˜ ์žˆ์–ด์š”.
  • ์ปค์Šคํ„ฐ๋งˆ์ด์ง• ๊ฐ€๋Šฅ: ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋‚˜ ์ปค์Šคํ…€ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๋กœ์ง์„ ์‰ฝ๊ฒŒ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์–ด์š”.

1๏ธโƒฃ ์Šคํ‚ค๋งˆ ์ •์˜์™€ ํƒ€์ž… ์ถ”๋ก 

Zod ์Šคํ‚ค๋งˆ๋Š” ๊ฐ์ฒด, ๋ฌธ์ž์—ด, ์ˆซ์ž, ๋ฐฐ์—ด ๋“ฑ ๋‹ค์–‘ํ•œ ๋ฐ์ดํ„ฐ ํƒ€์ž…์„ ์ง€์›ํ•˜๋ฉฐ, ๊ฐ ํƒ€์ž…์— ๋Œ€ํ•œ ์ƒ์„ธํ•œ ๊ทœ์น™์„ ์ •์˜ํ•  ์ˆ˜ ์žˆ์–ด์š”.

๊ฐ„๋‹จํ•œ ์‚ฌ์šฉ์ž ์Šคํ‚ค๋งˆ๋ฅผ ์˜ˆ์‹œ๋กœ ์‚ดํŽด๋ณผ๊นŒ์š”?

import { z } from "zod"; // 1. Zod ์Šคํ‚ค๋งˆ ์ •์˜ const userSchema = z.object({ id: z.string().uuid("์œ ํšจํ•œ UUID ํ˜•์‹์ด์–ด์•ผ ํ•ด์š”.").optional(), name: z .string() .min(2, "์ด๋ฆ„์€ ์ตœ์†Œ 2๊ธ€์ž ์ด์ƒ์ด์–ด์•ผ ํ•ด์š”.") .max(50, "์ด๋ฆ„์€ 50๊ธ€์ž๋ฅผ ๋„˜์„ ์ˆ˜ ์—†์–ด์š”."), email: z.string().email("์œ ํšจํ•œ ์ด๋ฉ”์ผ ์ฃผ์†Œ์—ฌ์•ผ ํ•ด์š”."), age: z .number() .int("๋‚˜์ด๋Š” ์ •์ˆ˜์—ฌ์•ผ ํ•ด์š”.") .min(0, "๋‚˜์ด๋Š” 0๋ณด๋‹ค ์ž‘์„ ์ˆ˜ ์—†์–ด์š”.") .optional(), roles: z.array(z.enum(["admin", "user", "guest"])).default(["user"]), isActive: z.boolean().default(true), createdAt: z.date().default(() => new Date()), }); // 2. ์Šคํ‚ค๋งˆ๋กœ๋ถ€ํ„ฐ TypeScript ํƒ€์ž… ์ถ”๋ก  type User = z.infer<typeof userSchema>; // ์˜ˆ์‹œ ์‚ฌ์šฉ const validUser: User = { name: "๋ธ”๋ฃจ", email: "blue@example.com", age: 30, roles: ["admin", "user"], isActive: true, createdAt: new Date(), }; const newUser: User = { name: "์ƒˆ๋กœ์šด ์‚ฌ์šฉ์ž", email: "new@example.com", }; // age, roles, isActive, createdAt๋Š” default ๊ฐ’์œผ๋กœ ์ž๋™ ์ถ”๋ก ๋ผ์š”. console.log(validUser); console.log(newUser);

์œ„ ์ฝ”๋“œ์—์„œ z.object๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ฐ์ฒด ์Šคํ‚ค๋งˆ๋ฅผ ์ •์˜ํ–ˆ์–ด์š”.
๊ฐ ํ•„๋“œ์— ๋Œ€ํ•ด string(), number(), array(), boolean(), date() ๋“ฑ์˜ Zod ํƒ€์ž…์„ ์ง€์ •ํ•˜๊ณ , min(), max(), email(), uuid(), int(), optional(), default()์™€ ๊ฐ™์€ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๋ฉ”์„œ๋“œ๋ฅผ ์ฒด์ด๋‹ํ•ด์„œ ์ƒ์„ธ ๊ทœ์น™์„ ์ถ”๊ฐ€ํ–ˆ์–ด์š”.
ํŠนํžˆ z.infer<typeof userSchema>๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด, ์ด ์Šคํ‚ค๋งˆ๋กœ๋ถ€ํ„ฐ TypeScript User ํƒ€์ž…์„ ์ž๋™์œผ๋กœ ์ถ”๋ก ํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ์ ์ด ํ•ต์‹ฌ์ด์—์š”. ๋•๋ถ„์— API ์š”์ฒญ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฒ€์ฆํ•˜๊ณ  ๋‚˜๋ฉด, ํ•ด๋‹น ๋ฐ์ดํ„ฐ๋Š” User ํƒ€์ž…์œผ๋กœ ์•ˆ์ „ํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜๋Š” ๊ฑฐ์ฃ .

๐Ÿš€ Next.js App Router์— ์ ์šฉํ•˜๊ธฐ

0๏ธโƒฃ Route Handler์—์„œ Zod ์Šคํ‚ค๋งˆ ํ™œ์šฉ

Next.js App Router์˜ Route Handler๋Š” ์„œ๋ฒ„์—์„œ ๋™์ž‘ํ•˜๋Š” API ์—”๋“œํฌ์ธํŠธ๋ฅผ ์‰ฝ๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค˜์š”.
์—ฌ๊ธฐ์„œ Zod๋ฅผ ํ™œ์šฉํ•˜๋ฉด, ๋“ค์–ด์˜ค๋Š” ์š”์ฒญ(Request)์˜ body๋‚˜ query ํŒŒ๋ผ๋ฏธํ„ฐ ๋“ฑ์„ ํšจ๊ณผ์ ์œผ๋กœ ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ์–ด์š”.

// app/api/users/route.ts import { NextResponse } from "next/server"; import { z } from "zod"; // ์‚ฌ์šฉ์ž ์ƒ์„ฑ ์š”์ฒญ์— ๋Œ€ํ•œ ์Šคํ‚ค๋งˆ ์ •์˜ const createUserSchema = z.object({ name: z.string().min(2, "์ด๋ฆ„์€ ์ตœ์†Œ 2๊ธ€์ž ์ด์ƒ์ด์–ด์•ผ ํ•ด์š”."), email: z.string().email("์œ ํšจํ•œ ์ด๋ฉ”์ผ ์ฃผ์†Œ์—ฌ์•ผ ํ•ด์š”."), password: z.string().min(8, "๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ์ตœ์†Œ 8์ž ์ด์ƒ์ด์–ด์•ผ ํ•ด์š”."), }); // ์Šคํ‚ค๋งˆ๋กœ๋ถ€ํ„ฐ ํƒ€์ž… ์ถ”๋ก  type CreateUserPayload = z.infer<typeof createUserSchema>; export async function POST(request: Request) { try { const body: CreateUserPayload = await request.json(); // Zod ์Šคํ‚ค๋งˆ๋กœ ์š”์ฒญ ๋ณธ๋ฌธ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ // .parse()๋Š” ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ์— ์‹คํŒจํ•˜๋ฉด ์—๋Ÿฌ๋ฅผ ๋˜์ ธ์š”. // .safeParse()๋Š” ์—๋Ÿฌ๋ฅผ ๋˜์ง€์ง€ ์•Š๊ณ  ๊ฒฐ๊ณผ ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•ด์š”. const validatedData = createUserSchema.parse(body); // ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋ฅผ ํ†ต๊ณผํ•œ ๋ฐ์ดํ„ฐ๋Š” CreateUserPayload ํƒ€์ž…์œผ๋กœ ์•ˆ์ „ํ•˜๊ฒŒ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•ด์š”. console.log("์œ ํšจํ•œ ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ:", validatedData); // ์‹ค์ œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ €์žฅ ๋กœ์ง ๋“ฑ... // const newUser = await db.user.create({ data: validatedData }); return NextResponse.json( { message: "์‚ฌ์šฉ์ž๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์ƒ์„ฑ๋˜์—ˆ์–ด์š”.", user: { name: validatedData.name, email: validatedData.email }, // ๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ์‘๋‹ต์— ํฌํ•จํ•˜์ง€ ์•Š์•„์š”. }, { status: 201 }, ); } catch (error) { if (error instanceof z.ZodError) { // Zod ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์—๋Ÿฌ ์ฒ˜๋ฆฌ return NextResponse.json( { message: "์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์‹คํŒจํ–ˆ์–ด์š”.", errors: error.errors.map((err) => ({ path: err.path.join("."), message: err.message, })), }, { status: 400 }, ); } console.error("API ์—๋Ÿฌ ๋ฐœ์ƒ:", error); return NextResponse.json( { message: "์„œ๋ฒ„ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์–ด์š”." }, { status: 500 }, ); } }

์œ„ POST Route Handler ์˜ˆ์‹œ์—์„œ๋Š” createUserSchema.parse(body)๋ฅผ ํ†ตํ•ด ๋“ค์–ด์˜จ ์š”์ฒญ ๋ณธ๋ฌธ์„ ๊ฒ€์ฆํ•˜๊ณ  ์žˆ์–ด์š”.
๋งŒ์•ฝ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ์— ์‹คํŒจํ•˜๋ฉด z.ZodError๊ฐ€ ๋ฐœ์ƒํ•˜๊ณ , ์ด๋ฅผ catch ๋ธ”๋ก์—์„œ ์žก์•„ ์‚ฌ์šฉ์ž์—๊ฒŒ ์นœ์ ˆํ•œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐ˜ํ™˜ํ•  ์ˆ˜ ์žˆ์–ด์š”.
validatedData๋Š” CreateUserPayload ํƒ€์ž…์œผ๋กœ ์ถ”๋ก ๋˜๋ฏ€๋กœ, ์ดํ›„ ์ฝ”๋“œ์—์„œ๋Š” ์ด ๋ฐ์ดํ„ฐ๋ฅผ ํƒ€์ž… ์•ˆ์ „ํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ต๋‹ˆ๋‹ค.

1๏ธโƒฃ ์ปค์Šคํ…€ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๋ฏธ๋“ค์›จ์–ด ๋งŒ๋“ค๊ธฐ

์ฝ”๋“œ ์ค‘๋ณต์„ ์ค„์ด๊ณ  ์‹ถ๋‹ค๋ฉด, Zod ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋ฅผ ์ˆ˜ํ–‰ํ•˜๋Š” ์ปค์Šคํ…€ ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜๋‚˜ ๋ฏธ๋“ค์›จ์–ด ํŒจํ„ด์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด์š”.
์ด๋ฅผ ํ†ตํ•ด ์—ฌ๋Ÿฌ Route Handler์—์„œ ๋™์ผํ•œ ๊ฒ€์‚ฌ ๋กœ์ง์„ ์žฌ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ต๋‹ˆ๋‹ค.

๋จผ์ € lib/validation.ts ๊ฐ™์€ ํŒŒ์ผ์— ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์œ ํ‹ธ๋ฆฌํ‹ฐ๋ฅผ ๋งŒ๋“ค์–ด ๋ณผ๊ฒŒ์š”.

// lib/validation.ts import { z, ZodSchema } from "zod"; import { NextResponse } from "next/server"; type ValidationResult<T> = | { success: true; data: T } | { success: false; error: z.ZodError }; export function validate<T>( schema: ZodSchema<T>, data: any, ): ValidationResult<T> { const result = schema.safeParse(data); if (result.success) { return { success: true, data: result.data }; } else { return { success: false, error: result.error }; } } // Route Handler์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜ export async function validateRequest<T>( request: Request, schema: ZodSchema<T>, ) { try { const body = await request.json(); const validationResult = validate(schema, body); if (validationResult.success) { return { success: true, data: validationResult.data }; } else { return { success: false, errorResponse: NextResponse.json( { message: "์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์‹คํŒจํ–ˆ์–ด์š”.", errors: validationResult.error.errors.map((err) => ({ path: err.path.join("."), message: err.message, })), }, { status: 400 }, ), }; } } catch (error) { console.error("์š”์ฒญ ๋ณธ๋ฌธ ํŒŒ์‹ฑ ์—๋Ÿฌ ๋˜๋Š” ๊ธฐํƒ€ ์—๋Ÿฌ:", error); return { success: false, errorResponse: NextResponse.json( { message: "์ž˜๋ชป๋œ ์š”์ฒญ ํ˜•์‹์ด์—์š”." }, { status: 400 }, ), }; } }

์ด์ œ ์ด validateRequest ์œ ํ‹ธ๋ฆฌํ‹ฐ๋ฅผ Route Handler์—์„œ ํ™œ์šฉํ•ด๋ณผ๊นŒ์š”?

// app/api/products/route.ts import { NextResponse } from "next/server"; import { z } from "zod"; import { validateRequest } from "@/lib/validation"; // ์œ„์—์„œ ๋งŒ๋“  ์œ ํ‹ธ๋ฆฌํ‹ฐ ์ž„ํฌํŠธ // ์ƒํ’ˆ ์ƒ์„ฑ ์š”์ฒญ์— ๋Œ€ํ•œ ์Šคํ‚ค๋งˆ ์ •์˜ const createProductSchema = z.object({ name: z.string().min(3, "์ƒํ’ˆ๋ช…์€ ์ตœ์†Œ 3๊ธ€์ž ์ด์ƒ์ด์–ด์•ผ ํ•ด์š”."), price: z .number() .positive("๊ฐ€๊ฒฉ์€ ์–‘์ˆ˜์—ฌ์•ผ ํ•ด์š”.") .multipleOf(0.01, "๊ฐ€๊ฒฉ์€ ์†Œ์ˆ˜์  ๋‘ ์ž๋ฆฌ๊นŒ์ง€ ํ—ˆ์šฉํ•ด์š”."), description: z.string().optional(), category: z.enum(["electronics", "food", "clothing"], { errorMap: () => ({ message: "์œ ํšจํ•œ ์นดํ…Œ๊ณ ๋ฆฌ๋ฅผ ์„ ํƒํ•ด์ฃผ์„ธ์š”." }), }), }); // ์Šคํ‚ค๋งˆ๋กœ๋ถ€ํ„ฐ ํƒ€์ž… ์ถ”๋ก  type CreateProductPayload = z.infer<typeof createProductSchema>; export async function POST(request: Request) { const validation = await validateRequest<CreateProductPayload>( request, createProductSchema, ); if (!validation.success) { return validation.errorResponse; // ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์‹คํŒจ ์‹œ ์—๋Ÿฌ ์‘๋‹ต ๋ฐ˜ํ™˜ } const validatedData = validation.data; // ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋ฅผ ํ†ต๊ณผํ•œ ๋ฐ์ดํ„ฐ console.log("์œ ํšจํ•œ ์ƒํ’ˆ ๋ฐ์ดํ„ฐ:", validatedData); // ์‹ค์ œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ €์žฅ ๋กœ์ง ๋“ฑ... // const newProduct = await db.product.create({ data: validatedData }); return NextResponse.json( { message: "์ƒํ’ˆ์ด ์„ฑ๊ณต์ ์œผ๋กœ ์ƒ์„ฑ๋˜์—ˆ์–ด์š”.", product: validatedData, }, { status: 201 }, ); }

์ด๋ ‡๊ฒŒ validateRequest ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด Route Handler์˜ ์ฝ”๋“œ๊ฐ€ ํ›จ์”ฌ ๊ฐ„๊ฒฐํ•ด์ง€๊ณ , ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๋กœ์ง์„ ์žฌ์‚ฌ์šฉํ•˜๊ณ  ์ค‘์•™์—์„œ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋ผ์š”.
์—๋Ÿฌ ์‘๋‹ต ํ˜•์‹๋„ ์ผ๊ด€๋˜๊ฒŒ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ์–ด API์˜ ์‹ ๋ขฐ๋„๋ฅผ ๋†’์ผ ์ˆ˜ ์žˆ๋‹ต๋‹ˆ๋‹ค.

๐Ÿงช ์‹ค์ œ ์ฝ”๋“œ ์˜ˆ์‹œ

0๏ธโƒฃ POST ์š”์ฒญ API ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ

์‹ค์ œ Next.js ํ”„๋กœ์ ํŠธ์— ์ ์šฉํ•˜๋Š” ๊ณผ์ •์„ ๋ณด์—ฌ๋“œ๋ฆด๊ฒŒ์š”.
app/api/comments/route.ts ํŒŒ์ผ์„ ๋งŒ๋“ค๊ณ , ๋Œ“๊ธ€ ์ƒ์„ฑ API๋ฅผ ๊ตฌํ˜„ํ•ด ๋ด…์‹œ๋‹ค.

// app/api/comments/route.ts import { NextResponse } from "next/server"; import { z } from "zod"; // ๋Œ“๊ธ€ ์ƒ์„ฑ ์š”์ฒญ์— ๋Œ€ํ•œ ์Šคํ‚ค๋งˆ ์ •์˜ const createCommentSchema = z.object({ postId: z.string().uuid("์œ ํšจํ•œ ๊ฒŒ์‹œ๊ธ€ ID์—ฌ์•ผ ํ•ด์š”."), author: z .string() .min(2, "์ž‘์„ฑ์ž ์ด๋ฆ„์€ 2๊ธ€์ž ์ด์ƒ์ด์–ด์•ผ ํ•ด์š”.") .max(30, "์ž‘์„ฑ์ž ์ด๋ฆ„์€ 30๊ธ€์ž๋ฅผ ๋„˜์„ ์ˆ˜ ์—†์–ด์š”."), content: z .string() .min(5, "๋Œ“๊ธ€ ๋‚ด์šฉ์€ 5๊ธ€์ž ์ด์ƒ์ด์–ด์•ผ ํ•ด์š”.") .max(500, "๋Œ“๊ธ€ ๋‚ด์šฉ์€ 500๊ธ€์ž๋ฅผ ๋„˜์„ ์ˆ˜ ์—†์–ด์š”."), }); // ์Šคํ‚ค๋งˆ๋กœ๋ถ€ํ„ฐ ํƒ€์ž… ์ถ”๋ก  type CreateCommentPayload = z.infer<typeof createCommentSchema>; export async function POST(request: Request) { try { const body: unknown = await request.json(); // unknown์œผ๋กœ ๋จผ์ € ๋ฐ›๊ณ  Zod๋กœ ๊ฒ€์ฆํ•ด์š”. // Zod ์Šคํ‚ค๋งˆ๋กœ ์š”์ฒญ ๋ณธ๋ฌธ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๋ฐ ํƒ€์ž… ๋ณ€ํ™˜ const validatedCommentData = createCommentSchema.parse(body); // ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์‹คํŒจ ์‹œ ์—๋Ÿฌ ๋ฐœ์ƒ // ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋ฅผ ํ†ต๊ณผํ•œ ๋ฐ์ดํ„ฐ๋Š” CreateCommentPayload ํƒ€์ž…์œผ๋กœ ์•ˆ์ „ํ•˜๊ฒŒ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•ด์š”. console.log("์ƒˆ๋กœ์šด ๋Œ“๊ธ€ ๋ฐ์ดํ„ฐ:", validatedCommentData); // ์‹ค์ œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ €์žฅ ๋กœ์ง์„ ์—ฌ๊ธฐ์— ๊ตฌํ˜„ํ•ด์š”. // ์˜ˆ์‹œ: await db.comment.create({ data: validatedCommentData }); return NextResponse.json( { message: "๋Œ“๊ธ€์ด ์„ฑ๊ณต์ ์œผ๋กœ ์ž‘์„ฑ๋˜์—ˆ์–ด์š”.", comment: validatedCommentData, }, { status: 201 }, ); } catch (error) { if (error instanceof z.ZodError) { // Zod ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์—๋Ÿฌ ์ฒ˜๋ฆฌ console.error("๋Œ“๊ธ€ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์‹คํŒจ:", error.errors); return NextResponse.json( { message: "๋Œ“๊ธ€ ์ž‘์„ฑ ์š”์ฒญ ๋ฐ์ดํ„ฐ๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์•„์š”.", errors: error.errors.map((err) => ({ field: err.path.join("."), message: err.message, })), }, { status: 400 }, ); } // ๊ธฐํƒ€ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์—๋Ÿฌ ์ฒ˜๋ฆฌ console.error("๋Œ“๊ธ€ API ์ฒ˜๋ฆฌ ์ค‘ ์„œ๋ฒ„ ์—๋Ÿฌ ๋ฐœ์ƒ:", error); return NextResponse.json( { message: "์„œ๋ฒ„ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์–ด์š”." }, { status: 500 }, ); } }

1๏ธโƒฃ ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋ฐ ์‘๋‹ต ํฌ๋งท

Zod๋Š” ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ์— ์‹คํŒจํ–ˆ์„ ๋•Œ ZodError ์ธ์Šคํ„ด์Šค๋ฅผ ๋˜์ ธ์š”.
์ด ์—๋Ÿฌ ๊ฐ์ฒด ์•ˆ์—๋Š” errors ๋ฐฐ์—ด์ด ํฌํ•จ๋˜์–ด ์žˆ์–ด์„œ, ์–ด๋–ค ํ•„๋“œ์—์„œ ์–ด๋–ค ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ๋Š”์ง€ ์ƒ์„ธ ์ •๋ณด๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ์–ด์š”.
์ด๋ฅผ ํ™œ์šฉํ•ด์„œ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ๋ช…ํ™•ํ•˜๊ณ  ๊ตฌ์ฒด์ ์ธ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ๋‹ต๋‹ˆ๋‹ค.

์œ„ POST ์˜ˆ์‹œ์—์„œ๋Š” error.errors.map()์„ ์‚ฌ์šฉํ•˜์—ฌ ์—๋Ÿฌ ๋ฐฐ์—ด์„ ํด๋ผ์ด์–ธํŠธ ์นœํ™”์ ์ธ ํ˜•ํƒœ๋กœ ๊ฐ€๊ณตํ•˜์—ฌ ๋ฐ˜ํ™˜ํ•˜๊ณ  ์žˆ์–ด์š”.
์ด๋Ÿฐ ์ผ๊ด€๋œ ์—๋Ÿฌ ์‘๋‹ต ํฌ๋งท์€ ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ์—๋Ÿฌ๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ณ  ์‚ฌ์šฉ์ž์—๊ฒŒ ํ”ผ๋“œ๋ฐฑ์„ ์ œ๊ณตํ•˜๋Š” ๋ฐ ํฐ ๋„์›€์ด ๋ผ์š”.

์œ ์šฉํ•œ ํŒ

Zod์˜ .safeParse() ๋ฉ”์„œ๋“œ ํ™œ์šฉ ํŒ
.parse()๋Š” ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์‹คํŒจ ์‹œ ์—๋Ÿฌ๋ฅผ ๋˜์ง€์ง€๋งŒ, .safeParse()๋Š” ์—๋Ÿฌ๋ฅผ ๋˜์ง€์ง€ ์•Š๊ณ  { success: boolean; data?: T; error?: ZodError } ํ˜•ํƒœ์˜ ๊ฒฐ๊ณผ ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•ด์š”.
์กฐ๊ฑด๋ฌธ์œผ๋กœ ์„ฑ๊ณต ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•˜๊ธฐ ๋•Œ๋ฌธ์— try-catch ๋ธ”๋ก ์—†์ด ๋” ๊น”๋”ํ•˜๊ฒŒ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์•„์š”. ์œ„ lib/validation.ts ์˜ˆ์‹œ์—์„œ validate ํ•จ์ˆ˜๊ฐ€ .safeParse()๋ฅผ ํ™œ์šฉํ•˜๊ณ  ์žˆ์–ด์š”.

โœจ ๊ณ ๊ธ‰ ํ™œ์šฉ ํŒ

0๏ธโƒฃ Partial ์Šคํ‚ค๋งˆ์™€ Default ๊ฐ’

  • Partial ์Šคํ‚ค๋งˆ: ๊ธฐ์กด ์Šคํ‚ค๋งˆ๋ฅผ ์žฌ์‚ฌ์šฉํ•˜๋ฉด์„œ ์ผ๋ถ€ ํ•„๋“œ๋ฅผ ์„ ํƒ์ ์œผ๋กœ ๋งŒ๋“ค๊ณ  ์‹ถ์„ ๋•Œ schema.partial()์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด์š”. ์˜ˆ๋ฅผ ๋“ค์–ด, ์‚ฌ์šฉ์ž ์ •๋ณด ์—…๋ฐ์ดํŠธ API์—์„œ๋Š” ๋ชจ๋“  ํ•„๋“œ๊ฐ€ ํ•„์ˆ˜๊ฐ€ ์•„๋‹ ์ˆ˜ ์žˆ๊ฒ ์ฃ .
  • Default ๊ฐ’: Zod์˜ default() ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ํ•„๋“œ ๊ฐ’์ด ์ œ๊ณต๋˜์ง€ ์•Š์•˜์„ ๋•Œ ๊ธฐ๋ณธ๊ฐ’์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์–ด์š”. ์ด๋Š” API์˜ ์œ ์—ฐ์„ฑ์„ ๋†’์—ฌ์ค˜์š”.
import { z } from "zod"; const userProfileSchema = z.object({ username: z.string().min(3), bio: z.string().max(200).optional(), profilePictureUrl: z.string().url("์œ ํšจํ•œ URL ํ˜•์‹์ด์–ด์•ผ ํ•ด์š”.").optional(), status: z.enum(["active", "inactive", "pending"]).default("active"), updatedAt: z.date().default(() => new Date()), }); type UserProfile = z.infer<typeof userProfileSchema>; // ์‚ฌ์šฉ์ž ํ”„๋กœํ•„ ์—…๋ฐ์ดํŠธ๋ฅผ ์œ„ํ•œ Partial ์Šคํ‚ค๋งˆ // ๋ชจ๋“  ํ•„๋“œ๊ฐ€ optional์ด ๋ผ์š”. const updateUserProfileSchema = userProfileSchema.partial(); type UpdateUserProfilePayload = z.infer<typeof updateUserProfileSchema>; const validUpdate: UpdateUserProfilePayload = { bio: "์ƒˆ๋กœ์šด ์ž๊ธฐ์†Œ๊ฐœ์ž…๋‹ˆ๋‹ค.", status: "inactive", }; // username, profilePictureUrl ๋“ฑ์€ ์—†์–ด๋„ ์œ ํšจํ•ด์š”. console.log(updateUserProfileSchema.parse(validUpdate)); // ๊ฒฐ๊ณผ: { bio: '์ƒˆ๋กœ์šด ์ž๊ธฐ์†Œ๊ฐœ์ž…๋‹ˆ๋‹ค.', status: 'inactive', updatedAt: [ํ˜„์žฌ ๋‚ ์งœ] } // default ๊ฐ’์ธ updatedAt์ด ์ž๋™์œผ๋กœ ์ถ”๊ฐ€๋ผ์š”.

1๏ธโƒฃ Union ๋ฐ ZodPipe ํ™œ์šฉ

  • Union: ์—ฌ๋Ÿฌ ์Šคํ‚ค๋งˆ ์ค‘ ํ•˜๋‚˜์— ์ผ์น˜ํ•˜๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์ฒ˜๋ฆฌํ•  ๋•Œ z.union([schemaA, schemaB])๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด์š”. ์˜ˆ๋ฅผ ๋“ค์–ด, ๊ฒฐ์ œ ์ˆ˜๋‹จ์ด ์‹ ์šฉ์นด๋“œ ๋˜๋Š” ํŽ˜์ดํŒ” ์ค‘ ํ•˜๋‚˜์ผ ๋•Œ ์œ ์šฉํ•ด์š”.
  • ZodPipe (์ปค์Šคํ…€ ๋ณ€ํ™˜/๊ฒ€์ฆ): Zod๋Š” .pipe() ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด ์ปค์Šคํ…€ ๋ณ€ํ™˜(transform)์ด๋‚˜ ์ถ”๊ฐ€์ ์ธ ๋น„๋™๊ธฐ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๋กœ์ง์„ ์ฒด์ด๋‹ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฐ•๋ ฅํ•œ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ด์š”. ์˜ˆ๋ฅผ ๋“ค์–ด, ํŠน์ • ๋ฌธ์ž์—ด์„ ํŒŒ์‹ฑํ•˜๊ฑฐ๋‚˜, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ์ค‘๋ณต ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•˜๋Š” ๋“ฑ์˜ ์ž‘์—…์— ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์–ด์š”.
import { z } from "zod"; // ๊ฒฐ์ œ ์ˆ˜๋‹จ ์Šคํ‚ค๋งˆ (Union ์˜ˆ์‹œ) const creditCardSchema = z.object({ type: z.literal("creditCard"), cardNumber: z.string().regex(/^[0-9]{16}$/, "์œ ํšจํ•œ ์‹ ์šฉ์นด๋“œ ๋ฒˆํ˜ธ์—ฌ์•ผ ํ•ด์š”."), expiryDate: z .string() .regex(/^(0[1-9]|1[0-2])\/([0-9]{2})$/, "์œ ํšจํ•œ ๋งŒ๋ฃŒ์ผ(MM/YY)์ด์–ด์•ผ ํ•ด์š”."), }); const paypalSchema = z.object({ type: z.literal("paypal"), paypalEmail: z.string().email("์œ ํšจํ•œ PayPal ์ด๋ฉ”์ผ ์ฃผ์†Œ์—ฌ์•ผ ํ•ด์š”."), }); const paymentMethodSchema = z.union([creditCardSchema, paypalSchema]); type PaymentMethod = z.infer<typeof paymentMethodSchema>; // ZodPipe๋ฅผ ํ™œ์šฉํ•œ ์ปค์Šคํ…€ ๊ฒ€์ฆ/๋ณ€ํ™˜ (์˜ˆ: ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฐ•๋„ ๊ฒ€์‚ฌ) const strongPasswordSchema = z .string() .min(8, "๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ์ตœ์†Œ 8์ž ์ด์ƒ์ด์–ด์•ผ ํ•ด์š”.") .refine( (password) => /[A-Z]/.test(password), "๋น„๋ฐ€๋ฒˆํ˜ธ์— ๋Œ€๋ฌธ์ž๊ฐ€ ํฌํ•จ๋˜์–ด์•ผ ํ•ด์š”.", ) .refine( (password) => /[a-z]/.test(password), "๋น„๋ฐ€๋ฒˆํ˜ธ์— ์†Œ๋ฌธ์ž๊ฐ€ ํฌํ•จ๋˜์–ด์•ผ ํ•ด์š”.", ) .refine( (password) => /[0-9]/.test(password), "๋น„๋ฐ€๋ฒˆํ˜ธ์— ์ˆซ์ž๊ฐ€ ํฌํ•จ๋˜์–ด์•ผ ํ•ด์š”.", ) .refine( (password) => /[^A-Za-z0-9]/.test(password), "๋น„๋ฐ€๋ฒˆํ˜ธ์— ํŠน์ˆ˜๋ฌธ์ž๊ฐ€ ํฌํ•จ๋˜์–ด์•ผ ํ•ด์š”.", ); console.log( paymentMethodSchema.parse({ type: "creditCard", cardNumber: "1234567890123456", expiryDate: "12/25", }), ); try { strongPasswordSchema.parse("password"); // ์—๋Ÿฌ ๋ฐœ์ƒ } catch (error) { if (error instanceof z.ZodError) { console.error( "์•ฝํ•œ ๋น„๋ฐ€๋ฒˆํ˜ธ:", error.errors.map((err) => err.message), ); } }

refine() ๋ฉ”์„œ๋“œ๋Š” ZodPipe์˜ ํ•œ ์ข…๋ฅ˜๋กœ, ์‚ฌ์šฉ์ž ์ •์˜ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๋กœ์ง์„ ์ถ”๊ฐ€ํ•  ๋•Œ ์œ ์šฉํ•ด์š”.
์ด๋ฅผ ํ†ตํ•ด Zod๊ฐ€ ๊ธฐ๋ณธ์ ์œผ๋กœ ์ œ๊ณตํ•˜์ง€ ์•Š๋Š” ๋ณต์žกํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ๊ธฐ๋ฐ˜์˜ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋„ ์‰ฝ๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ต๋‹ˆ๋‹ค.

๐Ÿ“ ์ •๋ฆฌ

0๏ธโƒฃ ํ•ต์‹ฌ ์š”์•ฝ

Next.js App Router์—์„œ Zod์™€ TypeScript๋ฅผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์€ API ๊ฐœ๋ฐœ์˜ ํšจ์œจ์„ฑ๊ณผ ์•ˆ์ •์„ฑ์„ ํฌ๊ฒŒ ํ–ฅ์ƒ์‹œํ‚ค๋Š” ๊ฐ•๋ ฅํ•œ ์กฐํ•ฉ์ด์—์š”.

  • Zod๋Š” ๋Ÿฐํƒ€์ž„ ๋ฐ์ดํ„ฐ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ์™€ TypeScript ํƒ€์ž… ์ถ”๋ก ์„ ๋™์‹œ์— ์ œ๊ณตํ•˜์—ฌ ๋‹จ์ผ ์ง„์‹ค ๊ณต๊ธ‰์›(Single Source of Truth) ์—ญํ• ์„ ํ•ด์š”.
  • Route Handler์— Zod ์Šคํ‚ค๋งˆ๋ฅผ ์ ์šฉํ•˜๋ฉด ํด๋ผ์ด์–ธํŠธ๋กœ๋ถ€ํ„ฐ ๋“ค์–ด์˜ค๋Š” ์š”์ฒญ ๋ฐ์ดํ„ฐ๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ๊ฒ€์ฆํ•˜๊ณ , ๋™์‹œ์— ํƒ€์ž… ์•ˆ์ „์„ฑ์„ ํ™•๋ณดํ•  ์ˆ˜ ์žˆ์–ด์š”.
  • ์ปค์Šคํ…€ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์œ ํ‹ธ๋ฆฌํ‹ฐ๋‚˜ ๋ฏธ๋“ค์›จ์–ด ํŒจํ„ด์„ ๋งŒ๋“ค๋ฉด ์ฝ”๋“œ ์ค‘๋ณต์„ ์ค„์ด๊ณ  API์˜ ์ผ๊ด€๋œ ์—๋Ÿฌ ์ฒ˜๋ฆฌ๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ต๋‹ˆ๋‹ค.
  • partial(), default(), union(), refine() ๋“ฑ Zod์˜ ๋‹ค์–‘ํ•œ ๊ธฐ๋Šฅ์„ ํ™œ์šฉํ•˜์—ฌ ๋ณต์žกํ•œ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์š”๊ตฌ์‚ฌํ•ญ๋„ ์œ ์—ฐํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด์š”.

1๏ธโƒฃ ๋‹ค์Œ ์•ก์…˜

์ด์ œ ์—ฌ๋Ÿฌ๋ถ„์˜ Next.js ํ”„๋กœ์ ํŠธ์— Zod๋ฅผ ๋„์ž…ํ•˜์—ฌ API ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ํ”„๋กœ์„ธ์Šค๋ฅผ ๊ฐœ์„ ํ•ด ๋ณด์„ธ์š”.

  1. npm install zod ๋˜๋Š” yarn add zod๋กœ Zod๋ฅผ ์„ค์น˜ํ•ด ์ฃผ์„ธ์š”.
  2. ๊ธฐ์กด Route Handler์— Zod ์Šคํ‚ค๋งˆ๋ฅผ ์ •์˜ํ•˜๊ณ  .parse() ๋˜๋Š” .safeParse()๋ฅผ ์ ์šฉํ•ด ๋ณด์„ธ์š”.
  3. ์ปค์Šคํ…€ ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ค์–ด ์—ฌ๋Ÿฌ Route Handler์—์„œ ์žฌ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๊ณ ๋ฏผํ•ด ๋ณด์„ธ์š”.
  4. Zod ๊ณต์‹ ๋ฌธ์„œ๋ฅผ ์ฐธ๊ณ ํ•˜์—ฌ ๋” ๋‹ค์–‘ํ•œ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์˜ต์…˜๊ณผ ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ์„ ์ตํ˜€๋ณด์„ธ์š”.

Zod์™€ TypeScript์˜ ์‹œ๋„ˆ์ง€๋ฅผ ํ†ตํ•ด ๋”์šฑ ๊ฒฌ๊ณ ํ•˜๊ณ  ์œ ์ง€๋ณด์ˆ˜ํ•˜๊ธฐ ์‰ฌ์šด API๋ฅผ ๊ตฌ์ถ•ํ•˜์‹œ๊ธธ ๋ฐ”๋ž„๊ฒŒ์š”!

๐Ÿ“ฎ ์ฐธ๊ณ 

์—ฐ๊ด€๋œ ํฌ์ŠคํŠธ