본문 바로가기
  • 하고 싶은 일을 하자
dev

Zod를 도입한 이유 (런타임 타입 검증)

by 박빵떡 2024. 8. 7.
반응형

우리 개발팀은 typescript로 타입을 검증하고 있었다. ts는 컴파일 타임에 타입 오류를 잡아주어 많은 도움이 되지만, 외부 API 응답을 런타임에 체크하는 것은 불가능하다. 물론 백엔드 개발자와 소통한 형식의 타입이 넘어오는 것을 믿고, QA를 거치기 때문에 큰 문제가 되지는 않았다.

 

하지만 간혹 예기치 못한 이슈로 type이 다른 데이터가 넘어와 런타임 때 문제가 발생하는 경우가 있었다. 이는 유저에게 좋지 못한 경험을 주었고, 데이터 구조가 복잡할수록 디버깅하기가 어려웠다.

 

이러한 문제를 해결하기 위해 런타임에 데이터의 타입을 검증할 수 있는 zod라는 라이브러리를 신규 API에 대해 점진적으로 도입해 보기로 했다.

 

기존 코드

// redux toolkit
[getCoupons.fulfilled]: (state, { payload }) => {
  state.coupons = payload.result.coupons.map(
    (row: {
      id: number;
      name: string;
      discountPrice: number;
      isValid: boolean;
    }) => ({
      couponId: row.id,
      couponName: row.name,
      price: row.price,
      isValid,
    }),
  );
},

 

위는 기존 코드의 예시이다.

result에 coupons가 있는지,  id가 number인 지 등을 확인하지 않는다.

여기에 zod를 적용 해보자.

 

zod 환경 세팅

src/common/schemas 폴더 내에 schema 파일을 생성한다.

// src/common/schemas/couponSchemas.ts
import { z } from 'zod';

export const CouponSchema = z.object({
  id: z.number(),
  name: z.string(),
  discountPrice: z.number(),
  isValid: z.boolean(),
});

export const ApiResponseSchema = z.object({
  result: z.object({
    coupons: z.array(CouponSchema),
    maxCouponDiscountPriceCouponId: z.number(),
  }),
});

 

그런데 기존에 사용하고 있었던 interface Coupon이 있었다.

// src/modules/damhwaMarket/types.tsx
export interface Coupon {
  id: number;
  name: string;
  discountPrice: number;
  isValid?: boolean;
}

 

이 interface는 여기저기서 쓰고 있다. 필요한 선언이다.

그렇다면 zod로 type check를 하고 싶을 때

예를 들어 Coupon이라고 치면 interface도 만들고 schema도 만들어야 하나?

 

이것은 zod의 infer를 사용하여 해결할 수 있다.

즉, interface Coupon 따로 만들 필요 없다.

// 타입 추출
export type Coupon = z.infer<typeof CouponSchema>;
export type ApiResponse = z.infer<typeof ApiResponseSchema>;

 

이로써 최종 코드는 다음과 같다.

// src/common/schemas/couponSchemas.ts
import { z } from "zod";

export const CouponSchema = z.object({
  id: z.number(),
  name: z.string(),
  discountPrice: z.number(),
  isValid: z.boolean(),
});

export const ApiResponseSchema = z.object({
  result: z.object({
    coupons: z.array(CouponSchema),
  }),
});

// 타입 추출
export type Coupon = z.infer<typeof CouponSchema>;
export type ApiResponse = z.infer<typeof ApiResponseSchema>;

 

그리고 선언했던 interface Coupon은 삭제하면 된다.

 

그리고 api 호출하는 부분에서 아래처럼 사용하면 된다.

ApiResponseSchema.parse(response);

 

type 불일치가 발생한다면

[
  {
    code: "invalid_type",
    expected: "boolean",
    received: "number",
    path: ["result", "coupons", 0, "id"],
    message: "Expected boolean, received number",
  },
  {
    code: "invalid_type",
    expected: "boolean",
    received: "number",
    path: ["result", "coupons", 1, "id"],
    message: "Expected boolean, received number",
  },
];

 

만약 응답이 배열일 경우 이렇게 모든 type 불일치하는 경우를 알려준다.

그런데 유저가 읽기에는 부적절하다는 것을 알 수 있다.

따라서 zod에 의해 발생한 type 에러이면 sentry에만 보내도록 했다.

 

백엔드 개발자와 추가적인 소통이 필요하다

한 가지 추가적인 오버헤드라면 api 응답의 type을 백엔드 개발자와 소통해야 했다.

(어떤 타입인지, null이 올 수 있는지, 필수값인지 등)

이러한 소통이 없으려면 tRPC를 적용하는 방법이 있겠으나 좀 더 개발 규모가 크기 때문에 나중에 고려해 보기로 했다.

 

결과

zod를 도입한 이후 API 응답 데이터의 타입을 더욱 견고히 검증할 수 있게 되었다. 이를 통해 런타임 오류를 줄이고, 디버깅 시간을 단축할 수 있었다. 또한 생성한 schema를 공통적으로 사용하여 코드의 일관성과 가독성을 높였다. 앞으로도 신규 API에 zod를 도입하고, 기존 API들도 zod를 사용하는 것으로 개선해 볼 계획이다.

반응형

댓글