Dev Camp : zod (6)

lunaxislu·2024년 3월 19일

dev-camp

목록 보기
5/5

저번시간에 zod를 통해서 parse, safeParse를 통해서 검증하는 방법과 에러 handle에 관해 약간 알아봤는데 이번에는 zod에 대해서 깊이 파보자.

zod type

zod에서 타입지정은 어떻게 할까??

zod에서 제공하는 z.infer를 통해 스키마(데이터)의 타입을 추출하여,
타입을 지정할 수 있다.

  const schema = z.string();
  type StringSchema = z.infer<typeof schema>; // StringSchema = string

응용해서

타입을 추출하여 타입을 지정할 수가 있는데 react-hook-form에서 각 field를 객체형태로 데이터를 제한 후 zod의 infer를 통해 타입 지정까지 할수있다.

import { useForm } from 'react-hook-form';
import { z } from "zod";

// schema 정의
const schema = z.object({
  name: z.string(),
  createdAt: z.date(),
  age: z.number(),
  email: z.string().email(),
});

// schema의 타입 지정
type TSchema = z.infer<typeof schema>;
  /* 
  type TSchema = {
    name: string;
    createdAt: Date;
    age: number;
    email: string;
  }
 */

// 호출된 react-hook-form의 타입지정
const form = useForm<TSchema>()

위의 form의 타입 을 마우스 호버하여 보면

const form: UseFormReturn<{
    name: string;
    createdAt: Date;
    age: number;
    email: string;
}, any, undefined>

이렇게 나온다. 이렇게 react-hook-form과 같이 어떤 형식과 규정이 있어야하는 라이브러리와 찰떡이다.


스키마(데이터) 정의는 string, number, boolean 등이 있겠지만,

객체로서 데이터는 보통 정의했던것 같다.( 내 경험상...)

객체로 데이터를 정의한다면, 그 세부 속성에 관한 검증함수를 통해 객체형태의 스키마를 정의 할수 있다.

로그인 회원가입에 관련하여 객체 스키마를 정의하며 예시를 통해 알아가보자

zod-자료형

import { z } from 'zod';

const user = z.object({
  name: z.string().length(6, { message: "plz fit 6" }),
  age: z.number().min(1,{message:'plz min 1'}).max(3,{message:'plz max 3'}), 
  email: z.string().email(),
  active: z.boolean(),
  createdAt: z.date(),
});

z.object() 를 사용해서 로그인|| 회원가입의 user의 스키마를 정의하고,

email은 당연히 문자열이고 그 하위 개념이므로 위 코드처럼 작성, 날짜, 활동중인지에 관련하여 각각의 맞는 타입으로 정의가 쉽고 간편하다.

위의 user의 타입을 추출해보면

type User = z.infer<typeof user>
// { name : string, age : number, email : string, active : boolean, createdAt : Date }

이렇게 타입 지정을 따로 할 필요없이 저렇게 추출하여 타입을 하나 만들 수가 있다.

물론 타입 은 단순히 string , number ,... 이지만 걱정마라,

zod가 알아서 타입과 제한한 규정까지 처리해서 에러를 뽐뿌해 줄 테니..

필수/선택

그런데 말입니다.

사실 조건부로 작성해도 되는것이 있고, 반드시 필수로 작성해야 할 일이 많다.

이렇게 필수로/ 선택적으로 쓰고 싶을 때는 어떻게 해야 할까??

필수는 당연히 위의 코드들 처럼 작성하면 되지만, active 속성을 선택적으로 입력하도록 변경해보자

const user = z.object({
  //....	
  active: z.boolean().optional(),
  createdAt: z.date(),
});

이렇게 맨 뒤에 optional() 을 붙이고 타입을 추론해보면

type User = z.infer<typeof user>
// { ... , active?:boolean | undefined }

이렇게 조건부로 선택 할수 있다.

Default Value

최초 회원가입시 값이 누락되어 있는 속성에 기본값을 주고 싶다면 default() 검증자를 사용할 수 있다.

const schema_1 = z.object({
  name:z.string(),
  active: z.boolean().default(false)
})

const schema_2 = z.object({
  name: z.string().default('default Value'),
  age: z.number()
})

이렇게 default 검증자에 값을 넣어주고 safeParse의 인자 값으로 검증을 해보자

const test1 = { name:'user입니다.' }
const test2 = { age : 20 }

schema_1.safeParse(test1)
schema_2.safeParse(test2)
{ success : true, data : { name:'user입니다.' , active:false } }
{ success : true, data : { name:'default Value' , age :20 } }

이렇게 검증하려는 자료형 객체에 속성을 추가하지 않아도 default 검증자로 인해 default 값이 들어있고, 검증이 성공했다는 객체 값을 반환한다.

배열(array)

