Zod로 입출력 간 데이터 변환하기

Zod로 입출력 간 데이터 변환하기

지난 포스팅에서 Zod로 스키마를 정의하는 다양한 방법에 대해서 알아보았는데요.

Zod의 부가적인 기능이지만 알아두면 굉장히 유용한 입출력 간 데이터 변환에 대해서 알아보겠습니다.

내장 트랜스포머

Zod는 입출력 간 문자열 변환을 돕기 위해서 트랜스포머(transformer)를 내장하고 있는데요. 대표적으로 .trim(), .toLowerCase(), .toUpperCase()를 들 수 있습니다.

그럼 이 3가지 내장 트랜스포머를 모두 사용해서 스키마를 하나 정의한 후에 데이터 변환을 해보겠습니다.

import { z } from "zod";

// 스키마 정의
const Transformers = z.object({
  trimmed: z.string().trim(),
  lowerCased: z.string().toLowerCase(),
  upperCased: z.string().toUpperCase(),
});

// 데이터 변환
const output = Transformers.parse({
  trimmed: " Hello, Zod! ",
  lowerCased: "Hello, Zod!",
  upperCased: "Hello, Zod!",
});

결과를 출력해보면 입력된 문자열이 좌우 공백 제거나 대소문자 변환이 되어서 출력되는 것을 볼 수 있습니다.

console.log(output);
콘솔
{
  trimmed: 'Hello, Zod!',
  lowerCased: 'hello, zod!',
  upperCased: 'HELLO, ZOD!'
}

트랜스포머 구현

Zod는 우리가 직접 구현한 트랜스포머도 사용할 수 있도록 .transform()라는 API도 제공하고 있는데요.

예를 들어, 입력은 숫자형이나 문자형으로 모두 받을 수는 있지만 출력할 때는 숫자형인 경우 문자형으로 변환해주는 스키마를 작성해보겠습니다.

import { z } from "zod";

// 스키마 정의
const ID = z
  .string()
  .or(z.number())
  .transform((id) => (typeof id === "number" ? String(id) : id));

// 데이터 변환
const id = ID.parse(1);

스키마 입력으로 숫자를 넘겨보면 문자형으로 출력이 나오는 것을 불 수 있습니다.

console.log(typeof id, id);
콘솔
string 1

그런데 이렇게 입력 자료형과 출력 자료형이 상이한 스키마로 부터 타입을 뽑아내면 어떻게 될지 궁금하지 않으신가요?

Zod를 사용하여 하나의 스키마로 유효성 검증과 타입 선언을 한 번에 해결하는 방법에 대해서는 별도 포스팅에서 자세히 다루고 있습니다.

z.infer을 사용해서 스키마로부터 타입을 추출해보면 출력 자료형 기준으로 타입 추론이 된다는 것을 알 수 있는데요.

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

만약에 입력 자료형 기준으로 타입을 뽑아내고 싶다면 대신에 z.input을 사용하면 됩니다. 마찬가지로 z.output도 있는데 z.infer과 동일하게 작동합니다.

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

같은 스키마로부터 입출력 타입을 모두 뽑아내면 다음과 같이 함수를 타이핑할 때 매우 유용하게 사용할 수 있습니다.

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

복잡한 입출력간 데이터 변환

Zod의 .transform() API를 활용하여 하나의 객체 내에서 여러 다른 속성이 서로에게 영향을 주는 좀 더 복잡한 변환도 구현할 수 있는데요.

예를 들어, firstNamelastName 속성은 필수 입력으로 middleName 속성은 선택 입력으로 받고, 이를 토대로 fullName 속성은 트랜스포머를 통해서 출력에 추가되도록 스키마를 정의해보겠습니다.

import { z } from "zod";

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}`,
  }));

이제 한 번은 middleName을 생략하고, 한 번은 middleName을 포함시켜 변환을 해보면

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

두 가지 경우 모두 정의한 변환 규칙에 따라서 결과에 fullName 속성이 추가되는 것을 볼 수 있습니다.

콘솔
{ firstName: 'John', lastName: 'Doe', fullName: 'John Doe' }
{
  firstName: 'John',
  middleName: 'K.',
  lastName: 'Doe',
  fullName: 'John K. Doe'
}

스키마로부터 타입을 추론해보면 결과 타입에만 fullName 속성이 포함되어 있는 것을 볼 수 있습니다.

type Input = z.input<typeof User>;
//   ^? type Input = { firstName: string; lastName: string; middleName?: string | undefined; }
type Output = z.output<typeof User>;
//   ^? type Output = { fullName: string; firstName: string; lastName: string; middleName?: string | undefined; }

마치며

지금까지 Zod로 유효성 검증 뿐만 아니라 입출력 간 데이터 변환도 가능하다는 것을 배웠습니다. 스키마 수준에서 이렇게 간편하게 데이터 변환을 할 수 있다는 점이 참 매력적이지 않나요? 🥰

Zod 관련 포스팅은 Zod 태그를 통해서 쉽게 만나보세요!

This work is licensed under CC BY 4.0 CC BY

개발자를 위한 뉴스레터

달레가 정리한 AI 개발 트렌드와 직접 만든 콘텐츠를 전해드립니다.

Discord