zod

Zod (Typescript Schema Validator)

TypeScript ํ™˜๊ฒฝ์—์„œ ๊ฐ์ฒด ๊ตฌ์กฐ์™€ ๋ฐ์ดํ„ฐ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•˜๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

๋ถˆํ™•์‹คํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๋Ÿฐํƒ€์ž„์—์„œ ๊ฒ€์ฆํ•˜์—ฌ ํƒ€์ž… ๋ถˆ์ผ์น˜๋กœ ์ธํ•œ ์˜ค๋ฅ˜๋ฅผ ์‚ฌ์ „์— ๋ฐฉ์ง€ํ•จ

  • Schema(์Šคํ‚ค๋งˆ): ๊ฐ์ฒด์˜ ์„ค๊ณ„๋„

  • ๋Ÿฐํƒ€์ž„ ๊ฒ€์ฆ: ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ๋Š” ์ปดํŒŒ์ผ ํƒ€์ž„์—์„œ ํƒ€์ž…์„ ์ฒดํฌํ•˜์ง€๋งŒ, ๋Ÿฐํƒ€์ž„์—์„œ ๋“ค์–ด์˜ค๋Š” ์™ธ๋ถ€ ๋ฐ์ดํ„ฐ๊นŒ์ง€ ์•ˆ์ „ํ•˜๊ฒŒ ๊ฒ€์ฆ ๊ฐ€๋Šฅ

  • ์™ธ๋ถ€ JSON, API ์‘๋‹ต, ์‚ฌ์šฉ์ž ์ž…๋ ฅ ๋“ฑ ๋ถˆํ™•์‹คํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ๋‹ค๋ฃจ๋Š” ๋ฐ ํ•„์ˆ˜

Zod ๋ฉ”์„œ๋“œ ํ•ต์‹ฌ ๋ถ„๋ฅ˜

1. ๊ฐ์ฒด/ํƒ€์ž… ์ •์˜

๋ฉ”์„œ๋“œ
๋ชฉ์ 
์˜ˆ์‹œ

z.object({...})

๊ฐ์ฒด ๊ตฌ์กฐ ์ •์˜

z.object({ name: z.string() })

z.string(), z.number(), z.boolean()

๊ธฐ๋ณธ ํƒ€์ž… ์ •์˜

z.string().min(3)

z.array(schema)

๋ฐฐ์—ด ๊ฒ€์ฆ

z.array(z.string())

z.enum([...])

๋ฆฌํ„ฐ๋Ÿด ๊ฐ’ ์ง‘ํ•ฉ ๊ฒ€์ฆ

z.enum(['A','B'])

z.literal(value)

ํŠน์ • ๊ฐ’๋งŒ ํ—ˆ์šฉ

z.literal('admin')


2. ๊ฐ’ ๊ฒ€์ฆ / ์กฐ๊ฑด๋ถ€ ๊ฒ€์ฆ

๋ฉ”์„œ๋“œ
๋ชฉ์ 
์˜ˆ์‹œ

.min(), .max()

์ตœ์†Œ/์ตœ๋Œ€ ๊ธธ์ด, ๊ฐ’ ๊ฒ€์ฆ

z.string().min(3)

.email(), .url()

ํฌ๋งท ๊ฒ€์ฆ

z.string().email()

.refine(predicate, { message })

์ปค์Šคํ…€ ์กฐ๊ฑด ๊ฒ€์ฆ

z.string().refine(val => val.includes('@'), { message: '์ด๋ฉ”์ผ ์•„๋‹˜' })

.optional()

์„ ํƒ์  ํ•„๋“œ

z.string().optional()

.nullable()

null ํ—ˆ์šฉ

z.string().nullable()


3. ๊ฐ’ ๋ณ€ํ™˜ / ๊ฐ€๊ณต

๋ฉ”์„œ๋“œ
๋ชฉ์ 
์˜ˆ์‹œ

.transform(fn)

๊ฒ€์ฆ๋œ ๊ฐ’์„ ๋‹ค๋ฅธ ํ˜•ํƒœ๋กœ ๋ณ€ํ™˜ (sync/async)

z.string().transform(val => Number(val))

.coerce.*()

์›์‹œ ํƒ€์ž… ๊ฐ•์ œ ๋ณ€ํ™˜

z.coerce.number() (๋ฌธ์ž์—ด โ†’ ์ˆซ์ž)

.catch(defaultValue)

์‹คํŒจ ์‹œ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ๋Œ€์ฒด

z.number().min(0).catch(0)


4. ํŒŒ์‹ฑ / ์•ˆ์ „ํ•œ ์ฒ˜๋ฆฌ

๋ฉ”์„œ๋“œ
๋ชฉ์ 
์˜ˆ์‹œ