배열이 없으면 섭하쥐~

Zod로 배열 스키마를 정의 할 수 있는 방법이 2가지 있다.

  • 타입을 명시 후 .array() 를 뒤에 붙여줄수도 있고
  • z.string() 안에 타입을 인자로 넘길 수 있다.

잠깐, 우리가 자바스크립트에서 배열, 객체를 선언 하는 방법도 2가지임. 혹시 와닿지 않을까봐 적는다.

const arr = new Array(1,2,3,4);
const arr2 = [1,2,3,4]

다시 돌아와서 코드로 보면

const arr = z.string().array(); // first
const arr2 = z.array(z.string()); // second

위의 스키마의 타입을 추출해보면 string[] 이 나온다.

type Arr = z.infer<typeof arr>
type Arr2 = z.infer<typeof arr2>
// type Arr && Arr2 = string[]

Record

타입스크립트의 record 유틸리티 타입으로 객체의 key , value의 타입을 지정이 가능한 것처럼

Zod에서도 record를 통해 key 또는 value의 값의 타입을 제한할 수 있다.

예컨대, 값으로 숫자만 사용할 수 있는 객체에 대한 스키마를 정의한다면

const mustNumber = z.record(z.number()); // 반드시 zod로 정의한 타입이 들어가야 한다.

mustNumber.parse({ a : 1 , b : 2}) // pass
mustNumber.parse({ c : 3 , d : '4'}) // fails

이렇게 숫자만으로 가능한 객체 검증자를 만들수 있다.

타입을 추출하여 보면

const mustNumber = z.record(z.number());

type T = z.infer<mustNumber>
// { [x : string] : number }

이렇게 나온다.


그 밖에도 key 값에 대하여도 타입을 제한 하고 싶다면, 2개의 인자를 넣어주면 된다.

첫번째 인자는 key값에 대한 타입정의( 반드시 zod여야함!!),
두번째 인자는 value 값에 대한 타입정의 이다.

const literalType = z.literal("record");
const test = z.record(literalType, z.number().or(z.string()));

type D = z.infer<typeof test>;
//type D = { record?: string | number | undefined }

enum

제한된 값 중에서 하나를 사용하도록 스키마를 정의하려면 이럴 때는 z.enum() 검증자를 사용해서 사용 가능한 값을 나열!! 해주면 된다.

예를 들어, junior , senior , leader 로 나눠진 스키마를 작성해보면

const literal = ['junior','senior','leader']
const grade = z.enum(literal);

grade는 이렇게 유니온 타입으로 나타나고 3가지 값 이외에는 유효성 검증이 실패한다.

그리고 grade 스키마로 부터 타입을 추출해보면 유니온 타입이 나오는 것이 확인이 된다.

grade.parse('junior'); // pass
grade.parse('senior'); // pass
grade.parse('leader'); // pass

grade.parse('enum'); // faile...

type GradeEnum = z.infer<typeof grade>
// type GradeEnum = 'junior' | 'senior' | 'leader' ; 

문자열 formatting

내가 경험했던 프로젝트에서는 문자열과 관련된 검증을 위해서는 string만으로는 적절치 못했다

이메일, 이름,...

이를 위해 zod에서는 string() 말고도 그 하위의 필요한 검증자들을 지원한다 .

const user = z.object({
  email:z.string().email(),
  url : z.string().url(),
  //...
})

다행히도 string() 다음에 -> .email() , .url() 와 같은 검증자를 추가하여 특정 포맷에 맞는 값만 유효성 검증에 통과할 수 있게 도와준다.

가장 중요한것은

스키마의 타입은??

여전히 타입은 string이다라는 것

다시 말해서 타입스크립트로 코드를 작성할 때는 여전히 일반 string을 사용하여 컴파일시에도 스키마에서 정의한 수준의 타입 검사는 일어나지 않는다. 엄격한 유효성 검증은 순수하게 실행 "시점"에서만 일어난다.

숫자형 지정

자바스크립트에서는 정수, 실수가 구분이 되지않아 z.number() 만으로는 한계가 있는데,

zod 에서는 이러한 숫자라는 general에서 specific하게 더욱 제한 할 수 있다.

const age = z.number().int()

age.parse(31) // pass
age.parse(31.33) // fails...

범위 제한

스키마에서는 타입과 타입관련하여 범위를 지정해 줄 수 있다.

const name = z.string().min(1).max(5);
const emptyString = ''
const NAME='CHOI'

name.parse(NAME) // pass
name.parse(emptyString) // fail...

string의 하위에서 범위를 줄수도 있고

const age = z.number().min(1).max(3)

age.parse(1) // pass
age.parse(120) // pass

// 아니면 이상 이하로 주고 싶다면

const rangeNumber = z.number().gte(1).lte(100);// 1이상 100이하
					
