Zod

seunghye jo·2023년 9월 6일
3

typescript

목록 보기
2/2
post-thumbnail

Zod

💡 **zod** : 스키마 선언 및 유효성 검사 라이브러리 💡 **zod를 사용하는 이유**

typescript의 유효성 검증은 컴파일 시에만 발생.

실제로 자바스크립트 프로그램이 실행 될 때는 아무런 역할을 하지 못함.

⇒ zod의 유효성 검증은 컴파일이 끝난 프로그램의 실행 시점에서 발생

typescript는 결국 예측 가능한 문제를 개발자에게 알려줌으로써 좀 더 견고한 코드를 짤 수 있도록 도와줌.

유효성 검증은 프로그램 실행 시점에서 최종 사용자를 위하여 동작.

요구사항 및 설치

  • 타입스크립트 4.5이상.
  • 타입스크립트 strict 모드 활성화.
    // tsconfig.json
    {
    	"compilerOptions": {
    		"strict" : true
    	}
    }
  • yarn add zod yarn add zod@canary

스키마 생성

기본 ⇒ z.{type}

import { z } from "zod";

// primitive values
z.string();
z.number();
z.bigint();
z.boolean();
z.date();
z.symbol();

// object
z.object({
  username: z.string()
});

// array
z.array(z.string())
z.string().array() // 💡BMS프로젝트 컨벤션!

// function
z.function();

// empty types
z.undefined();
z.null();
z.void(); // accepts undefined

// catch-all types
// allows any value
z.any();
z.unknown();

// never type
// allows no values
z.never();

타입 강제 ⇒ z.coerce.{type}

z.coerce.string(); // String(input)
z.coerce.number(); // Number(input)
z.coerce.boolean(); // Boolean(input)
z.coerce.bigint(); // BigInt(input)
z.coerce.date(); // new Date(input)

에러 메세지 지정

자료형 데이터 스키마 선언 시 에러 메세지 지정 가능.

const name = z.string({
  required_error: "Name is required",
  invalid_type_error: "Name must be a string",
});

const age = z.number({
  required_error: "Age is required",
  invalid_type_error: "Age must be a number",
});

const isActive = z.boolean({
  required_error: "isActive is required",
  invalid_type_error: "isActive must be a boolean",
});

const myDateSchema = z.date({
  required_error: "Please select a date and time",
  invalid_type_error: "That's not a date!",
});

const isNaN = z.nan({
  required_error: "isNaN is required",
  invalid_type_error: "isNaN must be not a number",
});

파싱

import { z } from "zod";

// 'string' 스키마 생성
const mySchema = z.string();

// 기본 파싱 => 타입이 일치하지 않으면 에러
mySchema.parse("tuna"); // => "tuna"
mySchema.parse(12); // => throws ZodError

// "safe"파싱 => 유효성 검사 실패 시에도 오류를 발생시키지 않음.
mySchema.safeParse("tuna"); // => { success: true; data: "tuna" }
mySchema.safeParse(12); // => { success: false; error: ZodError }
  • parse() : 스키마를 기준으로 데이터의 유효성 확인
  • parseAsync() : 비동기 정제를 사용하는 경우 parseAsync를 사용해야 함
  • safeParse() : 유효성 검사에 실패해도 오류를 던지지 않음.
  • safeParseAsync() : safeParse의 비동기버전. 편의상 .spa() 로도 사용 가능.

Validations

Strings validations

// validations
z.string().max(5);
z.string().min(5);
z.string().length(5);
z.string().email();
z.string().url();
z.string().emoji();
z.string().uuid();
z.string().cuid();
z.string().cuid2();
z.string().ulid();
z.string().regex(regex);
z.string().includes(string);
z.string().startsWith(string);
z.string().endsWith(string);
z.string().datetime(); // ISO 8601
z.string().ip(); // 기본값은 IPv4 및 IPv6

// transformations
z.string().trim();
z.string().toLowerCase();
z.string().toUpperCase();

Numbers validations

z.number().gt(5);
z.number().gte(5); // .min(5)
z.number().lt(5);
z.number().lte(5); // .max(5)

z.number().int(); // 정수

z.number().positive(); //     > 0 / 양수
z.number().nonnegative(); //  >= 0 
z.number().negative(); //     < 0 / 음수
z.number().nonpositive(); //  <= 0

z.number().multipleOf(5); // 5의 배수

z.number().finite(); // 유한한 수. 무한대의 값이면 안됨.
z.number().safe(); // javascript에서의 안전한 정수값 이내의 수

Date validations

z.date().min(new Date("1900-01-01"), { message: "Too old" });
z.date().max(new Date(), { message: "Too young!" });

validation + 에러메세지

z.string().email({ message: "Invalid email address" });
z.string().includes("tuna", { message: "Must include tuna" });
z.string().startsWith("https://", { message: "Must provide secure URL" });

z.number().lte(5, { message: "this👏is👏too👏big" });

타입 추출

z.infer를 활용하여 스키마의 추론타입 추출

