zod에서 타입 검증 후 수행할 수 있는 데이터 처리 및 에러 메시지 등을 커스텀하는 방법에 대해 알아보려 한다.
.transform()
transform은 스키마 검증을 통과한 데이터를 입맛에 맞게 변경하고 싶을 때 사용할 수 있다.
입력된 문자열이 스키마 검증을 통과했을 때, 문자열의 길이로 변환하고 싶다면 transform() 내부에 length를 리턴하도록 함수를 작성해 주면 된다.
// 입력된 문자열을 문자열의 길이로 변환
const stringToNumber = z.string().transform((val) => val.length);
stringToNumber.parse("string"); // 6
이전 글에서 사용했던 userSchema에서 사용자의 이름을 입력받은 후 이름의 길이를 필요로 한다면 다음처럼 작성할 수 있다.
const userSchema = z.object({
id: z.number(),
name: z.string().transform((name) => name.length);, // 문자열의 길이를 리턴함
email: z.string().email(),
admin: z.boolean().default(false).nullish(),
createdAt: z.date().default(() => new Date()),
});
.enum()
만약, 정해진 값에 한해서만 입력을 받아야 한다면 zod를 활용해서 값을 제한해 둘 수는 없을까?
enum을 사용하면 해당 필드에 들어올 수 있는 값을 미리 설정할 수 있다.
const Options = z.enum(['a', 'b', 'c']);
Options.parse('a'); // 'a'
Options.parse('d'); // 에러
위 코드에서 Options에 문자열 a, b, c가 아닌 값이 입력되면 에러를 던지게 되는 것이다.
검증 커스텀
다양한 방법을 사용해 스키마를 정의하고 검증을 수행할 수 있는데, 오류가 발생했을 때 특정 필드에 메시지를 보여주고 싶다면 .refine()을 활용할 수 있다.
.refine(검증 조건, 에러 메시지)
사용자 정보를 입력받을 때, 사용자 이름에 50자 이상이 입력되는 경우는 이상적이지 않다.
name 필드에 다음과 같이 스키마를 작성하면 50자 이상의 문자열이 입력되었을 때 에러와 함께 "이름은 50자 이하의 문자열만 가능합니다!"라고 에러 메시지를 던질 수 있다.
const userSchema = z.object({
id: z.number(),
name: z.string().refine((name) => name.length <= 50, {
message: "이름은 50자 이하의 문자열만 가능합니다!",
});,
email: z.string().email(),
admin: z.boolean().default(false).nullish(),
createdAt: z.date().default(() => new Date()),
});
refine은 transform으로 변환한 값을 검증하는데도 사용할 수 있다.
const nameToGreeting = z
.string()
.transform((val) => val.toUpperCase())
.refine((val) => val.length > 15)
.transform((val) => `Hello ${val}`)
.refine((val) => val.indexOf("!") === -1);
위 스키마에 michaeljordan23이라는 문자열이 입력되었을 때의 과정을 살펴보자
nameToGreeting.parse("michaeljordan23")
1. "michaeljordan23" → "MICHAELJORDAN23" (대문자로 변환)
2. "MICHAELJORDAN23".length = 16 (15자 이상 통과 ✅)
3. "MICHAELJORDAN23" → "Hello MICHAELJORDAN23" (변환)
4. 느낌표 없음 (최종 통과 ✅)
이 경우에는 모든 데이터 변환과 검증을 성공적으로 통과했으므로 Hello MICHAELJORDAN23이 리턴될 것이다.
그렇다면 다음과 같은 경우엔 어떻게 될까?
nameToGreeting.parse("michaeljordan!!!!")
1. "michaeljordan!!!!" → "MICHAELJORDAN!!!!"
2. 길이 18 (15자 이상 통과 ✅)
3. 변환 후 → "Hello MICHAELJORDAN!!!!"
4. "!" 포함 (에러 발생 ❌)
문자열에 !가 포함되어 있어 마지막 refine 조건인 val.indexOf("!")에서 -1이 아닌 다른 값이 리턴되어 검증에 실패하게 된다.
에러 커스텀
검증 로직이 없어도 required_error와 invalid_type_error 옵션을 사용해 모든 Zod 스키마 타입에서 에러를 커스텀할 수 있다.
const ageSchema = z.number({
required_error: "Age is required", // 값이 입력되지 않았을 때
invalid_type_error: "Age must be a number", // 잘못된 타입이 입력되었을 때
});
const nameSchema = z.string({
required_error: "Name is required",
invalid_type_error: "Name must be a string",
});
const isActiveSchema = z.boolean({
required_error: "isActive is required",
invalid_type_error: "isActive must be true or false",
});
const tagsSchema = z.array(z.string(), {
required_error: "Tags are required",
invalid_type_error: "Tags must be an array of strings",
});
required_error에는 필수인 값이 입력되지 않았을 때 나타낼 메시지를,
invalid_type_error에는 잘못된 타입이 입력되었을 때 나타낼 메시지를 설정할 수 있다.
** optional을 쓰면 해당 필드는 선택적인 필드가 되므로 required_error는 무시된다.
lte(), gte(), min(), max() 등을 사용하여 값의 범위를 설정할 수도 있는데, 범위를 벗어나는 값이 입력되었을 때는 다음처럼 에러 메시지를 설정할 수 있다.
const schema = z.number().lte(100, { message: "100 이하만 가능합니다!" });
schema.parse(50); // 통과
schema.parse(100); // 통과
schema.parse(101); // "100 이하만 가능합니다!"
const passwordSchema = z.string()
.min(8, { message: "비밀번호는 최소 8자 이상이어야 합니다!" })
.regex(/[A-Z]/, { message: "비밀번호에 대문자가 포함되어야 합니다!" });
passwordSchema.parse("abc123"); // "비밀번호는 최소 8자 이상이어야 합니다!"
passwordSchema.parse("abcdef12"); // "비밀번호에 대문자가 포함되어야 합니다!"
refine과 같은 방식으로 message 옵션을 사용하여 에러 메세지 커스텀이 가능하다.
이처럼 zod는 입력 값에 대해 먼저 데이터 처리를 할 수 있다.
API 응답에 대해 전처리를 먼저 수행한 후 컴포넌트에 데이터가 전달된다면 컴포넌트단에서 필요한 형태로 데이터 가공을 하는 훅을 만드는 과정을 축소시킬 수 있을 것이다.
간단한 함수는 refine을 통해 완전히 zod로 대체하는 것도 충분히 가능하다.
단순한 타입 검증만이 아닌 데이터 전처리까지 가능하다는 점이 굉장히 매력적인 라이브러리라고 생각된다.