저번주 과제 내용에서
zod를 활용해보면 좋겠다라는 과제 내용을 받았다. 그래서 zod에 대해서 한 번 알아보고 현재 개발 진행중인 GDSC KNU 홈페이지에 적용해보기로 하였다.
우선 zod에 대해서 공식문서에 관련하여 찾아보기로 하였다.
zod에서 공식문서를 찾아보면 zod에 대한 정의가 나와있다.
Zod is a TypeScript-first schema declaration and validation library. I'm using the term "schema" to broadly refer to any data type, from a simple string to a complex nested object.
Zod is designed to be as developer-friendly as possible. The goal is to eliminate duplicative type declarations. With Zod, you declare a validator once and Zod will automatically infer the static TypeScript type. It's easy to compose simpler types into complex data structures.
요약을 하자면
Zod는 TypeScript를 우선으로 하는 스키마 선언 및 검증 라이브러리로, 여기서 "스키마"는 간단한 문자열부터 복잡한 중첩 객체까지 모든 데이터 유형을 의미한다.
Zod는 개발자 친화적으로 설계되어 중복된 타입 선언을 제거하는 것을 목표로 한다. Zod를 사용하면 한 번 검증기를 선언하면 Zod가 자동으로 정적 TypeScript 타입을 추론한다. 간단한 타입들을 쉽게 조합하여 복잡한 데이터 구조를 만들 수 있다.
zod는 사용하기 위해서 일정 조건이 필요하다
우선 TypeScript에는 한계점이 존재한다.
내가 아는 타입스크립트의 한계 중 하나를 뽑으라고 한다면 젤 큰 한계점은 런타입때 타입 검증이 되지 않는다는 점이다.
TypeScript는 정적 타입 검사 도구로, 주로 컴파일 타임에 코드의 타입 오류를 잡는데 사용된다.
하지만 TypeScript는 JavaScript의 상위 집합으로, 컴파일러가 TypeScript 코드를 읽고 타입 검사를 수행한 후, 순수한 JavaScript 코드로 변환하게 되는데, 이때 JavaScript는 브라우저나 Node.js에서 실행하게 된다. 이때 변환된 JavaScript 코드에는 타입 정보가 포함되지 않기 때문에, 런타임에서는 TypeScript의 타입 시스템이 작동하지 않는다.
엥 그게 굳이 필요해?
보통 런타임 타입 검증은 외부 API 호출이라던가 사용자 입력을 할 때 예측할 수 없는 데이터 타입을 검증할 때 필요하다.
예를 들자면 API 호출같이 API 응답이 예상한 타입과 일치하지 않을 수 있다.
interface User {
id: number;
name: string;
}
const fetchUser = async (): Promise<User> => {
const response = await fetch('/api/user');
const data = await response.json();
return data;
}
const displayUser = async () => {
const user = await fetchUser();
console.log(user.id, user.name);
}
displayUser();
위 코드에서 fetchUser 함수는 외부 API로부터 사용자 데이터를 가져온다. 이때 TypeScript는 User 인터페이스를 통해 반환 타입을 정의하고 있지만, 실제 API 응답 데이터의 구조가 User 타입을 따르는지는 알 수 없다. API 응답이 id가 없는 객체이거나 name이 숫자일 경우, 런타임 오류가 발생할 수 있게 된다.
이때 한 번 zod를 활용해보자.
import { z } from 'zod';
const UserSchema = z.object({
id: z.number(),
name: z.string(),
});
type User = z.infer<typeof UserSchema>;
const fetchUser = async (): Promise<User> => {
const response = await fetch('/api/user');
const data = await response.json();
// 런타임 타입 검증
const parsedData = UserSchema.parse(data);
return parsedData;
}
const displayUser = async () => {
const user = await fetchUser();
console.log(user.id, user.name);
}
displayUser();
첫 번째 코드에서 zod를 추가한 내용의 코드이다. 위 예제에서는 UserSchema
는 User
타입을 정의하고, UserSchema.parse(data)
를 통해 런타임에 데이터를 검증시킨다. 이때 API 응답이 예상된 타입과 일치하지 않을 경우 오류가 발생하며, 문제를 조기에 발견할 수 있게 된다. 어떻게 보면 double check라고 생각하면 된다.
우선 React-hook-form 라이브러리는 ref를 통해 uncontrolled Component로 이루어진 라이브러리이다. 이 말인 즉슨, 폼상태가 로컬 상태로 관리되지 않기 때문에, 폼 요소들이 변경될 때마다 전체 컴포넌트가 리렌더링이 되지 않는 장점이 있다.
react-hook-form의 한계점
우선적으로 react hook form은 폼 데이터의 타입 검증을 기본적으로 제공하지 않는다.
개발자가 직접 타입을 관리해야되는데다가 복잡한 폼일 경우, 상태 관리를 효율적으로 수행하는데 어려움이 있을 수 있다. 특히 중첩된 필드나 배열 형태의 데이터 구조를 다룰 때 복잡성이 증가한다.
또한 uncontrolled Component이기 때문에 런타임에서만 폼데이터를 접근할 수 있다. 결과론적으로 TypeScript의 한계점을 고스란히 안고가는 결과가 발생하게 된다. 이 점을 Zod가 타입 안정성을 확보할 수 있게 된다.
zod와 같이 쓰게 된다면?
Zod를 통해 작성하게 된다면 구조화된 에러 메서지를 제공해주기 때문에, 에러 메서지를 일관되게 관리를 할 수 있어 재사용성도 매우 높일 수 있고 유지 보수성도 높일 수 있다.
import { z } from 'zod';
export type SignUpSchemaType = z.infer<typeof SignUpSchema>;
export const SignUpSchema = z.object({
name: z.string().min(1, { message: '🚨 이름은 필수로 입력해주셔야 합니다.' }),
age: z
.number({
invalid_type_error: '🚨 나이는 숫자형식으로 입력해주셔야 합니다.',
})
.min(1, { message: '🚨 나이는 1살부터 입력이 가능합니다.' }),
studentNumber: z
.string()
.min(10, { message: '🚨 학번을 10자리로 입력해주세요.' })
.max(10, { message: '🚨 학번을 10자리로 입력해주세요.' }),
major: z
.string()
.min(1, { message: '🚨 전공을 필수로 입력해주셔야 합니다.' }),
phoneNumber: z.string().regex(/^\d{3}-\d{4}-\d{4}$/, {
message: '🚨 올바른 전화번호 형식을 입력해주세요. 예: 000-0000-0000',
}),
});
보통 zod를 사용할 경우 Schema를 통해서 Type을 선언 후 여러 에러 메시지를 선언할 수 있게 된다.
이후 react-hook-form을 통해 작성을 할 때
const {
register,
formState: { errors },
} = useForm<SignUpSchemaType>({
resolver: zodResolver(SignUpSchema),
});
zodResolver를 통해 react-hook-form과 연결시켜준다. 이렇게 되면 react-hook-form을 통해서 register를 통해 예외처를 다 작성해줬어야 했지만
<SignupInput
id='major'
title='전공'
placeholder='전공의 정식명칭을 입력해주세요.'
type='string'
register={register('major')}
/>
간단하게 컴포넌트 데이터를 넘겨줄 명을 써주면 된다.
<ErrorMessage
errors={errors}
name='major'
render={({ message }) => <Error role='alert'>{message}</Error>}
/>
이 때 react-hook-form 라이브러리를 이용하여 Error Message라는 컴포넌트를 사용하여 작성해주었다.
이후 추가 정보 입력시 잘 적용되는 것을 볼 수 있다.
react-hook-form이랑 zod를 사용하게 되면 ref를 통해서 얻는 불안정성을 조금 더 높일 수 있게 된다. 물론 그렇다고 해서 꼭 Zod를 써야하나? 이 부분에 관련해서는 생각하기 나름인것 같다.(그렇다면 JS는 진짜 문제가 많기 때문)
그래서 선택에 따라서 한 번 정말 프로젝트 내에서 필요하다라고 생각하면 써보는 것을 강력 추천한다!