Zod로 TypeScript 런타임 유효성 검사하기
문제
TypeScript 타입은 컴파일 타임에만 존재한다. 런타임에서는 완전히 사라진다.
type User = {
id: number;
name: string;
email: string;
};
// API에서 받은 데이터가 정말 User 타입인지?
const res = await fetch("/api/user/1");
const user: User = await res.json(); // 타입 캐스팅일 뿐, 실제 검증은 없다
서버에서 { id: "abc", name: null } 같은 데이터가 오면 그냥 통과돼서 한참 뒤에 UI에서 터진다. 디버깅할 때 원인 찾기가 지옥인 거다.
해결
Zod를 쓰면 스키마 하나로 타입 정의 + 런타임 검증을 동시에 할 수 있다.
npm install zod
import { z } from "zod";
// 스키마 정의 = 타입 정의 + 런타임 검증 규칙
const UserSchema = z.object({
id: z.number(),
name: z.string().min(1),
email: z.string().email(),
});
// 스키마에서 타입 자동 추출
type User = z.infer<typeof UserSchema>;
// API 응답 검증
const res = await fetch("/api/user/1");
const data = await res.json();
const user = UserSchema.parse(data); // 잘못된 데이터면 여기서 바로 에러!
에러를 던지지 않고 처리하고 싶으면 safeParse를 쓴다.
const result = UserSchema.safeParse(data);
if (!result.success) {
console.error(result.error.flatten());
// { fieldErrors: { email: ["Invalid email"] } }
return;
}
// result.data는 검증된 User 타입
console.log(result.data.name);
폼 유효성 검사에도 바로 쓸 수 있다.
const SignupSchema = z.object({
username: z.string().min(3, "3자 이상 입력"),
password: z.string().min(8, "8자 이상 입력"),
confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
message: "비밀번호가 일치하지 않습니다",
path: ["confirmPassword"],
});
핵심 포인트
- TypeScript 타입은 런타임에서 사라지니까, 외부 데이터는 반드시 런타임 검증이 필요하다
z.infer로 스키마에서 타입을 추출하면 타입 정의 중복을 없앨 수 있다parse는 에러를 던지고,safeParse는 Result 타입을 반환한다- API 응답, 폼 입력, 환경변수 등 외부에서 들어오는 데이터에 적극 활용하면 된다