TypeScript는 정적 타입 시스템을 제공합니다. 즉, 코드를 작성하고 컴파일하는 시점에 타입 오류를 잡아낼 수는 있지만, 실제 코드가 브라우저나 서버에서 실행되는 런타임에서는 TypeScript의 타입 정보가 완전히 사라집니다.
컴파일 후 dist/ 폴더에 생성되는 파일은 전부 .js 확장자이며, 타입 정보는 포함되어 있지 않습니다.
API 응답, 사용자 입력 등 외부에서 유입되는 데이터는 런타임에 들어오며, 이에 대해 TypeScript는 아무런 보장도 하지 않습니다.
이러한 경우 런타임 오류가 발생하고, 이는 운영상의 장애로 이어질 수 있습니다.
런타임 데이터를 검사하려면 아래 중 하나가 필요합니다.
⇒ Yup/ Zod/ joi / Avj
이러한 라이브러리를 쓰면 어떠한 장점이 있나요?
⇒ 유효성 검사 기능 간결하게 구현 + 에러 처리 용이 + 스키마 정의 후 별도의 인터페이스 생성 없이 재활용 가능
joi - 정적 타입 추론을 지원하지 않아 제외 (Node.js에서 잘 쓰임.)
Avj - 가장 오래되긴 했습니다. (2015 / 올해 릴리즈 버전은 8까지 있음.) 다만 이 친구는 ts 지원 안되고, 특히 호환성과 의존성 문제들이 있을 수 있다고 합니다. 해당 라이브러리를 사용한 오래된 서비스들이 있다보니 계속 유지되고 있는것으로 보입니다.
1import * as yup from 'yup';
2
3const schema = yup.string();
4
5schema.isValid(123).then(console.log); // ✅ true (숫자도 통과)
6Zod
Yup은 아래 예시처럼 기본적으로 타입에 관대한 편입니다.
yup.string().isValid(333).then(console.log); // truestrict() 옵션을 사용하면 해결되긴 하지만, 이걸 매번 명시해야 한다는 점에서 실수의 여지가 큽니다.
반면 Zod는 기본적으로 엄격한 타입 검사를 적용합니다. 실수로 숫자를 문자열로 받는 일을 미연에 방지할 수 있죠.
저도 라이브러리 적용 전엔 아래 같은 함수들을 하나하나 구현했습니다.
const emailValidation = (email: string) => {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
};→ 이 방식은 검증 로직 재사용성 부족, 오류 메시지 일관성 문제, 테스트 및 유지보수 비용 증가 등의 문제가 있습니다.
비밀번호의 경우라면 비밀번호 정책에 따른 validation에 비밀번호 입력 / 비밀번호 확인 Input에 따라 별도의 함수를 만들고… x100
1import { z } from 'zod';
2
3const emailSchema = z.string().email({ message: "이메일 형식이 아닙니다." });
4
5const result = emailSchema.safeParse(userInput);
6
7if (!result.success) {
8 console.error(result.error.format());
9}emailSchema를 타입으로도 바로 사용할 수 있어 스키마와 타입 정의 일치safeParse를 사용해 런타임에서도 안전하게 검사 및 에러 핸들링 가능parse vs safeParse 언제, 왜 사용해야 할까?Zod에서 제공하는 parse와 safeParse는 모두 스키마 기반의 유효성 검사를 수행하지만, 오류를 처리하는 방식이 다릅니다. 이를 통해 예외 기반 처리와 조건 분기 기반 처리를 명확히 구분할 수 있습니다.
| API | 예외 발생 | 반환 타입 | 사용 시점 |
|---|---|---|---|
parse() | ❗ 예외 발생 | 값 or throw | 데이터가 반드시 올바르다고 가정할 때 |
safeParse() | ❌ 예외 없음 | { success: boolean, data?, error? } | 사용자 입력 등 오류 발생 가능성이 있는 경우 |
parse()는 테스트 코드, seed 데이터, 내부 시스템 간 통신에서 주로 사용safeParse()는 사용자 입력 폼, 외부 API 응답, 클라이언트-서버 통신의 입력 처리 등 안전성이 필요한 영역에서 사용1
2// 예외 발생 → try/catch 필요
3try {
4 const value = z.string().email().parse("not-an-email");
5} catch (e) {
6 console.error("유효하지 않은 이메일:", e);
7}
8
91
2// 조건 분기 → 흐름 제어가 깔끔함
3const result = z.string().email().safeParse("not-an-email");
4if (!result.success) {
5 console.error("입력 오류:", result.error.format());
6Zod의 강력한 장점은 타입 정의와 검증 스키마가 동일 소스로부터 유도된다는 것입니다. 이로 인해 타입 불일치로 인한 런타임 오류가 줄어듭니다.
1const userSchema = z.object({
2 id: z.number(),
3 name: z.string(),
4 email: z.string().email(),
5});
6
7// 타입 자동 추론
8type User = z.infer<typeof userSchema>;이렇게 하면 API 응답 타입을 별도 선언할 필요 없이 userSchema만 잘 작성하면 됩니다.
format() 활용Zod는 유효성 실패 시 다양한 형태의 에러 메시지 커스터마이징이 가능하며, safeParse 결과의 error.format()은 폼과 UI에 에러 메시지를 표시할 때 매우 유용합니다.
1const userSchema = z.object({
2 email: z.string().email({ message: '이메일 형식이 아닙니다.' }),
3 password: z.string().min(8, { message: '비밀번호는 최소 8자 이상이어야 합니다.' }),
4});
5
6const result = userSchema.safeParse(formData);
7
8// 결과가 실패했을 경우, 각 필드별 메시지를 구조화된 형태로 제공
9if (!result.success) {
10 const errors = result.error.format();
11 console.log(errors.email?._errors[0]); // "이메일 형식이 아닙니다."
12}이 구조는 react-hook-form과 연동할 때도 그대로 쓸 수 있어 UX 개선에 도움이 됩니다.
Zod는 단순히 검증만 하는 것이 아니라, 데이터를 자동으로 가공(transform) 할 수도 있습니다. 예를 들어:
1const schema = z.string().transform((val) => val.trim().toLowerCase());
2
3const result = schema.safeParse(" ExAmPlE@Email.Com ");
4console.log(result.data); // "example@email.com"
5
6merge, extend, partial, pick, omit 같은 스키마 조작 APIZod는 객체 스키마를 조작할 수 있는 다양한 메서드를 제공합니다. 이를 통해 공통 필드 추출, 일부 필드 제외, 부분 입력 허용 등이 가능합니다.
1const baseUser = z.object({
2 name: z.string(),
3 email: z.string().email(),
4});
5
6const updateUser = baseUser.partial(); // 모든 필드를 선택적으로 바꿈
7const createUser = baseUser.extend({
8 password: z.string().min(8),
9});→ 유지보수성과 재사용성을 대폭 향상시킬 수 있습니다.
제가 참여한 프로젝트에서는 자원 등록 폼의 입력 오류로 인해 여러 문제가 반복적으로 발생했습니다.
대표적인 사례는 다음과 같습니다.
초기에는 각 입력 필드마다 정규식과 조건문으로 직접 검증 로직을 작성했지만, 점점 케이스가 많아지면서 다음과 같은 어려움이 생겼습니다.
이런 반복적인 문제를 해결하기 위해, Zod와 react-hook-form을 기반으로 입력 검증 시스템을 리빌딩했습니다.
Zod로 각 입력값에 대한 스키마를 명확하게 정의safeParse()를 사용해 검증 결과를 안전하게 분기 처리error.format()을 활용해 필드별 에러 메시지를 구조화하여 UI에 표시1// 실제 코드는 아닙니다.
2const userSchema = z.object({
3 email: z.string().email({ message: '이메일 형식이 아닙니다.' }),
4 password: z.string().min(8, { message: '비밀번호는 최소 8자 이상이어야 합니다.' }),
5});
6
7const result = userSchema.safeParse(formData);
8
9if (!result.success) {
10 const errors = result.error.format();
11 // errors.email._errors[0] 이런 식으로 UI에 표시 가능
12}이렇게 하니 다음과 같은 개선이 즉각적으로 체감되었습니다.
TypeScript는 정적 타입 안정성을 제공하지만, 실제 사용자의 입력이나 외부 API 응답과 같은 런타임 데이터까지 안전하게 다루려면 그 이상의 대응이 필요합니다.
Zod는 단순한 유효성 검증 도구를 넘어, 다음과 같은 프론트엔드 개발 전반의 신뢰성을 높이는 역할을 합니다.
safeParse, format() 등을 통한 UI와 자연스럽게 연동되는 에러 처리.transform(), .merge(), .partial() 등으로 복잡한 폼 스키마도 선언적으로 구성React Hook Form과의 결합으로 실제 폼 입력부터 전송 직전까지 일관된 검증 로직을 유지할 수 있었고,
이전에는 수작업 수정이 필요했던 입력 오류들을 사용자 수준에서 예방할 수 있게 되었습니다.
이번 개선을 통해 단순히 오류를 줄이는 데 그치지 않고,
"검증 로직은 선언형으로 구성되고 UI 로직과 명확히 분리돼야 유지보수가 쉬워진다"는 점을 실감할 수 있었습니다.
Zod는 지금 이 순간에도 업데이트가 활발하고, 타입 안정성 기반의 프론트엔드 유효성 검증을 고민하는 팀이라면 충분히 도입할 만한 가치가 있는 도구입니다.