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를 활용하여 하나의 객체 내에서 여러 다른 속성이 서로에게 영향을 주는 좀 더 복잡한 변환도 구현할 수 있는데요.
예를 들어, firstName
과 lastName
속성은 필수 입력으로 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 태그를 통해서 쉽게 만나보세요!