Zod로 유효성 검사 구현에 생산성을 더해보아요
TypeScript를 쓰는데 별도의 데이터 유효성 검사 라이브러리가 필요한 이유
TypeScript는 컴파일 타임에 타입을 검사하지만, 런타임에 데이터 유효성을 검사하지 않습니다.
TypeScript는 정적 타입 검사를 합니다. 코드 작성 시 및 컴파일 시점까지 변수, 함수 등의 타입들에 대한 검사를 진행합니다. 다만, 타입 검사와 타입 추론은(타입 자동완성) 우리가 TypeScript로 코드를 작성하고 컴파일하는 시점까지만 누릴 수 있습니다.
⇒ 런타임에는 TypeScript를 JavaScript로 변환한 코드가 실행되기에 TypeScript정보는 사라지기 때문이죠. 실제로 build 후 배포를 위한 dist폴더에는 js파일만 있는 것 기억하시죠…
⇒ 이로 인해 런타임에서 발생하는 데이터들에 대한 유효성 검사가 필요합니다.
*런타임 데이터: API 응답, 사용자 입력, API의 응답값등 외부에서 들어오는 데이터는 런타임에 처리되며 타입스크립트는 이에 대한 검증을 제공하지 않습니다.
런타임 데이터에 대한 유효성 검사를 구현하는데 생산성을 높여주는 라이브러리
⇒
Yup
/ Zod
/ joi
/ Avj
라이브러리 왜 써요?
⇒ 유효성 검사 기능 간결하게 구현 + 에러 처리 용이 + 스키마 정의 후 별도의 인터페이스 생성 없이 재활용 가능
joi와 Avj는 우선 건너뜁시다
joi - 정적 타입 추론을 지원하지 않아 제외 (Node.js에서 잘 쓰임.)
Avj - 가장 오래되긴 했습니다. (2015 / 올해 릴리즈 버전은 8까지 있음.) 다만 이 친구는 ts 지원 안되고, 특히 호환성과 의존성 문제들이 있을 수 있다고 합니다. 해당 라이브러리를 사용한 오래된 서비스들이 있다보니 계속 유지되고 있는것으로 보입니다.
Yup과 Zod
Yup
- 스키마 정의가 유연하다
- Formik과 함께 많이 쓰는듯 - Formik공식문서에서 추천중 ‣
- 역사가 깊다 - yup은 Jun 25, 2016 / zod는 Apr 5, 2020
- repository star 22.7k
Zod
- TS와 강력한 호환성 제공 - 엄격한 정적 타입 검사 제공
- 의존성이 없다 ⇒ 경량이다
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 스타일이 Yup에 비해 간결한편
- React-hook-form과 조합해 자주 쓰임
- release가 잦으며 업데이트 + 관리가 잘 되고있다. 현재 최신 version v3.23.8
- 구문이 더 간결하다 → 복잡한 validation구현 시 yup에 비해 간결한 구문으로 구현이 가능하다
- 검사에 더해 데이터 형변환도 지원
- repository star 33k
Zod를 선택한 이유
Yup은 문자형 데이터에 대한 숫자 검증을 엄격하게 하지 않음. 마치 암묵적 타입 변환을 하는 JS 같네요.
const testSchema = yup .string() .validate(333) .then((res) => console.log(res)); // string 자료형을 예상했는데 333을 넣어 확인했을때 엄격하게 자료형을 검사하지 않음. // 반환값은 333 const testSchema = yup .string() .isValid(11) .then((res) => console.log(res)); // true를 반환
이에 대해 엄격히 검사하려면 strict API를 함께 사용해줘야함.
const testSchema = yup .string() .strict() .validate(333) .then((res) => console.log(res)); // 333을 반환하지 않고 끝남 const testSchema = yup .string() .strict() .isValid(11) .then((res) => console.log(res)); //false를 반환
이처럼 Zod보다 엄격하지 않고 저 then체이닝도 맘에 안들었습니다.
⇒ 문법 간결함이 Zod에 비해 떨어진다고 느꼈습니다.
이 라이브러리를 쓰지 않는다면 유효성 검사 함수를 개별 구현하시면 됩니다
저도 라이브러리 적용 전엔 아래 같은 함수들을 하나하나 구현했습니다.
const emailValidation = (email: string) => { const emailValidation = /^[0-9a-zA-Z]([-_\.]?[0-9a-zA-Z])*[-_\.]?@[0-9a-zA-Z]([-_\.]?[0-9a-zA-Z])*\.[a-zA-Z]{1,6}$/i; if (email === '' || !emailValidation.test(email)) { return false; } return true; };
비밀번호의 경우라면 비밀번호 정책에 따른 validation에 비밀번호 입력 / 비밀번호 확인 Input에 따라 별도의 함수를 만들고… x100
zod를 사용하면
parse 사용 예시
import { z } from 'zod'; const emailValidationSchema = z.string().email({ message: "Invalid email address" }); const emailValidation = (email: string) => { try { emailValidationSchema.parse(email); return true; } catch (e) { return false; } };
safeParse 사용 예시
const result = emailValidationSchema.safeParse(email); if(!result.success){ // 유효성 검사 실패에 대한 처리 return; }
parse와 safeParse의 차이점
- parse: 유효하지 않은 입력에 대해 예외를 던집니다.
- safeParse: 유효하지 않은 입력에 대해 예외를 던지지 않고, 결과를 객체 형태로 반환합니다.
이 두 API는 상황에 따라 다르게 사용할 것 같은데 현재 프로젝트의 경우 api 호출 전 폼에 대한 입력값 검사들을 진행했기에 safeParse를 주로 사용했습니다.