Zod
기존 JavaScript/TypeScript 환경에서 데이터 검증을 수행할 때는 조건문을 활용하거나 yup 같은 라이브러리를 사용했습니다.
하지만 최근에는 Zod가 더욱 강력하고 직관적인 방식으로 데이터 검증을 제공하여 많은 개발자들에게 사랑받고 있습니다.
Zod는 런타임 타입 검증(Runtime Type Validation)을 지원하는 TypeScript-first 라이브러리로, 선언적으로 스키마를 정의하고 검증할 수 있도록 도와줍니다.
Zod의 주요 장점
- TypeScript와의 강력한 시너지: 스키마 정의만으로 타입 추론이 가능
- 간결하고 선언적인 API: 직관적인 문법을 제공
- 런타임 타입 검증: TypeScript의 컴파일 단계가 아니라 실행 중에도 타입을 보장
- 체이닝 방식의 유효성 검사: 다양한 조건을 쉽게 추가 가능
Zod를 활용한 스키마 정의
const FormSchema = z.object({
id: z.string(),
customerId: z.string({ invalid_type_error: "Please select a customer." }),
amount: z.coerce
.number()
.gt(0, { message: "Please enter an amount greater than $0." }),
status: z.enum(["pending", "paid"], {
invalid_type_error: "Please select an invoice status.",
}),
date: z.string(),
});
특정 필드 제외
const CreateInvoice = FormSchema.omit({ id: true, date: true });
z.object({...})
객체 형태의 스키마를 정의하는 기본적인 함수입니다. 내부에 포함된 필드들을 정의할 수 있습니다.
z.string({ invalid_type_error })
문자열 타입을 기대하며, 잘못된 타입의 값이 들어오면 사용자 지정 에러 메시지를 반환합니다.
const schema = z.string({ invalid_type_error: "This field must be a string." });
schema.parse(123); // 오류 발생
z.coerce.number().gt(0, { message })
- z.coerce.number(): 입력된 값을 숫자로 강제 변환합니다.
- .gt(0): 입력값이 0보다 커야 한다는 조건을 추가합니다. .gt(x)는 "greater than x"를 의미합니다.
const schema = z.coerce.number().gt(0, { message: "Value must be greater than 0." });
schema.parse("5"); // 5 (문자열이 숫자로 변환됨)
schema.parse(0); // 오류 발생
z.enum(["pending", "paid"])
특정 문자열 값만 허용하는 열거형(enum) 타입을 정의합니다.
const statusSchema = z.enum(["pending", "paid"]);
statusSchema.parse("pending"); // 정상 동작
statusSchema.parse("completed"); // 오류 발생
z.omit({ id: true, date: true })
기존 스키마에서 특정 필드를 제외하여 새로운 스키마를 생성할 수 있습니다.
const schema = z.object({ id: z.string(), name: z.string(), age: z.number() });
const newSchema = schema.omit({ id: true });
console.log(newSchema.shape); // { name: z.string(), age: z.number() }
Zod의 safeParse를 활용한 폼 데이터 검증
아래 함수에서 safeParse()를 활용하여 폼 데이터를 검증하고 있습니다.
export async function createInvoice(prevState: State, formData: FormData) {
const validatedFields = CreateInvoice.safeParse({
customerId: formData.get("customerId"),
amount: formData.get("amount"),
status: formData.get("status"),
});
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
message: "Missing Fields. Failed to Create Invoice.",
};
}
const { customerId, amount, status } = validatedFields.data;
const amountInCents = amount * 100;
const date = new Date().toISOString().split("T")[0];
try {
await sql`
INSERT INTO invoices (customer_id, amount, status, date)
VALUES (${customerId}, ${amountInCents}, ${status}, ${date})
`;
} catch (error) {
return { message: "Database Error: Failed to Create Invoice." };
}
revalidatePath("/dashboard/invoices");
redirect("/dashboard/invoices");
}
유효성 검사 과정
- CreateInvoice.safeParse()를 사용해 formData 값을 검증합니다.
- 실패할 경우 validatedFields.error.flatten().fieldErrors를 반환하여 상세한 에러 정보를 제공합니다.
- 검증이 성공하면, 데이터를 변환(amount → cents) 후 데이터베이스에 삽입합니다.
- 성공적으로 삽입되면, 페이지 캐시를 재검증(revalidatePath)하고 사용자를 대시보드로 리디렉트(redirect)합니다.
추가적인 Zod 기능 활용
- refine(): 특정 조건을 추가로 검증할 때 사용
- transform(): 데이터를 원하는 형태로 변환할 때 사용
- merge(): 여러 스키마를 결합할 때 사용
예를 들어, date 필드가 현재 날짜 이후여야 한다면 refine()을 사용할 수 있습니다.
const EnhancedFormSchema = FormSchema.extend({
date: z.string().refine((val) => new Date(val) > new Date(), {
message: "Date must be in the future.",
}),
});
Zod를 활용하면 얻을 수 있는 장점
- 유효성 검사를 간결하고 가독성 높게 작성 가능
- TypeScript와 함께 사용하면 자동 완성 및 타입 안전성 확보
- 서버와 클라이언트 간 데이터 검증 일관성 유지
- 에러 메시지를 체계적으로 관리 가능
'개발 공부' 카테고리의 다른 글
| 웹소켓(WebSocket) (1) | 2025.04.04 |
|---|---|
| REST API (0) | 2025.03.27 |
| Request Waterfall 방식과 Parallel 방식 (0) | 2025.02.22 |
| Tailwind CSS vs. 다른 프레임워크 (0) | 2025.02.12 |
| Tailwind CSS 성능 최적화 (0) | 2025.02.11 |