validation(zod)

๋‚ ์งœ
Nov 29, 2024 11:05 AM
zod : ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
  • ์กฐ๊ฑด์„ ์•Œ๋ ค์ฃผ๋ฉด zod๊ฐ€ ์•Œ์•„์„œ ๊ฒ€์‚ฌ
  • validation ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ
  • transform ๋ฐ์ดํ„ฐ ๋ณ€ํ™˜
 
  • ์‚ฌ์‹ค input์€ ๊ธฐ๋ณธ ์†์„ฑ์œผ๋กœ ๋ธŒ๋ผ์šฐ์ € ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋ฅผ ์ง€์›ํ•œ๋‹ค(ํ”„๋ก ํŠธ์—”๋“œ์šฉ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ) โ‡’ min/max, minLength/maxLength, pattern(์ •๊ทœ์‹)
  • zod๋Š” ๋ฐฑ์—”๋“œ์šฉ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ์— ์ ํ•ฉ
 

๊ธฐ๋ณธ ์‚ฌ์šฉ๋ฒ•

import { z } from "zod"; // ๊ทœ์น™ (์—ฌ๊ธฐ์„œ๋Š” ๋ฌธ์ž์—ด์ด์–ด์•ผ ํ•˜๋Š” ๊ทœ์น™) const mySchema = z.string(); // ๊ฒ€์‚ฌ mySchema.parse("tuna"); // => ok. ("tuna") mySchema.parse(12); // => ์—๋Ÿฌ. (throws ZodError) // safe ๊ฒ€์‚ฌ (๊ฒ€์‚ฌ๊ฐ€ ์‹คํŒจํ•ด๋„ ์—๋Ÿฌx) mySchema.safeParse("tuna"); // => { success: true; data: "tuna" } mySchema.safeParse(12); // => { success: false; error: ZodError }
  • parse์˜ ๊ฒฐ๊ณผ๊ฐ’์€ ๋ฐ์ดํ„ฐ, safeParse๋Š” ์ •๋ณด๊ฐ€ ๋‹ด๊ธด ๊ฐ์ฒด์ž„์„ ์ฃผ์˜

๊ทœ์น™๋“ค

1. ํƒ€์ž… ์œ ํ˜• ๊ทœ์น™๋“ค

  1. ์›์‹œํƒ€์ž… ๊ทœ์น™
    1. // primitive values z.string(); z.number(); z.bigint(); z.boolean(); z.date(); z.symbol(); // empty types z.undefined(); z.null(); z.void(); // accepts undefined
      • ์›์‹œํƒ€์ž…์€ ๊ฐ•์ œ ํ˜•๋ณ€ํ™˜ ๊ฐ€๋Šฅ
        • z.coerce.string(); // String(input) z.coerce.number(); // Number(input) z.coerce.boolean(); // Boolean(input) z.coerce.bigint(); // BigInt(input) z.coerce.date(); // new Date(input) //ex. const schema = z.coerce.string(); schema.parse("tuna"); // => "tuna" schema.parse(12); // => "12" schema.parse(12); // => "12" schema.parse(true); // => "true" schema.parse(undefined); // => "undefined" schema.parse(null); // => "null"
  1. ๋ฆฌํ„ฐ๋Ÿด
    1. const tuna = z.literal("tuna"); const twelve = z.literal(12); const twobig = z.literal(2n); // bigint literal const tru = z.literal(true);
 
 

2. ์›์‹œํ˜•๋“ค ๊ทœ์น™

String
//์œ ํšจ์„ฑ ๊ทœ์น™ z.string().max(5); z.string().min(5); z.string().length(5); z.string().email(); z.string().url(); z.string().emoji(); //๋ณ€ํ˜• (transforms) z.string().trim(); // trim whitespace z.string().toLowerCase(); // toLowerCase z.string().toUpperCase(); // toUpperCase
String ์™ธ์—๋„, ๋ฌด์ˆ˜ํžˆ ๋งŽ์€ ๊ทœ์น™๋“ค์ด ์žˆ์Œ โ‡’ https://zod.dev/ ์ฐธ๊ณ 
 

Method

1. parse : ์•ž์„œ ๋งŒ๋“  ๊ทœ์น™์— ๋งž๋Š”์ง€ ๋ฐ์ดํ„ฐ ๊ฒ€์‚ฌ

const stringSchema = z.string(); stringSchema.parse("fish"); // => returns "fish" stringSchema.parse(12); // throws error

