Zod로 유효성 검사 구현에 생산성을 더해보아요

date
Aug 27, 2024
slug
zod
author
status
Public
tags
Etc
summary
type
Post
thumbnail
milad-fakurian-0uUzrqDeBNY-unsplash.jpg
category
updatedAt
Aug 3, 2025 02:07 PM
 

TypeScript를 사용하는데 왜 별도의 데이터 유효성 검사 라이브러리가 필요할까요?

TypeScript의 한계: 정적 타입 검사만 제공

TypeScript는 정적 타입 시스템을 제공합니다. 즉, 코드를 작성하고 컴파일하는 시점에 타입 오류를 잡아낼 수는 있지만, 실제 코드가 브라우저나 서버에서 실행되는 런타임에서는 TypeScript의 타입 정보가 완전히 사라집니다.
컴파일 후 dist/ 폴더에 생성되는 파일은 전부 .js 확장자이며, 타입 정보는 포함되어 있지 않습니다.
 

💥 문제는 런타임에 발생합니다

API 응답, 사용자 입력 등 외부에서 유입되는 데이터는 런타임에 들어오며, 이에 대해 TypeScript는 아무런 보장도 하지 않습니다.
  • API가 예기치 않게 스펙을 바꾸었을 때
  • 사용자가 예상치 못한 입력을 했을 때
  • 타 시스템에서 전송된 데이터가 손상되었을 때
이러한 경우 런타임 오류가 발생하고, 이는 운영상의 장애로 이어질 수 있습니다.
 

그래서 필요한 것이 런타임 유효성 검사

런타임 데이터를 검사하려면 아래 중 하나가 필요합니다.
  1. 직접 검증 함수 구현
  1. 유효성 검사 라이브러리 사용
 

런타임 데이터에 대한 유효성 검사를 구현하는데 생산성을 높여주는 라이브러리들

Yup/ Zod/ joi / Avj
이러한 라이브러리를 쓰면 어떠한 장점이 있나요?
⇒ 유효성 검사 기능 간결하게 구현 + 에러 처리 용이 + 스키마 정의 후 별도의 인터페이스 생성 없이 재활용 가능
 

joi와 Avj는 우선 건너뜁시다

joi - 정적 타입 추론을 지원하지 않아 제외 (Node.js에서 잘 쓰임.)
Avj - 가장 오래되긴 했습니다. (2015 / 올해 릴리즈 버전은 8까지 있음.) 다만 이 친구는 ts 지원 안되고, 특히 호환성과 의존성 문제들이 있을 수 있다고 합니다. 해당 라이브러리를 사용한 오래된 서비스들이 있다보니 계속 유지되고 있는것으로 보입니다.
 

라이브러리 비교: Yup vs Zod

Yup

  • 장점
    • 스키마 정의 유연함
    • Formik과의 높은 호환성 (공식 문서에서 추천)
    • 역사가 깊다 - yup은 Jun 25, 2016 / zod는 Apr 5, 2020
    • repository star 22.7k
  • 단점
    • 타입스크립트와의 정적 타입 추론 부족
    • then 기반의 체이닝 처리 → 코드 가독성 낮음
    • strict()을 명시하지 않으면 암묵적 타입 변환 허용
import * as yup from 'yup'; const schema = yup.string(); schema.isValid(123).then(console.log); // ✅ true (숫자도 통과)
 
Zod
  • 장점
    • TS와 완벽한 통합 (타입 추론 가능)
      • notion image
    • 의존성 없음 (경량)
      • package.lock.json을 보면 dependencies필드가 없는 것을 볼 수 있음.
        //package.lock.json "node_modules/zod": { "version": "3.23.8", "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", "funding": { "url": "https://github.com/sponsors/colinhacks" } }
    • API 구문이 간결하고 직관적
    • parse, safeParse API 제공 (에러 핸들링 분리)
    • React Hook Form과의 강력한 조합
    • release가 잦으며 업데이트 + 관리가 잘 되고있다. 현재 최신 version v3.23.8
    • 검사에 더해 데이터 형변환도 지원
    • repository star 33k
  • 단점
    • 스키마 정의 유연성은 Yup보다 약간 떨어질 수 있음
 

Zod를 선택한 이유

Yup은 아래 예시처럼 기본적으로 타입에 관대한 편입니다.
yup.string().isValid(333).then(console.log); // true
strict() 옵션을 사용하면 해결되긴 하지만, 이걸 매번 명시해야 한다는 점에서 실수의 여지가 큽니다.
반면 Zod는 기본적으로 엄격한 타입 검사를 적용합니다. 실수로 숫자를 문자열로 받는 일을 미연에 방지할 수 있죠.
 

