December 3, 20255 minutes
백엔드 API에서 넘어오는 데이터나 사용자가 입력한 폼 데이터를 다루다 보면, 이게 정말 우리가 기대한 그 형태가 맞는지 의심스러울 때가 한두 번이 아닌데요.
그래서 우리는 습관적으로 수많은 if 문을 작성하며 데이터를 검증하곤 합니다.
이번 글에서는 지루한 유효성 검사 코드를 획기적으로 줄여주고, 타입스크립트의 정적 타입 안전성과 런타임 검증을 동시에 잡아주는 강력한 라이브러리인 ‘Zod’에 대해 깊이 있게 알아보려고 합니다.
Zod를 사용하면 복잡한 보일러플레이트 코드 없이도 아주 우아하게 데이터를 검증할 수 있는데요.
왜 우리가 굳이 손수 검증 로직을 짜느라 고생할 필요가 없는지, Zod가 어떻게 개발자의 퇴근 시간을 앞당겨줄 수 있는지 상세한 튜토리얼로 준비했습니다.
먼저 Zod 없이 순수 타입스크립트만으로 데이터를 검증할 때 어떤 일이 벌어지는지 살펴보는 게 좋겠는데요.
보통 우리는 누락된 속성을 체크하기 위해 조건문을 덕지덕지 붙여야 하고, 데이터 타입을 일일이 확인하며 기본값까지 직접 설정해야 하는 번거로움을 겪습니다.
type User = {
name: string;
age: number;
};
async function fetchUser(): Promise<User> {
const res = await fetch('https://api.example.com/user');
const data = await res.json();
// 수동 검증의 지옥
if (!data.age || typeof data.age !== 'number') {
throw new Error(`User has no valid age`);
}
return {
// 이름이 없으면 기본값을 넣는 로직도 수동입니다.
name: data.name || 'John Doe',
age: data.age,
};
}이런 식의 방어적 코딩(Defensive Programming)은 데이터 구조가 조금만 복잡해져도 금방 관리하기 힘든 스파게티 코드가 되어버리는데요.
중첩된 객체나 배열이라도 다루게 된다면, 검증 로직만 수백 줄이 되는 끔찍한 경험을 하게 됩니다.
실수가 발생하기 딱 좋은 구조이며, 유지보수 측면에서도 최악이라고 할 수 있습니다.
Zod는 이러한 문제를 해결하기 위해 선언적인 방식으로 데이터의 형태(Schema)를 정의하고 검증하는데요.
코드가 얼마나 깔끔해지는지 바로 확인해 보겠습니다.
import { z } from 'zod';
// 스키마 정의가 곧 문서이자 타입입니다.
const UserSchema = z.object({
name: z.string().default('John Doe'),
age: z.number(),
});
async function fetchUser() {
const res = await fetch('https://api.example.com/user');
const data = await res.json();
// 단 한 줄로 검증과 타입 추론 완료!
return UserSchema.parse(data);
}보시다시피 UserSchema.parse(data) 한 줄로 모든 검증이 끝났습니다.
Zod는 정의된 스키마를 바탕으로 들어오는 데이터가 유효한지 런타임에 검사하고, 만약 데이터가 스키마와 맞지 않으면 즉시 상세한 에러를 던져주는데요.
이를 통해 코드는 훨씬 더 읽기 쉬워지고(DRY), 개발자는 비즈니스 로직에만 집중할 수 있게 됩니다.
Zod의 가장 강력한 무기 중 하나는 바로 타입 추론 기능인데요.
스키마를 정의해 두면, 타입스크립트 타입을 별도로 정의할 필요가 전혀 없습니다.
// 별도의 interface나 type 정의가 필요 없습니다.
type User = z.infer<typeof UserSchema>;
// User 타입은 자동으로 다음과 같이 추론됩니다:
// { name: string; age: number; }
이렇게 하면 ‘단일 진실 공급원(Single Source of Truth)‘을 유지할 수 있게 되는데요.
스키마를 수정하면 타입도 자동으로 업데이트되므로, 타입 정의와 검증 로직이 따로 노는 불상사를 원천적으로 차단할 수 있습니다.
데이터 검증에 실패했을 때 무작정 에러를 던지는(throw) 것이 부담스러울 때가 있는데요.
이럴 때는 safeParse 메서드를 사용하면 됩니다.
const validation = UserSchema.safeParse(user);
if (!validation.success) {
// 검증 실패 시, 우아하게 에러를 처리할 수 있습니다.
console.error(validation.error.format());
} else {
// 검증 성공 시, 안전하게 데이터에 접근합니다.
const userData = validation.data;
console.log(userData.name);
}safeParse는 결과 객체(Result Object)를 반환하는데, 성공 여부를 success 속성으로 명확하게 구분해 줍니다.
이는 함수형 프로그래밍 패턴에서 에러를 예외가 아닌 데이터로 처리할 때 매우 유용한 방식인데요.
서버가 다운되지 않도록 예외 처리를 유연하게 하고 싶을 때 적극 추천하는 패턴입니다.
현실 세계의 데이터는 단순하지 않고 복잡하게 얽혀 있는 경우가 많은데요.
Zod는 작은 스키마들을 레고 블록처럼 조립하여 복잡한 유효성 검사 로직을 구축할 수 있도록 지원합니다.
import { z } from 'zod';
// 재사용 가능한 주소 스키마
const AddressSchema = z.object({
street: z.string(),
city: z.string(),
state: z.string(),
postalCode: z.string(),
country: z.string(),
});
// 주소 스키마를 포함하는 사용자 스키마
export const UserSchema = z.object({
address: AddressSchema, // 스키마 재사용
name: z.string().default('John Doe'),
role: z.enum(['admin', 'user', 'guest']), // 리터럴 타입 지원
isActive: z.boolean().readonly(), // 읽기 전용 속성
});위 코드에서 보듯 AddressSchema를 따로 정의해 두면, 사용자 모델뿐만 아니라 배송지 정보나 회사 위치 등 주소가 필요한 모든 곳에서 재사용할 수 있습니다.
또한 z.enum이나 readonly 같은 기능을 통해 타입스크립트의 고유한 기능들도 완벽하게 지원하는데요.
이렇게 모듈화 된 스키마는 유지보수성을 극대화해 줍니다.
Zod는 단순히 타입만 체크하는 것이 아니라, 데이터의 구체적인 형태까지 아주 세밀하게 검증할 수 있는 기능을 제공합니다.
이메일 주소가 맞는지, 비밀번호가 너무 짧지는 않은지 확인하기 위해 정규표현식을 직접 짤 필요가 없는데요.
Zod에 내장된 다양한 메서드들을 활용하면 됩니다.
const SignupSchema = z.object({
email: z.string().email({ message: "유효한 이메일 주소를 입력해주세요." }),
homepage: z.string().url().optional(), // 선택적 필드
age: z.number().min(18, "성인만 가입 가능합니다.").max(100),
bio: z.string().max(100, "자기소개는 100자를 넘길 수 없습니다."),
});이렇게 메서드 체이닝을 통해 제약 조건을 직관적으로 추가할 수 있으며, 각 조건마다 커스텀 에러 메시지를 지정할 수도 있습니다.
이는 프론트엔드에서 폼 유효성 검사를 할 때 사용자 경험(UX)을 높여주는 핵심적인 기능이기도 합니다.
API에서 문자로 된 숫자가 넘어오거나, 특정 조건에 따라 데이터를 가공해야 할 때가 있는데요.
Zod는 coerce와 transform, refine을 통해 이런 까다로운 요구사항도 해결해 줍니다.
const ProductSchema = z.object({
// 문자열 "100"이 들어오면 숫자 100으로 자동 변환
price: z.coerce.number().min(0),
// 데이터 변환: 입력된 문자열을 대문자로 변환
tag: z.string().transform((val) => val.toUpperCase()),
// 커스텀 논리 검증: 패스워드 확인 등에 유용
password: z.string().min(8),
confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
message: "비밀번호가 일치하지 않습니다.",
path: ["confirmPassword"], // 에러가 표시될 위치 지정
});z.coerce를 사용하면 느슨한 타입의 데이터를 강제로 원하는 타입으로 변환할 수 있어 매우 편리합니다.
특히 .refine 메서드는 단순한 타입 체크를 넘어, 두 필드 간의 값을 비교하거나 복잡한 비즈니스 로직을 검증 과정에 포함시킬 수 있게 해 주는 강력한 도구인데요.
이를 활용하면 비밀번호 확인 로직 같은 것을 아주 깔끔하게 구현할 수 있습니다.
지금까지 Zod를 활용해 타입 안전성을 지키면서도 검증 로직을 간소화하는 방법에 대해 알아보았는데요.
Zod는 단순한 라이브러리를 넘어, 타입스크립트 개발자에게 없어서는 안 될 필수 도구로 자리 잡았습니다.
수동으로 작성하던 지루한 if 문들과 작별하고, 선언적이고 직관적인 Zod 스키마로 여러분의 코드를 더 단단하게 만들어보세요.