const User = z.object({
  username: z.string(),
});

type User = z.infer<typeof User>; // { username: string }

아래와 같이 number와 string을 모두 허용하나, 출력을 할 때에는 string으로 변환하는 스키마 작성 시.
infer는 출력 자료형을 기준으로 타입 추론이 된다.

const ID = z
  .string()
  .or(z.number())
  .transform((id) => (typeof id === "number" ? String(id) : id));

type ID = z.infer<typeof ID>;
//   ^? type ID = string

입력 자료형을 기준으로 타입을 뽑아내고 싶다면 z.input 을 사용 (outputinfer와 동일)

type Input = z.input<typeof ID>;
//   ^? type Input = string | number
type Output = z.output<typeof ID>;
//   ^? type ID = string

위처럼 inputoutput 타입을 모두 뽑아내면 아래와 같이 유용하게 함수 타이핑이 가능

function processID(input: Input): Output {
  const output = ID.parse(input);
  // ID 처리 로직
  return output;
}

Method

default()

속성에 기본값을 부여.

const User = z.object({
  email: z.string(),
  active: z.boolean().default(false),
});

const user = User.parse({
  email: "user@test.com",
});

console.log(user);
/*
{
  email: 'user@test.com',
  active: false
}
*/

optional()

모든 스키마를 옵셔널한 값으로 만들 수 있다

const schema = z.optional(z.string());

schema.parse(undefined); // => undefined
type A = z.infer<typeof schema>; // string | undefined

object 내의 요소를 optional한 값으로 설정 가능

const user = z.object({
  username: z.string().optional(),
});
type C = z.infer<typeof user>; // { username?: string | undefined };

nullables()

널러블 타입 생성 가능.

const nullableString = z.nullable(z.string());
const nullableString = z.string().nullable(); // 위와 동일

nullableString.parse("asdf"); // => "asdf"
nullableString.parse(null); // => null

nullish()

undefined와 null을 모두 허용

const nullishString = z.string().nullish(); // string | null | undefined

// equivalent to
z.string().nullable().optional();

transform()

입출력간 데이터 변환.

// 입력을 받을 땐 string, number 모두 가능하지만 string으로 전환
const ID = z
  .string()
  .or(z.number())
  .transform((id) => (typeof id === "number" ? String(id) : id));

type ID = z.infer<typeof ID>;
//  ^? type ID = string
// 조금 더 복잡한 형태의 transform도 가능
const User = z
  .object({
    firstName: z.string(),
    middleName: z.string().optional(),
    lastName: z.string(),
  })
  .transform((user) => ({
    ...user,
    fullName: user.middleName
      ? `${user.firstName} ${user.middleName} ${user.lastName}`
      : `${user.firstName} ${user.lastName}`,
  }));

두 경우 모두 정의한 변환 규칙에 따라서 결과에 fullName 속성이 추가

console.log(User.parse({ firstName: "John", lastName: "Doe" }));
//{ firstName: 'John', lastName: 'Doe', fullName: 'John Doe' }

console.log(User.parse({ firstName: "John", middleName: "K.", lastName: "Doe" }))
//{ firstName: 'John', middleName: 'K.', lastName: 'Doe', fullName: 'John K. Doe }

or()

|| , 또는.

const ID = z.string().or(z.number())
type ID = z.infer<typeof ID>;
// type ID = string | number

and()

&&, 그리고.

const nameAndAge = z
  .object({ name: z.string() })
  .and(z.object({ age: z.number() })); // { name: string } & { age: number }

// equivalent to
z.intersection(z.object({ name: z.string() }), z.object({ age: z.number() }));

refine()

커스텀 validation

const myString = z.string().refine((val) => val.length <= 255, {
  message: "String can't be more than 255 characters",
});
  • refine() 은 두 개의 함수 인자를 받습니다.
    첫번째 인자는 유효성 검사
    두번째 인자는 옵션을 설정합니다. 허용하는 옵션은 아래와 같습니다.
    (value) ⇒ {message: string, path: [string | number], params: object}
    - 옵션 - 에러메세지
    ```tsx
    const longString = z.string().refine(
    	// 유효성 검사 함수
      (val) => val.length > 10,
    	// 옵션 설정 함수
      (val) => ({ message: `${val} is not more than 10 characters` })
    );
    ```
    
    - 옵션 - 에러 경로 지정
    
    ```tsx
    const passwordForm = z
      .object({
        password: z.string(),
        confirm: z.string(),
      })
      .refine((data) => data.password === data.confirm, {
        message: "Passwords don't match",
        path: ["confirm"], // path of error
      });
    
    passwordForm.parse({ password: "asdf", confirm: "qwer" });
    ```
  • refine()transforms() 와 함께 사용할 수 있다
    z.string()
      .transform((val) => val.length)
      .refine((val) => val > 25);

catch()

에러 발생 시 설정 값을 반환

const numberWithCatch = z.number().catch(42);

numberWithCatch.parse(5); // => 5
numberWithCatch.parse("tuna"); // => 42