직접 검증 함수 구현하기 vs Zod

저도 라이브러리 적용 전엔 아래 같은 함수들을 하나하나 구현했습니다.
const emailValidation = (email: string) => { const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return regex.test(email); };
→ 이 방식은 검증 로직 재사용성 부족, 오류 메시지 일관성 문제, 테스트 및 유지보수 비용 증가 등의 문제가 있습니다.
비밀번호의 경우라면 비밀번호 정책에 따른 validation에 비밀번호 입력 / 비밀번호 확인 Input에 따라 별도의 함수를 만들고… x100
 

Zod로 구현한 예

import { z } from 'zod'; const emailSchema = z.string().email({ message: "이메일 형식이 아닙니다." }); const result = emailSchema.safeParse(userInput); if (!result.success) { console.error(result.error.format()); }
  • emailSchema를 타입으로도 바로 사용할 수 있어 스키마와 타입 정의 일치
  • safeParse를 사용해 런타임에서도 안전하게 검사 및 에러 핸들링 가능
 
 

Zod의 parse vs safeParse 언제, 왜 사용해야 할까?

Zod에서 제공하는 parsesafeParse는 모두 스키마 기반의 유효성 검사를 수행하지만, 오류를 처리하는 방식이 다릅니다. 이를 통해 예외 기반 처리조건 분기 기반 처리를 명확히 구분할 수 있습니다.
API
예외 발생
반환 타입
사용 시점
parse()
❗ 예외 발생
값 or throw
데이터가 반드시 올바르다고 가정할 때
safeParse()
❌ 예외 없음
{ success: boolean, data?, error? }
사용자 입력 등 오류 발생 가능성이 있는 경우

  • parse()테스트 코드, seed 데이터, 내부 시스템 간 통신에서 주로 사용
  • safeParse()사용자 입력 폼, 외부 API 응답, 클라이언트-서버 통신의 입력 처리안전성이 필요한 영역에서 사용

코드 예시 비교

// 예외 발생 → try/catch 필요 try { const value = z.string().email().parse("not-an-email"); } catch (e) { console.error("유효하지 않은 이메일:", e); }
// 조건 분기 → 흐름 제어가 깔끔함 const result = z.string().email().safeParse("not-an-email"); if (!result.success) { console.error("입력 오류:", result.error.format());
 

타입 따로 선언할 필요 없음! Zod의 타입 추론 활용

Zod의 강력한 장점은 타입 정의와 검증 스키마가 동일 소스로부터 유도된다는 것입니다. 이로 인해 타입 불일치로 인한 런타임 오류가 줄어듭니다.
const userSchema = z.object({ id: z.number(), name: z.string(), email: z.string().email(), }); // 타입 자동 추론 type User = z.infer<typeof userSchema>;
이렇게 하면 API 응답 타입을 별도 선언할 필요 없이 userSchema만 잘 작성하면 됩니다.
 

Zod 에러 메시지 커스터마이징과 format() 활용

Zod는 유효성 실패 시 다양한 형태의 에러 메시지 커스터마이징이 가능하며, safeParse 결과의 error.format()폼과 UI에 에러 메시지를 표시할 때 매우 유용합니다.
const userSchema = z.object({ email: z.string().email({ message: '이메일 형식이 아닙니다.' }), password: z.string().min(8, { message: '비밀번호는 최소 8자 이상이어야 합니다.' }), }); const result = userSchema.safeParse(formData); // 결과가 실패했을 경우, 각 필드별 메시지를 구조화된 형태로 제공 if (!result.success) { const errors = result.error.format(); console.log(errors.email?._errors[0]); // "이메일 형식이 아닙니다." }
이 구조는 react-hook-form과 연동할 때도 그대로 쓸 수 있어 UX 개선에 도움이 됩니다.
 

입력값 가공(Transform)도 가능

Zod는 단순히 검증만 하는 것이 아니라, 데이터를 자동으로 가공(transform) 할 수도 있습니다. 예를 들어:
const schema = z.string().transform((val) => val.trim().toLowerCase()); const result = schema.safeParse(" ExAmPlE@Email.Com "); console.log(result.data); // "example@email.com"
  • 이를 통해 입력값 정규화(standardization) 를 쉽게 처리할 수 있어 API 요청 전 데이터를 항상 일관된 상태로 유지 가능
 