rangeNumber.parse(1) //pass
rangeNumber.parse(100) // pass

rangeNumber.parse(101) // fails..

//cf
const rangeNumber = z.number().gt(1).lt(100)// 1초과 100미만

ETC

그 밖에도 file 같은 유효성 검사는 어떻게 할까??
핸드폰 입력란에서 첫 3글자는 010으로 시작해야하는데 이런 유효성 검사는 어떻게 할까?? 생각 할 수있다.

그래서 조금더 알아보고자 한다.


instanceof

공식홈페이지에서 찾아 봤을 때 file과 관련된 검증자는 없는 것 같았다.

그래서 생각 할 수 있는 것이 instanceof 검증자 이다.



const t = z.instanceof(File).or(z.instanceof(FileList)); 
const handler = (e: ChangeEvent<HTMLInputElement>) => {
  const target = e.target.files; // e.target.files[0] 면 File타입
  t.parse(target); // pass  
};

<input type="file" onChange={handler} />

이렇게 상속으로 검증도 할 수 있다.

refine

"there are many so-called "refinement types" you may wish to check for that can't be represented in TypeScript's type system."

you can define a custom validation check on any Zod schema with .refine

공식문서에서 발췌하였다.

비밀번호는 특수문자 !@#$를 반드시 포함하게 하고 싶거나,

핸드폰 번호는 010 으로 시작하는지 검사하고 싶을 때 새롭게 custom validation을 할 수 있는 메소드가 있다. refine 이라는 메소드인데

const schema = z.string().refine( val=>console.log(val) ) // val는 검사하는 값이다. 

refine의 인자값은 2개가 들어갈 수 있는데

  • 첫번째는 유효성 검사하는 함수가 들어간다.
  • 두번째로는 RefineParams라는 객체의 options가 들어갈 "수" 있다.

공식문서에서는 2번째 인자값의 타입을 보여줬는데

type RefineParams = {
  // override error message
  message?: string;

  // appended to error path
  path?: (string | number)[];

  // params object you can use to customize message
  // in error map
  params?: object; 
  // 에러시 params안 객체안에 내가 원하는걸 마구잡이로 넣어두 된다. ex) state 값이라던지 등 
};

이렇게 명시되어있다.

경우에 따라서 첫 번째 인자 값으로 유효성 함수로 커스텀 검증을 한 이후 error시 message등을 커스텀 할 수 있다.

좀더 2번 째 인자값을 advanced 하게 사용하고 싶다면 함수형태 로 넣을 수 있다.

/*
For advanced cases, 
the second argument can also be a function that returns RefineParams.
*/
const longString = z.string().refine(
  (val) => val.length > 10,
  (val) => ({ message: `${val} is not more than 10 characters` })
  // 함수형태로 사용가능하며, 반환 값은 위의 타입처럼 RefineParams다.
);

이렇게 간단하게 알아봤는데 한번 실용적인 코드를 봐야지

const phoneRegex = /^010\d{8}$/;
const phoneSchema = z
    .string()
    .min(11, "연락처는 11자리여야 합니다.") // 이렇게 메세지 커스텀을 바로 넣어줄수 있다.
    .max(11, "연락처는 11자리여야 합니다.")
    .refine(
      (value) => phoneRegex.test(value),
      "010으로 시작하는 11자리 숫자를 입력해주세요" 
      // 이렇게 메세지 커스텀을 바로 넣어줄수 있다.
    );

//-----------------------------------
  
const passwordRegex =
  /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
const user = z.object({
      password : z
      .string()
      .min(6, "비밀번호는 최소 6자리 이상이어야 합니다.")
      .max(100, "비밀번호는 100자리 이하이어야 합니다.")
      .refine(
        (value) => passwordRegex.test(value),
        "비밀번호는 최소 6자리 이상, 영문, 숫자, 특수문자를 포함해야 합니다."
      ),
      confirmPassword : z
      .string()
      .min(6, "비밀번호는 최소 6자리 이상이어야 합니다.")
      .max(100, "비밀번호는 100자리 이하이어야 합니다.")
      .refine(
        (value) => passwordRegex.test(value),
        "비밀번호는 최소 6자리 이상, 영문, 숫자, 특수문자를 포함해야 합니다."
      )
    })
	.refine(val=>val.confirmPassword === val.password,'비밀번호가 일치 하지 않습니다.')

마치며

이렇게 typescript의 한계를 zod에서는 여러가지를 조합하여 더욱 강력하게 runtime에서까지 엄격히, 대단히 검사할 수가 있는 것을 알아봤다.

이젠 react-hook-form을 천천히 음미해 보고 그다음 zod와의 결합으로 간단하고 편리하게 한번 로그인, 회원가입을 구현해보자

0개의 댓글