.parse(value)

๊ฐ’ ๊ฒ€์ฆ + ๋ณ€ํ™˜, ์‹คํŒจ ์‹œ ์˜ˆ์™ธ throw

schema.parse(input)

.safeParse(value)

๊ฐ’ ๊ฒ€์ฆ + ๋ณ€ํ™˜, ์‹คํŒจ ์‹œ { success, error } ๋ฐ˜ํ™˜

schema.safeParse(input)

.parseAsync(value)

async transform ํฌํ•จ ์‹œ ์‚ฌ์šฉ

await schema.parseAsync(input)

.safeParseAsync(value)

async transform + ์•ˆ์ „ ์ฒ˜๋ฆฌ

await schema.safeParseAsync(input)


5. ํƒ€์ž… ์ถ”๋ก 

๋ฉ”์„œ๋“œ
๋ชฉ์ 
์˜ˆ์‹œ

z.infer<typeof schema>

์Šคํ‚ค๋งˆ ๊ธฐ๋ฐ˜ TypeScript ํƒ€์ž… ์ถ”๋ก 

type User = z.infer<typeof userSchema>

Error

ZodError ๊ตฌ์กฐ๋ฅผ ํ†ตํ•ด ์–ด๋–ค ํ•„๋“œ๊ฐ€ ์™œ ์‹คํŒจํ–ˆ๋Š”์ง€ ํ™•์ธ ๊ฐ€๋Šฅ

  • .errors : ๊ฐ ํ•„๋“œ๋ณ„ ์—๋Ÿฌ ์ •๋ณด

  • .message : ๊ธฐ๋ณธ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€

const schema = z.string().min(5, { message: "์ตœ์†Œ 5๊ธ€์ž ํ•„์š”" });
const result = schema.safeParse("abc");
if (!result.success) console.log(result.error.errors);

ZodError ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ

{
  issues: [
    {
      code: 'invalid_type',    // ์—๋Ÿฌ ์ฝ”๋“œ. Zod ๋‚ด๋ถ€์—์„œ ์ •์˜ํ•œ ์—๋Ÿฌ ์œ ํ˜•
      expected: 'string',      // ์˜ˆ์ƒ ํƒ€์ž… ๋˜๋Š” ๊ฐ’
      received: 'undfeind',    // ์‹ค์ œ ๋“ค์–ด์˜จ ๊ฐ’
      path: [Array],           // ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•œ ํ•„๋“œ ๊ฒฝ๋กœ. ์ตœ์ƒ์œ„ ํ•„๋“œ๋Š” []๋กœ ํ‘œ์‹œ
      message: 'Not a string!' // ์ปค์Šคํ…€/๊ธฐ๋ณธ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€
    }
  ],
  // ๋Ÿฐํƒ€์ž„์— ์—๋Ÿฌ๋ฅผ ์ถ”๊ฐ€ํ•  ๋•Œ ์‚ฌ์šฉ๋˜๋Š” ๋ฉ”์„œ๋“œ
  addIssue: [Function (anonymous)], 
  addIssues: [Function (anonymous)],
  // ๋‚ด๋ถ€์ ์œผ๋กœ issues์™€ ๋™์ผ
  errors: [
    {
      code: 'invalid_type',
      expected: 'string',
      received: 'undefined',
      path: [Array],
      message: 'Not a string!'
    }
  ]
}

์—๋Ÿฌ ๊ด€๋ จ ๋ฉ”์„œ๋“œ

  • .refine()

    • ์ปค์Šคํ…€ ์กฐ๊ฑด ๊ฒ€์ฆ์„ ์ถ”๊ฐ€ํ•  ๋•Œ ์‚ฌ์šฉ

    • ์กฐ๊ฑด์„ ๋งŒ์กฑํ•˜์ง€ ์•Š์œผ๋ฉด ์ง€์ •ํ•œ ๋ฉ”์‹œ์ง€๋กœ ์‹คํŒจ ์ฒ˜๋ฆฌ

  • .catch()

    • ๊ฒ€์ฆ ์‹คํŒจ ์‹œ ๊ธฐ๋ณธ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ์ฒ˜๋ฆฌ

    • ์˜ˆ์™ธ๋ฅผ ๋˜์ง€์ง€ ์•Š๊ณ  ์•ˆ์ „ํ•˜๊ฒŒ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ๋Œ€์ฒด ๊ฐ€๋Šฅ

  • .message()

    • ๊ธฐ๋ณธ ์ œ๊ณต๋˜๋Š” ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ์ปค์Šคํ…€ํ•  ๋•Œ ์‚ฌ์šฉ

    • ๋Œ€๋ถ€๋ถ„ .min(), .max(), .email() ๋“ฑ ๋ฉ”์„œ๋“œ์— ์˜ต์…˜์œผ๋กœ ์ „๋‹ฌ