readonly()

읽기전용

const schema = z.object({ name: string }).readonly();
type schema = z.infer<typeof schema>;
// Readonly<{name: string}>

const result = schema.parse({ name: "fido" });
result.name = "simba"; // error


Objects

// 모든 속성은 기본적으로 **required** 
const Dog = z.object({
  name: z.string(),
  age: z.number(),
});
  • .shape : 속성의 스키마 타입에 접근
    Dog.shape.name; // => string schema
    Dog.shape.age; // => number schema
  • keyof() : 객체의 key값으로 zodEnum 스키마 추출
    const keySchema = Dog.keyof();
    keySchema; // ZodEnum<["name", "age"]>
  • keyof() : 객체의 key값으로 zodEnum 스키마 추출
    const keySchema = Dog.keyof();
    keySchema; // ZodEnum<["name", "age"]>
  • extend() : 객체 스키마 상속
    const DogWithBreed = Dog.extend({
      breed: z.string(),
    });
  • pick() : 특정 키만 저장
    const Dogname = Dog.pick({ name: true });
    type Dogname = z.infer<typeof Dogname>; // => { name: string }
  • omit() : 특정 키만 삭제
    const NoNameDog = Dog.omit({ id: true });
    type NoNameDog= z.infer<typeof NoNameDog>; // => { age: number }
  • merge() : 객체 머지
    const BaseTeacher = z.object({ students: z.array(z.string()) });
    const HasID = z.object({ id: z.string() });
    
    const Teacher = BaseTeacher.merge(HasID);
    const Teacher = BeseTeacher.extend(HasId.shape) // merge() 와 동일
    type Teacher = z.infer<typeof Teacher>; // => { students: string[], id: string }
  • partial() : 일부\전체 속성을 옵셔널한 값으로 지정
    const user = z.object({
      email: z.string(),
      username: z.string(),
    });
    
    const partialUser = user.partial(); // { email?: string | undefined; username?: string | undefined } 
    
    // 옵셔널 속성 지정
    const optionalEmail = user.partial({
      email: true
    }); // { email?: string | undefined; username: string }
    
    //💡.partial은 한 depth만 적용. deepPartial()은 깊이 상관없이 적용가능 //
  • required() : 일부/전체 속성을 필수 값으로 지정
    const partialUser = user.partial();
    const requiredUser = partialUser .required(); // { email: string; username: string }
    
    const requiredEmail = partialUser .required({
      email: true,
    }); // { email: string; username?: string | undefined; }

Array

array를 사용할 땐 호출 순서에 주의해야 한다.

z.string().optional().array(); // (string | undefined)[]
z.string().array().optional(); // string[] | undefined
  • element : 배열 요소의 스키마 타에 접근
    stringArray.element; // => string schema
  • nonempty : 배열에 요소가 하나 이상 포함되도록 함
    const nonEmptyStrings = z.string().array().nonempty();
    
    nonEmptyStrings.parse([]); // throws: "Array cannot be empty"
    nonEmptyStrings.parse(["Ariana Grande"]); // passes
    
    // 에러메세지 지정 가능
    const nonEmptyStrings = z.string().array().nonempty({
      message: "Can't be empty!",
    });
  • min / max / length
    z.string().array().min(5); // 반드시 5개 이상의 항목 포함
    z.string().array().max(5); // 반드시 5개 이하의 항목 포함
    z.string().array().length(5); // 정확히 5개의 항목 포함

Function

인자와 리턴 타입 정의

const myFunction = z
  .function()
  .args(z.string(), z.number()) // accepts an arbitrary number of arguments
  .returns(z.boolean());

type myFunction = z.infer<typeof myFunction>;
// => (arg0: string, arg1: number)=>boolean
  • implement() : 함수의 리턴 개체의 유효성을 검사하여 새로운 함수를 반환
    const trimmedLength = z
      .function()
      .args(z.string())
      .returns(z.number())
      .implement((arg) => {
        // TypeScript knows arg is a string!
        return arg.trim().length;
      });
    
    trimmedLength("sandwich"); // => 8
    trimmedLength(" asdf "); // => 4
    • 리턴문이 없는 함수에는 z.void() 사용 가능
    • returns를 생략할 수 도 있음. 타입 추론에 의존 가능.

Dates

z.date()에는 Date 타입만 유효.

zod 3.20ver 부터 z.coerce.date() 로 Date타입 강제 가능.

z.date().safeParse(new Date()); // success: true
z.date().safeParse("2022-01-12T00:00:00.000Z"); // success: false

// v3.2
const dateSchema = z.coerce.date();

dateSchema.safeParse("2023-01-10"); // success: true
dateSchema.safeParse("1/10/23"); // success: true

// 날짜 유형에 맞지 않으면 유효성검사 통과 불가능
dateSchema.safeParse("2023-13-10") // success: false
dateSchema.safeParse("0000-00-00") // success: false
profile
프론트엔드 개발자 성장일기 💭

0개의 댓글