Zod는 주로 타입스크립트와 함께 사용되며, 정적 타입 추론을 통해 스키마 유효성을 검증하는 데 유용한 라이브러리이다.
TypeScript-first schema validation with static type inference
정적 타입 추론을 위해선 먼저 스키마를 정의해야 한다.
프론트단에서 사용자 정보에 대한 응답을 다루는 로직을 개발하고 있다고 가정하자.
스키마 정의
API의 응답 객체가 id, name, email, admin 필드로 구성되어 있다고 할 때, zod를 사용하여 다음처럼 userSchema를 구성할 수 있다.
const userSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
admin: z.boolean(),
createdAt: z.date(),
});
유효성 검사
이 스키마를 가지고 API의 응답 객체가 정확한 타입들로 구성되어 있는지 검증할 수 있다.
.parse()
zod의 .parse()를 사용해 유효성 검사를 할 수 있다.
const validatedData = {
id: 1,
name: "John",
email: "email@email.com",
admin: false,
createdAt: "2024-02-16T12:00:00Z"
};
userSchema.parse(validatedData); // 유효한 데이터이므로 validatedData 리턴
const invalidatedData = {
id: 1,
name: 123, // string이 아닌 number가 입력됨
email: "email@email.com",
admin: false,
createdAt: "2024-02-16T12:00:00Z"
};
userSchema.parse(invalidatedData); // 유효하지 않은 데이터이므로 ZodError를 throw
우리가 정의한 스키마에 일치한다면 유효성 검증을 통과했으므로 해당 데이터를 그대로 리턴하고,
불일치한다면 유효하지 않은 데이터로 간주하여 ZodError를 throw 하게 된다.
.safeParse()
만약, 어떤 상황은 에러를 thorw 하지 않기를 원할 수도 있으므로 .safeParse()를 사용하여 유효성 검증을 수행할 수 있다.
이때 리턴 값은 다음과 같다.
- 유효성 검사 성공 시: { success: true; data: validatedData }
- 유효성 검사 실패 시: { success: false; data: ZodError }
const validatedData = {
id: 1,
name: "John",
email: "email@email.com",
admin: false,
createdAt: "2024-02-16T12:00:00Z"
};
userSchema.safeParse(validatedData); // { success: true; data: validatedData }
const invalidatedData = {
id: 1,
name: 123, // string이 아닌 number가 입력됨
email: "email@email.com",
admin: false,
createdAt: "2024-02-16T12:00:00Z"
};
userSchema.safeParse(invalidatedData); // { success: false; error: ZodError }
타입 추론
만약 userSchema가 정의된 현재 상태에서 userType이 필요하면 어떻게 해야 할까?
interface User { id: number...}와 같이 타입을 또다시 정의하는 것은 같은 코드를 또 작성하는 것과 다를 것이 없다.
zod에서는 z.infer를 통해 스키마를 타입으로 뽑아내 사용할 수 있다.
type userType = z.infer<typeof userSchema>; // 스키마로부터 타입 추론
타입을 직접 선언했다면 최소 5줄의 코드가 작성했을 텐데, z.infer 이 한 줄의 코드가 5줄을 대체하게 된 것이다.
기본값 설정
음. 그럼 이제 스키마를 정의할 때 기본값을 정의할 수 있지 않을까라는 생각을 하게 된다.
위에서 정의한 userSchema에서 createdAt은 생성된 날짜를 의미하므로 자동으로 생성 시점의 날짜정보가 들어갈 수 있도록 초기값을 설정하면 사용자가 직접 입력할 필요도 없을 것 같다.
이럴때 .default()를 사용하여 초기값을 설정할 수 있다.
const userSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
admin: z.boolean(),
createdAt: z.date().default(() => new Date()),
});
Optional, Nulllable, Nullish
하지만, 관리자 여부는 실사용자가 직접 정할 것 같진 않은 부분이니 null값도 들어올 수 있을 것 같고 누군가는 선택을 하지 않을 수도 있을 것 같다는 생각이 든다.
선택적으로 값이 입력될 수 있을 때에는 .optional()을 사용할 수 있다.
const userSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
admin: z.boolean().optional(), // admin은 필수 입력 값이 아님
createdAt: z.date().default(() => new Date()),
});
null 값이 입력되게 하려면 .nullable()을 사용할 수 있다.
const userSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
admin: z.boolean().nullable(), // admin 필드는 null값 입력 가능
createdAt: z.date().default(() => new Date()),
});
하지만 우리가 admin 필드는 optional이면서 null도 입력될 수 있어야 한다.
이럴 때는 .nullish()를 사용하여 optionals + nullables로 입력받을 수 있게 할 수 있다.
const userSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
admin: z.boolean().nullish(), // admin 필드는 optinal이면서 null값 가능
createdAt: z.date().default(() => new Date()),
});
위의 과정처럼 zod를 활용하면 쉽게 유효성 검증과 타입 추론이 가능하다.
추가로, API 응답에는 있지만 실제로 프론트단에서 사용하지 않는 필드는 zod에 정의하지 않아도 된다.
TypeScript는 컴파일 시점에서의 타입에러만 잡아낼 수 있고 런타임에서의 타입 에러는 처리할 수 없다.
런타임에는 JavaScript가 작동되기 때문이다.
또한, TypeScript는 number만 입력받도록 강제하는 것은 가능하지만, 원하는 숫자 범위를 강제하거나 정수/실수 구분은 불가능하다.
zod를 활용하면 입력값의 최솟값, 최댓값을 지정할 수도 있다. (참고: https://zod.dev/?id=strings)
따라서, TypeScript와 함께 zod를 사용하면 타입 안전성과 데이터 검증을 동시에 만족할 수 있다.