transform

๊ฒ€์ฆํ•œ ๊ฐ’(validated value)์„ ์›ํ•˜๋Š” ํ˜•ํƒœ๋กœ ๋ณ€ํ™˜ํ•  ๋•Œ ์‚ฌ์šฉ

  • ์™ธ๋ถ€ ๋ฐ์ดํ„ฐ๊ฐ€ ๊ฒ€์ฆ์€ ๋˜์ง€๋งŒ, ์•ฑ ๋‚ด๋ถ€์—์„œ ๋‹ค๋ฅธ ํ˜•ํƒœ๋กœ ์‚ฌ์šฉํ•ด์•ผํ•  ๋•Œ

  • Ex: ๋ฌธ์ž์—ด โ†’ ์ˆซ์ž, ๋‚ ์งœ ๋ฌธ์ž์—ด โ†’ Date ๊ฐ์ฒด, API ์‘๋‹ต ํฌ๋งท ๋ณ€ํ™˜ ๋“ฑ

  • validate โ†’ ๋ณ€ํ™˜ โ†’ ์‚ฌ์šฉ ์ˆœ์„œ๋ฅผ ํ•œ ์ค„๋กœ ์ค„์ผ ์ˆ˜ ์žˆ์Œ

  • async ๋น„๋™๊ธฐ ํ•จ์ˆ˜๋„ ์ง€์›ํ•จ

    • async transform์„ ์“ธ ๊ฒฝ์šฐ, parseAsync() ๋˜๋Š” safeParseAsync()๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•จ

const dateSchema = z.string().transform(str => new Date(str));

const date = dateSchema.parse("2025-08-17T00:00:00Z");
console.log(date instanceof Date); // true

๊ตญ์ œํ™” (i18n)

Zod ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ๊ตญ์ œํ™”ํ•  ์ˆ˜ ์žˆ์Œ

  • lib/i18n-zod.ts์— ์„ค์ •์„ ๋‘๊ณ , ํ”„๋กœ์ ํŠธ ์ „์—ญ์—์„œ import { z } from '@/lib/i18n-zod'๋กœ ์‚ฌ์šฉ

  • ์‚ฌ์šฉ์ž ์š”์ฒญ ํ—ค๋”(Accept-Language)์— ๋”ฐ๋ผ ๋™์ ์œผ๋กœ ์–ธ์–ด๋ฅผ ๋ฐ”๊ฟ”์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅ

import i18next from 'i18next';
import { z } from 'zod';
import { zodI18nMap } from 'zod-i18n-map'
// import your language translation files
import translation from 'zod-i18n-map/locales/ko/zod.json';

i18next.init({
  lng: 'ko',
  resources: {
    ko: { zod: translation },
  },
});
z.setErrorMap(zodI18nMap);

export { z };

ETC

  • API ์‘๋‹ต ๊ฒ€์ฆ

    • ์™ธ๋ถ€ API์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์„ ๋•Œ ํ•ญ์ƒ safeParse ์‚ฌ์šฉ

    • ์˜ˆ์™ธ๋กœ ์•ฑ์ด ํ„ฐ์ง€๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ ๊ฐ€๋Šฅ

  • TypeScript ํƒ€์ž…๊ณผ ์—ฐ๊ณ„

    • z.infer<typeof schema>๋กœ ํƒ€์ž… ์ถ”๋ก  โ†’ ํƒ€์ž… ์ •์˜ ์ค‘๋ณต ๋ฐฉ์ง€

  • Nested Object ๊ฒ€์ฆ

    • ๊ฐ์ฒด ์•ˆ ๊ฐ์ฒด ๊ตฌ์กฐ๋„ .object ์•ˆ์— .object๋ฅผ ์ค‘์ฒฉํ•ด ์‚ฌ์šฉ ๊ฐ€๋Šฅ

  • Optional & Default

    • optional()๊ณผ default()๋ฅผ ์กฐํ•ฉํ•˜์—ฌ ๋ˆ„๋ฝ ํ•„๋“œ ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅ

  • Coercion

    • ์ˆซ์ž๊ฐ€ ๋ฌธ์ž์—ด๋กœ ์˜ค๋Š” API ์‘๋‹ต ์‹œ .coerce.number() ์‚ฌ์šฉ ๊ฐ€๋Šฅ

  • Async ๊ฒ€์ฆ

    • ๋น„๋™๊ธฐ ๊ฒ€์ฆ ๋กœ์ง ํ•„์š” ์‹œ z.preprocess + async ๊ฐ€๋Šฅ

Last updated