본문 바로가기
개발

저의 훼이보릿 라이브러리 zod에 대해 정리해보겠어요(+ 실제 사용 예시)

by dohye1 2024. 8. 4.

목차

    반응형

    2023 하반기에 zod를 정말 유용하게 사용했다

    zod는 데이터의 스키마를 작성하기쉽게 도와주는 라이브러리이다

     

    zod말고 yup이나 formik등의 라이브러리도 있는데, 왜 하필 zod냐?

    zod만이 가지는 장점이 있기때문에 아래의 사용예시를 보고 매력을 느낀다면 한번 써보시는것을 추천한다

    zod

     

    GitHub - colinhacks/zod: TypeScript-first schema validation with static type inference

    TypeScript-first schema validation with static type inference - colinhacks/zod

    github.com

     

    zod 사용방법

    zod를 사용해서 원시타입/객체/배열 등 다양한 타입의 스키마 작성이 가능하다.

    그리고 parse, safeParse라는 메서드를 사용해 스키마와 실제 데이터의 타입을 검사할 수 있다.

    import { z } from "zod";
    
    // creating a schema for strings
    const mySchema = z.string();
    
    // parsing
    mySchema.parse("tuna"); // => "tuna"
    mySchema.parse(12); // => throws ZodError
    
    // "safe" parsing (doesn't throw error if validation fails)
    mySchema.safeParse("tuna"); // => { success: true; data: "tuna" }
    mySchema.safeParse(12); // => { success: false; error: ZodError }
    

     

    import { z } from "zod";
    
    const User = z.object({
      username: z.string(),
    });
    
    User.parse({ username: "Ludwig" });
    
    // extract the inferred type
    type User = z.infer<typeof User>;
    // { username: string }
    

     

    parse : 스키와마 데이터의 형태를 검사하고, 만약 스키마에 정의된 조건(필드명, 데이터타입, 필수여부...)에 맞지않으면 에러를 던진다.

    safeParse : parse와 동작이 유사하지만 스키마의 조건에 맞지않다면 에러메시지가 담긴 객체를 반환한다.

     

    그리고 parse, safeParse를 사용하면 그 결과는 깊은복사를 통해 복사된 객체가 반환된다.

    그리고! 스키마에 정의되지않는 필드는 제거한 결과값을 반환한다.

     

    아래 예시를 보면 스키마에는 name과 age만 정의되어있고,

    데이터는 name, birth_year, height를 가지고있는데,

    parse의 결과는 스키마와 데이터의 중복된 필드인 name만 담겨있는것을 볼수있다.

     

     

    zod를 어떻게 사용했냐면

    1. schema 작성
    2. react-hook-form 사용시 validation check schema
    3. schema를 정적 타입으로 생성
    4. 런타임 타입 체크
    5. url 쿼리스트링에 필요한값/필요없는 값이 들어있는지 검증할때 사용

    실무 사용예시

    1. 쿼리스트링 처리

    1. url query string을 읽는다.
    2. query string을 객체로 파싱해서 특정 페이지가 받을수있는 데이터들만 포함되어있는지 검증한다.(safeParse)
    3. 2의 결과값에서 success가 true면 파싱한 객체를 컴포넌트에 전달하고 ,success가 false면 빈객체를 반환한다.
    // nextjs의 getServerSideProps를 사용해 서버에서 query string이 유효한지 검증한다.
    export const getServerSideProps: GetServerSideProps<{ initialQuery: Partial<TestListQuery> }> = async (context) => {
      const parsed = queryString.parseUrl(context.req.url ?? "", { ...arrayParseOption, parseBooleans: true }).query;
    
      const result = testListQuerySchema.partial().safeParse(parsed);
    
      // 특정페이지가 받을수있는 valid한 query string이라면 컴포넌트에 넘겨주고 아니라면 빈객체를 넘겨준다.
      const initialQuery = result?.success ? result.data : {};
    
      return {
        props: { initialQuery },
      };
    };
    
    export default function TestList({ initialQuery }: InferGetServerSidePropsType<typeof getServerSideProps>) {
      return <TestListContent initialQuery={initialQuery} />;
    }
    
    // ==================== 스키마 ====================
    
    const nonEmptyStringSchema = z.string();
    
    export const brandCdSchema = z.object({
      brandCd: nonEmptyStringSchema,
    });
    
    // 스키마를 확장하는 방법 : extend
    export const testListQuerySchema = brandCdSchema.extend({
      isFinal: z.boolean().optional(),
      startDate: z.string().optional(),
      endDate: z.string().optional(),
    });
    
    // 스키마를 타입으로 추출 z.infer
    type TestListQuery = z.infer<typeof testListQuerySchema>;

     

    2. react-hook-form

    import { useForm } from 'react-hook-form';
    import { zodResolver } from '@hookform/resolvers/zod';
    import * as z from 'zod';
    
    const schema = z.object({
      name: z.string().min(1, { message: 'Required' }),
      age: z.number().min(10),
    });
    
    
    const App = () => {
      const {
        register,
        handleSubmit,
        formState: { errors },
      } = useForm({
       // zod로 선언한 스키마를 useForm에 넘겨줘서 form 제출시 검증기준으로 사용
        resolver: zodResolver(schema),
      });
    
      return (
        <form onSubmit={handleSubmit((d) => console.log(d))}>
          <input {...register('name')} />
          {errors.name?.message && <p>{errors.name?.message}</p>}
          <input type="number" {...register('age', { valueAsNumber: true })} />
          {errors.age?.message && <p>{errors.age?.message}</p>}
          <input type="submit" />
        </form>
      );
    };

    3. api response type check

    const getBrandList = async (_: QueryFunctionContext<ReturnType<typeof BRAND_QUERY_KEY.LIST>>) => {
      const url = "/api/brand/all"
      
      const { data } = await api.get<APIData<Brand[]>>(url);
      
      // response 데이터의 포맷을 검사한다
      brandSchema.array().parse(data.data, { path: [url] });
      return data;
    };
    
      /* [
          {
            "code": "invalid_type",
            "expected": "string",
            "received": "number",
            "path": [ "name" ],
            "message": "Expected string, received number"
          }
      ] */

     

    api 검증시 zod + axios를 합친 zodius라는 라이브러리도 있다고한다.

     

    Zodios | Zodios

    End-to-end typesafe REST API toolbox

    www.zodios.org

     

    이건 좀 신기한듯..!!

    그런데 이 라이브러리를 사용하려면 클라이언트와 서버의 합의가 필요해보긴함!

     

    다른 라이브러리와의 차이점

     

    GitHub - colinhacks/zod: TypeScript-first schema validation with static type inference

    TypeScript-first schema validation with static type inference - colinhacks/zod

    github.com

     

    Joi

    • 정적 타입 추론이 불가능하다.

    Yup

    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
    • union & intersection schemas를 지원하지않음
    // zod는 Union도 선언이 가능하다
    const a = z.union([z.literal("apple"), z.literal("banana")])
    type A = z.infer<typeof a>

     

     

    joi vs yup vs zod | npm trends

    Comparing trends for .

    npmtrends.com

    npm trend를 보면 zod의 상승세를 확인해볼수있다.

    zod deep dive

    zod의 parse 메서드에 대해 어떻게 동작하는지 궁금해서 찾아보다가, 정리가 잘 된 블로그를 발견했다.

     

    z.string() 구현하기

    ZodType과 ZodString 클래스를 간단히 구현하면서 zod가 어떻게 문자열을 검증하는지 알아봅니다.

    www.philly.im

    이분의 블로그에서 감명깊게 본 부분을 참고해보겠습니다...

    safeParse

    1. zodType 추상클래스

    - zod 타입클래스가 가져야할 메서드의 형태를 정의

    safeParse의 구현을 보면 내부적으로 _parse라는 메서드를 사용한다.

    abstract class ZodType {
      abstract _parse( data: unknown ): { isValid: true; data: unknown } | { isValid: false; reason?: string };
    
      safeParse(data: unknown) {
        const result = this._parse(data);
    
        if (result.isValid) {
          return { success: true, data: result.data };
        } else {
          return { success: false, error: new Error(result.reason ?? "검증 실패") };
        }
      }
    }
    

     

    2. 문자열을 가지는 ZodString 클래스의 내부 구현을 보면 1애서 정의한 추상클래스를 확장하기때문에 _parse라는 메서드의 내부 구현을 해줘야한다.

    그런데 생각보다 엄청 간단했음.,...!

    typeof 메서드를 사용하여 데이터가 string타입인지 검증해서 response를 결정한다.

    class ZodString extends ZodType {
      _parse( data: unknown ): { isValid: true; data: unknown } | { isValid: false; reason?: string } {
        if (typeof data === "string") {
          return {
            isValid: true,
            data,
          };
        } else {
          return {
            isValid: false,
            reason: `${data}는 string이 아닙니다.`,
          };
        }
      }
    }
    

    참고

    https://www.philly.im/blog/implementing-zod-using-typescript-1