- Published on
Schema Validation Layer (with zod)
API 반환값을 추론하기 위해 예상되는 타입을 Generic으로 기존에 넣어주었다.
export default async function fetchPosts() {
const { data } = await axios.get<Post>('https://jsonplaceholder.typicode.com/posts');
return data;
}
export type Post = {
userId: number;
id: number;
title: string;
body: string;
};
하지만 실제로는 Post
가 아닌 Post[]
를 반환함에도 타입에러가 잡히지 않는다.
Generic
으로 타입을 넣어주는 방식은 Compile Time에서 에러가 잡히지 않기 때문에 Run Time에서 예상치 못한 문제가 발생할 수 있다.
이러한 문제를 Schema Validation Layer 를 추가하여 해결해본다.
Schema Validation
npm에 Schema Validation을 지원하는 패키지가 다양하게 있다. (링크)
그 중에 zod를 사용해본다.
위 예제를 zod 로 Schema Validaition 을 하면 아래와 같이 작성할 수 있다.
// schema/post.ts
import axios from 'axios';
import { z } from 'zod';
export const Post = z.object({
userId: z.number(),
id: z.number(),
title: z.string(),
body: z.string(),
});
export type Post = z.infer<typeof Post>;
export const Posts = z.array(Post);
export type Posts = z.infer<typeof Post>;
export default async function fetchPosts() {
const { data } = await axios.get('https://jsonplaceholder.typicode.com/posts');
return Posts.parse(data);
}
만약 잘못된 Schema 로 parse 를 시도한다면, ZodError 를 throw 한다.
zod 는 schema 단에서 데이터를 다룰 수 있는 매우 많은 method 를 제공해준다.
그리고, infer
method 를 사용하면 schema 를 기반으로 추론한 타입을 사용할 수 있다.
Zod Schema
.optional
body: z.string().optional(),
다음과 같이 사용하면 body?: string
의 의미를 갖는다.
.nullable
body: z.string().nullable(),
다음과 같이 사용하면 body: string | null
의 의미를 갖는다.
이 경우, body 필드가 parse 대상에 없는 경우 Error 를 던진다.
.nullish
body: z.string().nullish(),
다음과 같이 사용하면 body?: string | null | undefined
의 의미를 갖는다.
위 예시는 z.string().optional().nullabe()
과 동일하다.
z.enum
const Fish = z.enum(['Salmon', 'Tuna', 'Trout']);
console.log(Fish.enum); // => {Salmon: "Salmon", Tuna: "Tuna", Trout: "Trout"};
console.log(Fish.enum.Salmon); // => "Salmon"
console.log(Fish.options); // => ["Salmon", "Tuna", "Trout"]
Zod 는 enum 을 위한 자체적인 메서드를 지원한다. enum 으로 생성한 값은 .enum
, .options
으로 다양한 값으로 사용할 수 있다.
z.nativeEnum
enum FishEnum {
Salmon = 'Salmon',
Tuna = 'Tuna',
Trout = 'Trout',
}
const Fish = z.nativeEnum(FishEnum);
앞서 소개한 z.enum() 이 enum 을 정의하거나 유효성을 검증하는 가장 추천되어지는 방식이지만, 이미 존재하는 enum 을 사용해야 하는 경우도 있다. 이럴 경우, z.nativeEnum
을 사용하여 스키마를 정의할 수 있다.
z.infer
const Post = z.object({
userId: z.number(),
id: z.number(),
title: z.string(),
body: z.string(),
});
type Post = z.infer<typeof Post>;
// type Post = {
// id: number;
// userId: number;
// title: string;
// body: string;
// }
infer 를 사용하면 만들어 둔 스키마를 기반으로 타입 추론을 해준다.
Zod Method
.parse
ZodSchema.parse()
의 형태로 사용하며, 만든 Schema 가 인자에 들어오는 대상을 parse 할 수 있는지 검증하고, 유효하다면, schema 타입에 맞는 value를 반환한다. 유효하지 않다면 error 를 throw 한다.
API 반환 값을 parsing 하는 용도 외에 다양한 방식으로 사용할 수도 있다.
payload 에 들어갈 값 중에 id 를 걸러줘야 하고, 해당 값을 넣으면 서버에서 오류를 뱉는다고 가정한다. 그렇다면 다음과 같은 방식을 사용할 수 있을것이다.
function fetchSomething(payload: { id: string; name: string; content: string }) {
const newPayload = Object.fromEntries(
Object.entries(payload).filter(([key, value]) => key !== 'id')
);
// do something...
}
그러나, payload 에 또 다른 값이 영향을 주어서 filter 해야 하는 값이 또 추가된다면 해당 로직을 재수정 해야하고, 이는 매우 귀찮은 작업이다. 이를 parse 로 해결할 수 있다.
const Something = z.object({
name: z.string();
content: z.string();
})
type Something = z.infer<typeof Something>;
function fetchSomething(payload: Something) {
const newPayload = Something.parse(payload);
// do something...
}
로직이 매우 간단해지고, 다른 필드가 추가적으로 들어오더라도 안전하게 payload 를 서버에 넘겨줄 수 있다.
.safeParse
앞서 소개한 parse 는 인자가 parsing 을 통과하지 못하면 Error 를 throw 한다.
그러나, error 가 throw 되는 것을 원치 않는 경우도 있을 것이다.
예를 들어, parsing 에 실패했을 때 다른 로직을 실행할 수도, 기존 값을 수정할 수도 있을 것이다.
safeParse는 다음과 같이 사용한다.
let name: unknown = 'myName';
console.log(z.string().safeParse(name)); // => { success: true; data: "myName" }
name = 1;
console.log(z.string().safeParse(name)); // => { success: false; error: ZodError }
const { success, data, error } = z.string().safeParse(name);
if (!success) {
// do something!!
}
.preprocess
때때로 Schema 에 통과하기 전 값을 변형시키고 싶은 경우가 있다.
예를 들어, 서버에서 넘겨주는 데이터 인터페이스가 바뀌었고 하위 호환성을 위해 기존 인터페이스를 유지 시켜줘야 할 필요가 있는 경우 preprocess 를 유용하게 사용할 수 있다.
const PostV2 = z.preprocess(
(input: any) => {
input.subtitle = input.version === 1 ? '' : input.subtitle;
// or input.subtitle ??= ""
return input;
},
z.object({
userId: z.number(),
id: z.number(),
title: z.string(),
body: z.string(),
subtitle: z.string(),
})
);
.transform
transform 은 Schema 를 통과한 데이터를 변형시켜 줄 때 사용한다.
서버에서 넘겨준 데이터와 클라이언트에서 렌더링을 위해 필요한 데이터의 인터페이스가 다른 경우가 흔히 있다.
이런 경우, 값을 가공하는 역할을 Schema Validation Layer
에 위임함으로써 주 로직을 보다 깔끔하게 관리할 수 있다.
export type Url = z.infer<typeof Url>; // Type Inference => Url: string
export const Url = z
.object({
protocol: z.string(),
host: z.string(),
pathname: z.string(),
})
.transform(({ protocol, host, pathname }) => {
return `${protocol}//${host}${pathname}`;
});