merge, extend, partial, pick, omit 같은 스키마 조작 API

Zod는 객체 스키마를 조작할 수 있는 다양한 메서드를 제공합니다. 이를 통해 공통 필드 추출, 일부 필드 제외, 부분 입력 허용 등이 가능합니다.
const baseUser = z.object({ name: z.string(), email: z.string().email(), }); const updateUser = baseUser.partial(); // 모든 필드를 선택적으로 바꿈 const createUser = baseUser.extend({ password: z.string().min(8), });
→ 유지보수성과 재사용성을 대폭 향상시킬 수 있습니다.
 

실무에서 어떻게 활용했을까?

제가 참여한 프로젝트에서는 자원 등록 폼의 입력 오류로 인해 여러 문제가 반복적으로 발생했습니다.
대표적인 사례는 다음과 같습니다.
  • 특정 필드에 대한 검증 정책이 변경되었지만, 일부 입력값이 여전히 잘못된 형식으로 서버에 전송됨
  • 필수 항목 누락 상태로 등록 시도가 이루어짐
  • 그 결과, 운영팀이 직접 데이터를 수정해야 하는 수작업 업무가 지속적으로 발생
초기에는 각 입력 필드마다 정규식과 조건문으로 직접 검증 로직을 작성했지만, 점점 케이스가 많아지면서 다음과 같은 어려움이 생겼습니다.
  • 검증 로직이 여러 곳에 중복되어 변경사항을 반영하기 어려움
  • 에러 메시지가 일관되지 않아 사용자 피드백이 불명확
  • 테스트와 유지보수에 시간이 점점 더 많이 소요됨
 

🛠 그래서 어떻게 개선했나?

이런 반복적인 문제를 해결하기 위해, Zodreact-hook-form을 기반으로 입력 검증 시스템을 리빌딩했습니다.
  • Zod로 각 입력값에 대한 스키마를 명확하게 정의
  • safeParse()를 사용해 검증 결과를 안전하게 분기 처리
  • 실패한 경우, error.format()을 활용해 필드별 에러 메시지를 구조화하여 UI에 표시
// 실제 코드는 아닙니다. const userSchema = z.object({ email: z.string().email({ message: '이메일 형식이 아닙니다.' }), password: z.string().min(8, { message: '비밀번호는 최소 8자 이상이어야 합니다.' }), }); const result = userSchema.safeParse(formData); if (!result.success) { const errors = result.error.format(); // errors.email._errors[0] 이런 식으로 UI에 표시 가능 }
이렇게 하니 다음과 같은 개선이 즉각적으로 체감되었습니다.
  • 모든 입력 검증 로직을 일원화 → 변경 시에도 한 곳만 수정하면 됨
  • 검증 실패 메시지를 UI에 정확하고 일관되게 출력
  • 클라이언트에서 잘못된 데이터가 아예 서버에 도달하지 않음 → 운영 리소스 절감
 

🔚 결론

TypeScript는 정적 타입 안정성을 제공하지만, 실제 사용자의 입력이나 외부 API 응답과 같은 런타임 데이터까지 안전하게 다루려면 그 이상의 대응이 필요합니다.
Zod는 단순한 유효성 검증 도구를 넘어, 다음과 같은 프론트엔드 개발 전반의 신뢰성을 높이는 역할을 합니다.
  • 타입스크립트와 완전히 통합된 스키마 정의
  • safeParse, format() 등을 통한 UI와 자연스럽게 연동되는 에러 처리
  • .transform(), .merge(), .partial() 등으로 복잡한 폼 스키마도 선언적으로 구성
  • API 요청 전 단계에서 데이터 정합성을 강제할 수 있는 구조
React Hook Form과의 결합으로 실제 폼 입력부터 전송 직전까지 일관된 검증 로직을 유지할 수 있었고,
이전에는 수작업 수정이 필요했던 입력 오류들을 사용자 수준에서 예방할 수 있게 되었습니다.
이번 개선을 통해 단순히 오류를 줄이는 데 그치지 않고,
"검증 로직은 선언형으로 구성되고 UI 로직과 명확히 분리돼야 유지보수가 쉬워진다"는 점을 실감할 수 있었습니다.
Zod는 지금 이 순간에도 업데이트가 활발하고, 타입 안정성 기반의 프론트엔드 유효성 검증을 고민하는 팀이라면 충분히 도입할 만한 가치가 있는 도구입니다.