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. ํ์ ์ ํ ๊ท์น๋ค
- ์์ํ์ ๊ท์น
- ์์ํ์ ์ ๊ฐ์ ํ๋ณํ ๊ฐ๋ฅ
// 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"
- ๋ฆฌํฐ๋ด
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");