목차
2023 하반기에 zod를 정말 유용하게 사용했다
zod는 데이터의 스키마를 작성하기쉽게 도와주는 라이브러리이다
zod말고 yup이나 formik등의 라이브러리도 있는데, 왜 하필 zod냐?
zod만이 가지는 장점이 있기때문에 아래의 사용예시를 보고 매력을 느낀다면 한번 써보시는것을 추천한다
zod
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를 어떻게 사용했냐면
- schema 작성
- react-hook-form 사용시 validation check schema
- schema를 정적 타입으로 생성
- 런타임 타입 체크
- url 쿼리스트링에 필요한값/필요없는 값이 들어있는지 검증할때 사용
실무 사용예시
1. 쿼리스트링 처리
- url query string을 읽는다.
- query string을 객체로 파싱해서 특정 페이지가 받을수있는 데이터들만 포함되어있는지 검증한다.(safeParse)
- 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라는 라이브러리도 있다고한다.
이건 좀 신기한듯..!!
그런데 이 라이브러리를 사용하려면 클라이언트와 서버의 합의가 필요해보긴함!
다른 라이브러리와의 차이점
Joi
- 정적 타입 추론이 불가능하다.
Yup
- 디폴트 타입이 옵셔널이다. https://github.com/jquense/yup
- function schemas를 지원하지않는다.
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>
npm trend를 보면 zod의 상승세를 확인해볼수있다.
zod deep dive
zod의 parse 메서드에 대해 어떻게 동작하는지 궁금해서 찾아보다가, 정리가 잘 된 블로그를 발견했다.
이분의 블로그에서 감명깊게 본 부분을 참고해보겠습니다...
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
'개발' 카테고리의 다른 글
Next.js 14 app directory 사용시 새로 알게된 내용 정리 (0) | 2024.11.12 |
---|---|
Deep Link란 무엇일까? (1) | 2024.08.18 |
[GIT] 실무에서 자주쓰는 Git 명령어 정리 (21) | 2024.07.26 |
Error 객체를 JSON.stringify에 넣으면 빈객체가 출력되는건에 대하여 (1) | 2024.04.27 |
10분만에 npm cli 명령어로 자기소개하기 (1) | 2023.09.28 |