목표
zod를 사용해 validation을 구현해보자.
Zod
TypeScript/JavaScript용 데이터 검증 및 타입 안전성 라이브러리를 말합니다.
즉, 런타임에 데이터의 형태를 검사하고, 동시에 TypeScript 타입을 자동으로 만들어주는 도구
1
npm install zod
왜 필요한가?
TypeScript는 컴파일 시점에 타입을 검사하기 때문에 서버나 클라이언트가 실제로 받는 런타임 데이터(예: API 응답, 폼 입력)는 타입이 잡히지 않음.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Typescript
interface User {
name: string;
age: number;
}
const user: User = JSON.parse('{ "name": "Alice" }');
// 타입은 맞다고 생각하지만, 런타임에서 오류 발생 가능 (age 없음)
// Zod 사용
import { z } from "zod";
const UserSchema = z.object({
name: z.string(),
age: z.number(),
});
UserSchema.parse({ name: "Alice" });
// ❌ ZodError: Required field 'age' is missing
즉, Typescript의 정적 타입 안전성을 런타임 검증(runtime validation) 으로 확장
타입 자동 추론(z.infer)
Zod는 TypeScript와 완벽히 통합되어 있습니다.
“스키마 = 타입”
1
type User = z.infer<typeof UserSchema>;
User 타입은 UserSchema에 따라 자동으로 생성되므로 별도의 interface 선언이 필요 없습니다.
기본 사용법
Zod는 z.object()로 객체 스키마를 선언하고 .parse() 또는 .safeParse()로 데이터를 검증합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { z } from "zod";
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1),
age: z.number().int().optional(),
});
const input = { id: "invalid-uuid", name: "Kim" };
// parse: 예외 발생
try {
const user = UserSchema.parse(input);
} catch (e) {
console.error(e.errors); // ZodError
}
// safeParse: 예외 대신 결과 객체 반환
const result = UserSchema.safeParse(input);
if (!result.success) {
console.log(result.error.format());
} else {
console.log(result.data);
}
선언 단계 (스키마 문법)
기본 문법 — 원시 타입
1
2
3
4
5
6
7
8
9
import { z } from "zod";
z.string();
z.number();
z.boolean();
z.null();
z.undefined();
z.bigint();
z.symbol(); // (주의: symbol 관련 동작은 제한적일 수 있음)
문자열/숫자 · 자주 쓰는 메서드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 문자열
z.string(); // 기본
z.string().min(1); // 최소 길이
z.string().max(100); // 최대 길이
z.string().email(); // 이메일 포맷 검사
z.string().uuid(); // UUID 형식
z.string().regex(/^\d+$/); // 정규식 체크
z.string().nonempty(); // 빈문자열 금지 (v3엔 .nonempty())
// 숫자
z.number()
.int() // 정수
.positive() // > 0
.nonnegative() // >= 0
.min(1)
.max(100);
배열 · 튜플
1
2
3
4
5
z.array(z.string()); // 문자열 배열
z.array(z.number()).nonempty(); // 최소 1개
// tuple
z.tuple([z.string(), z.number()]);
객체 스키마와 유틸 (partial, pick, omit, extend 등)
1
2
3
4
5
6
7
8
9
10
11
12
13
const User = z.object({
id: z.string().uuid(),
name: z.string(),
age: z.number().optional(),
});
// 부분 스키마
const PartialUser = User.partial(); // 모든 필드 optional
const PublicUser = User.omit({ age: true }); // age 제거
const SmallUser = User.pick({ id: true, name: true });
// 확장
const ExtendedUser = User.extend({ role: z.enum(["user", "admin"]) });
enum / nativeEnum / record / map
1
2
3
4
5
6
7
8
9
10
11
12
// 문자열 enum
const E = z.enum(["red", "green", "blue"]);
// native typescript enum
enum Color {
Red = "red",
Green = "green",
}
const Native = z.nativeEnum(Color);
// record: (v3에서는 단일 인자 사용 가능)
const R = z.record(z.string()); // { [key: string]: string }
커스텀 검증 — refine / superRefine
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 단일 값 검증
const Positive = z
.number()
.refine((n) => n > 0, { message: "양수여야 합니다." });
//다중 필드(객체) 검증 — superRefine
const Range = z
.object({ min: z.number(), max: z.number() })
.superRefine((obj, ctx) => {
if (obj.min > obj.max) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "min은 max보다 작아야 합니다.",
});
}
});
superRefine는 복합 검증(교차 필드 규칙)에 적합합니다.
검증 단계
parse()
입력값을 스키마에 맞게 검증합니다.
- 검증 실패 시 예외(ZodError)를 던집니다.
- 검증 성공 시, 입력값 그대로 반환
- 동기적 코드 : try/catch 사용 가능할 때
- 코드가 간결, 성공 시 바로 타입 보장
- 예외 처리 필요, 서버 API에서 바로 쓰면 crash 가능
즉, 단순 검사용이나 내부 검증용으로 좋고, API 엔드포인트에서는 안전하지 않을 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
import { z } from "zod";
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1),
});
try {
const user = UserSchema.parse({ id: "invalid-uuid", name: "Kim" });
console.log(user);
} catch (err) {
console.error(err.errors); // ZodError: 문제 있는 필드 정보 출력
}
safeParse()
입력값을 스키마에 맞게 검증합니다.
실패 시 예외를 던지지 않고 결과 객체를 반환합니다.
- API, 폼 검증, 서버 응답, try/catch 없이 안전하게 사용 가능
- 안전, 서버/클라이언트 어디서든 사용 가능
- 체인 형식으로 바로 접근 불가(성공 여부 먼저 확인 필요)
실무에서는 API 엔드포인트, 폼 입력 검증, SSR/CSR 경계에서 주로 사용합니다.
1
2
3
4
5
6
7
const result = UserSchema.safeParse({ id: "invalid-uuid", name: "Kim" });
if (!result.success) {
console.log(result.error.format()); // 각 필드별 에러 메시지
} else {
const user = result.data; // 타입 추론 보장
}
safeParse() 결과 구조
1
2
3
4
5
6
7
8
9
10
11
// 성공 케이스
{
success: true;
data: T; // 스키마에 맞는 값, 타입이 좁혀짐
}
// 실패 케이스
{
success: false;
error: ZodError; // 오류 상세 정보
}
success (boolean) : true → 검증 성공 / false → 검증 실패
data (T | undefined) : 검증 성공 시 존재 / 실패 시 undefined
error (ZodError | undefined) : 검증 실패 시 존재 / 성공 시 undefined
즉, result는 성공 여부에 따라 data 또는 error 중 하나만 존재하는 구조입니다.
ZodError
ZodError 객체의 주요 속성:
issues: 문제 있는 필드 목록 배열, 각 요소는{ path, message, code, ... }format(): 필드별 에러 메시지를 계층적으로 포맷flatten(): formState에 바로 바인딩 가능한 단순 구조 반환errors: issues 배열의 원본
success 체크 → data/error 접근 → 타입 안전성 확보
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const error = result.error;
console.log(error.issues);
// [
// { path: ["id"], message: "Invalid uuid", code: "invalid_string" },
// { path: ["name"], message: "String must contain at least 1 character(s)", code: "too_small" }
// ]
// 서버 API
const result = UserSchema.safeParse(req.body);
if (!result.success) return res.status(400).json(result.error.format());
// 폼 검증
const result = LoginSchema.safeParse(formData);
if (!result.success) {
setFormErrors(result.error.flatten().fieldErrors);
}
// 데이터 후처리
if (result.success) {
const user = result.data;
// user는 타입 안전하게 사용 가능
}