Zod is a TypeScript-first schema declaration and validation library.
공식문서에 나와있는 그대로 해석하자면, 타입스크립트 친화적인 zod를 이용해 스키마를 정의하고 검증할 수 있습니다.
저희 프로젝트의 Admin page에서는 react-hook-form과 Apollo GraphQL이 사용되고 있습니다.
보시다시피 form 제출 전 유효성 검사가 필수로 이루어져야 하며, 유효성 검사를 해야 하는 항목들이 많은 것을 확인하실 수 있습니다. 이를 위해 저희 팀은 zod 라이브러리를 도입하여 런타임 환경에서도 type error를 감지하며 유효성 검사를 진행하고 있습니다.
이전 포스팅에서 react-hook-form의 useForm을 리팩토링한 과정을 적었습니다! 리팩토링 과정에서 zod schema 또한 create, edit에서 사용되고 있었습니다.
두 페이지의 zod schema는 유사한 듯 다른 점이 있었습니다. 코드의 간결성과 유지보수성을 위해, 기존 page에 있던 zod schema를 utils.ts
파일에 따로 빼 정리했습니다.
utils.ts
import { levels, languages } from '@/lib/constants'
import { z } from 'zod'
const commonSchema = z.object({
title: z.string().min(1, { message: 'required' }).max(200),
isVisible: z.boolean(),
difficulty: z.enum(levels),
languages: z.array(z.enum(languages)).min(1),
description: z
.string()
.min(1)
.refine((value) => value !== '<p></p>'),
inputDescription: z
.string()
.min(1)
.refine((value) => value !== '<p></p>'),
outputDescription: z
.string()
.min(1)
.refine((value) => value !== '<p></p>'),
testcases: z
.array(
z.object({
input: z.string().min(1),
output: z.string().min(1)
})
)
.min(1),
timeLimit: z.number().min(0),
memoryLimit: z.number().min(0),
hint: z.string().optional(),
source: z.string().optional(),
template: z
.array(
z
.object({
language: z.enum([
'C',
'Cpp',
'Golang',
'Java',
'Python2',
'Python3'
]),
code: z.array(
z.object({
id: z.number(),
text: z.string(),
locked: z.boolean()
})
)
})
.optional()
)
.optional()
})
export const editSchema = commonSchema.extend({
id: z.number(),
tags: z
.object({ create: z.array(z.number()), delete: z.array(z.number()) })
.optional(),
samples: z.object({
create: z
.array(z.object({ input: z.string().min(1), output: z.string().min(1) }))
.min(1),
delete: z.array(z.number().optional())
})
})
export const createSchema = commonSchema.extend({
tagIds: z.array(z.number()),
samples: z
.array(
z.object({
input: z.string().min(1),
output: z.string().min(1)
})
)
.min(1)
})
위 코드에서 commonSchema는 공통된 구조를 정의하고 있으며, editSchema와 createSchema는 commonSchema를 확장하여 각각 필요한 추가 필드를 정의하고 있습니다. 코드의 중복은 줄고, 관리는 쉬워지는 것입니다.
extend 메서드는 기존의 스키마를 기반으로 새로운 스키마를 확장할 때 사용됩니다. 예를 들어, editSchema와 createSchema는 공통된 필드를 가지면서도 서로 다른 추가 필드들을 포함하고 있습니다.
또한, optional() 메서드를 사용하여 필드가 선택적(optional)임을 지정할 수 있습니다. 이를 통해 해당 필드가 존재하지 않아도 검증을 통과할 수 있습니다.
zod 내의 message를 활용하면, 해당하는 유효성 검사를 통과하지 못했을 때의 에러 메시지를 직접 지정해 줄 수 있습니다.
const methods = useForm<CreateProblemInput>({
resolver: zodResolver(createSchema),
defaultValues: {
difficulty: Level.Level1,
tagIds: [],
samples: [{ input: '', output: '' }],
testcases: [{ input: '', output: '' }],
hint: '',
source: '',
template: [],
isVisible: true
}
})
위 코드처럼 react-hook-form을 이용할 때, resolver
에 zodResolver(Schema)
형태로 구현합니다. 이때 유효성 검사를 통과하지 않으면
const { handleSubmit, setValue, getValues } = methods
다음의 submit이 동작하지 않으니, 디버깅 시 유의해야 합니다.
Zod를 사용하여 스키마 선언과 유효성 검사를 쉽게 처리할 수 있으며, 타입 안전성을 높일 수 있습니다. 이를 통해 개발 생산성을 높이고 버그를 줄일 수 있었습니다! 아래와 같이 이전에 바닐라 자바스크립트로 하나하나 유효성 검사를 해 본 적이 있었는데요,
//reference: mdn
const emailRegExp =
/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;
zod 라이브러를 이용해 간편하고, 가독성 좋고, 확장성까지 좋은 유효성 검사를 진행할 수 있어 럭키입니다!