1-1. safeParse : ํ‹€๋ ค๋„ ์—๋Ÿฌ ๋˜์ง€์ง€ ์•Š๋Š” ๊ฒ€์‚ฌ

stringSchema.safeParse(12); // => { success: false; error: ZodError } stringSchema.safeParse("billie"); // => { success: true; data: 'billie' }

1-2. parseAsync: ๋น„๋™๊ธฐ parse

refine์˜ ํ•จ์ˆ˜๊ฐ€ async ํ•จ์ˆ˜์ผ ๋•Œ ์“ฐ๋Š” ๊ฒƒ
const stringSchema = z.string().refine(async (val) => val.length <= 8); await stringSchema.parseAsync("hello"); // => returns "hello" await stringSchema.parseAsync("hello world"); // => throws error await stringSchema.safeParseAsync("hello"); await stringSchema.spa("hello");
 

2. refine : ์ปค์Šคํ…€ ๊ทœ์น™

const myString = z.string().refine((val) => val.length <= 255, { message: "String can't be more than 255 characters", });
ํ•จ์ˆ˜ ๊ฒฐ๊ณผ๊ฐ’์ด true๋ฉด ์„ฑ๊ณต, false๋ฉด ์—๋Ÿฌ
 
  • object ์ „์ฒด์— refineํ•˜๋Š” ๊ฒฝ์šฐ ์—๋Ÿฌ ๋ฉ”์„ธ์ง€๋Š” formError๊ฐ€ ๋จ. ํŠน์ • ํ•„๋“œ์˜ ์—๋Ÿฌ ๋ฉ”์„ธ์ง€(fieldError)๊ฐ€ ๋˜๊ธธ ์›ํ•œ๋‹ค๋ฉด? โ‡’ path ํ•„๋“œ ์‚ฌ์šฉ!
    • const passwordForm = z .object({ password: z.string(), confirm: z.string(), }) .refine((data) => data.password === data.confirm, { message: "Passwords don't match", path: ["confirm"], // path of error });

2-1. superRefine : ์Šคํ‚ค๋งˆ ์ „์ฒด ๋˜๋Š” ์—ฌ๋Ÿฌ ํ•„๋“œ ๊ฐ„์˜ ์ƒํ˜ธ ๊ด€๊ณ„๋ฅผ ๊ฒ€์ฆ

โ€ข ์Šคํ‚ค๋งˆ ์ „์ฒด ๊ฒ€์ฆ: ์Šคํ‚ค๋งˆ์˜ ์—ฌ๋Ÿฌ ํ•„๋“œ ๊ฐ’์„ ๋™์‹œ์— ์ฐธ์กฐํ•˜์—ฌ ์ƒํ˜ธ ๊ด€๊ณ„๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ฒ€์ฆ ๊ฐ€๋Šฅ โ€ข ์‚ฌ์šฉ์ž ์ •์˜ ์˜ค๋ฅ˜ ์ถ”๊ฐ€: ๊ฒ€์ฆ ์‹คํŒจ ์‹œ ํŠน์ • ํ•„๋“œ์— ๋Œ€ํ•ด ์‚ฌ์šฉ์ž ์ •์˜ ์˜ค๋ฅ˜๋ฅผ ์ถ”๊ฐ€ ๊ฐ€๋Šฅ โ€ข ๋‹ค์ค‘ ๋กœ์ง ์ฒ˜๋ฆฌ: ์—ฌ๋Ÿฌ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๋กœ์ง์„ ํ•œ ๋ฒˆ์— ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅ โ€ข ๊ฒ€์ฆ ์ค‘๋‹จ ๊ธฐ๋Šฅ: ํŠน์ • ์กฐ๊ฑด์—์„œ ๊ฒ€์ฆ์„ ์ค‘๋‹จ(Abort Early)ํ•˜์—ฌ ์ดํ›„ ๊ฒ€์ฆ ๋กœ์ง์ด ์‹คํ–‰๋˜์ง€ ์•Š๋„๋ก ์„ค์ •๊ฐ€๋Šฅ
const emailSchema = z.string().superRefine((data, ctx) => { if (data.field1 !== data.field2) { ctx.addIssue({ //์˜ค๋ฅ˜๋ฅผ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ๋Š” ์ปจํ…์ŠคํŠธ ๊ฐ์ฒด, addIssue๋กœ ์—๋Ÿฌ ์ •๋ณด ์ •์˜ code: "custom", message: "field1 and field2 must match", path: ["field1"], // ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•œ ํ•„๋“œ ๊ฒฝ๋กœ }); ctx.addIssue({ code: "custom", message: "field1 and field2 must match", path: ["field2"], }); } });
//๊ฒ€์ฆ ์ค‘๋‹จ ๊ธฐ๋Šฅ => `ctx.addIssue()` ๋ฉ”์„œ๋“œ์— `fatal: true` ํ”Œ๋ž˜๊ทธ๋ฅผ ์ถ”๊ฐ€ const emailSchema = z.string().superRefine(async (email, ctx) => { const emailTest = z.string().email().safeParse(email); if (!emailTest.success) { ctx.addIssue({ code: "invalid_string", validation: "email", message: "Invalid Email", path: ["email"], //์—†์œผ๋ฉด error๊ฐ€ formError๋กœ ๋จ fatal: true, // ๊ฒ€์ฆ ์ค‘๋‹จ }); return; // ์ดํ›„ ๋กœ์ง(์ฆ‰ ๋ฐ‘์— ์ฝ”๋“œ)๊ณผ superRefine ์ดํ›„ ์ถ”๊ฐ€ ๊ฒ€์ฆ ์‹คํ–‰ x } // ์ด๋ฉ”์ผ ์ค‘๋ณต ์ฒดํฌ (ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅธ ๊ฒฝ์šฐ์—๋งŒ ์‹คํ–‰) const emailExists = await checkEmailInDatabase(email); // ๊ฐ€์ƒ์˜ ๋™๊ธฐ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์กฐํšŒ ํ•จ์ˆ˜ });
 

3. transform : ๋ฐ›์€ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ€ํ™˜ 

const stringToNumber = z.string().transform((val) => val.length); stringToNumber.parse("string"); // => 6
ํ•จ์ˆ˜์˜ return ๊ฐ’์œผ๋กœ ๋ณ€ํ™˜
 

4. default : ๊ธฐ๋ณธ๊ฐ’

const stringWithDefault = z.string().default("tuna"); stringWithDefault.parse(undefined); // => "tuna"
 
 

์—๋Ÿฌ ํ•ธ๋“ค๋ง

parse ๊ฒฐ๊ณผ ๊ฐ’์˜ success ์†์„ฑ ๋˜๋Š” error์œผ๋กœ ๊ทœ์น™์— ์ ํ•ฉ์—ฌ๋ถ€๋ฅผ ์•Œ ์ˆ˜ ์žˆ๋‹ค.
const result = z .object({ name: z.string(), }) .safeParse({ name: 12 }); if (!result.success) { result.error.issues; /* [ { "code": "invalid_type", "expected": "string", "received": "number", "path": [ "name" ], "message": "Expected string, received number" } ] */ }
 
  • format : ์—๋Ÿฌ ๊ฒฐ๊ณผ๋ฅผ ๊ฐ„๋‹จํ•˜๊ฒŒ ์•Œ ์ˆ˜ ์žˆ์Œ
    • ํ˜•ํƒœ - result.error.format()
if (!result.success) { const formatted = result.error.format(); /* { name: { _errors: [ 'Expected string, received number' ] } } */ formatted.name?._errors; // => ["Expected string, received number"] }
  • flatten : object์˜ ์—๋Ÿฌ์™€ ํ•„๋“œ์—๋Ÿฌ๋ฅผ ๊ตฌ๋ถ„ํ•ด์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Œ
    • ํ˜•ํƒœ - result.error.flatten()
if (!result.success) { console.log(result.error.flatten()); } /* { formErrors: [], fieldErrors: { name: ['Expected string, received null'], contactInfo: ['Invalid email'] }, } */
 

์—๋Ÿฌ๋ฉ”์‹œ์ง€

ํ•ด๋‹นํ•˜๋Š” ๊ทœ์น™์— ๋งž์ง€ ์•Š์€ ๋ฐ์ดํ„ฐ๊ฐ€ ์˜ค๋ฉด ์—๋Ÿฌ๋ฅผ ๋ณด๋‚ด๋Š”๋ฐ ์ด๋•Œ ๋ฉ”์„ธ์ง€๋ฅผ ์ปค์Šคํ…€ํ•  ์ˆ˜ ์žˆ์Œ
const name = z.string({ required_error: "Name is required", invalid_type_error: "Name must be a string", }); z.string().min(5, { message: "Must be 5 or more characters long" }); z.string().min(5, "Must be 5 or more characters long");