Home Zod
Post
X

Zod

목표

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는 타입 안전하게 사용 가능
}
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.