
react-hook-form은 써봤는데, zod는 처음 써봐서 zod에 대한 정리를 하려고 한다!
react-hook-form과 zod은 React에서 폼을 간편하게 관리하고, 유효성 검사를 수행하는 데 유용한 라이브러리다. 두 개의 라이브러리를 함께 사용하면, 폼 입력 관리와 검증 과정을 매우 효율적으로 처리할 수 있다.
보통 TypeScript에서,zod를 사용하는 것이 타입을 검증하는 면에서 훨씬 유용하다는 장점이 있다.
npm install react-hook-form zod @hookform/resolvers
import { z } from "zod";
const schema = z.object({
// email 필드는 문자열이며,이메일 형식
email: z.string().email(),
// password 필드는 문자열이며, 최소 6자 이상
password: z.string().min(6),
});
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
const {
register, // 각 input 요소에 연결하는 함수
handleSubmit,
formState: { errors }, // 유효성 검사-에러처리
} = useForm({
// 유효성 검사를 Zod 스키마를 기반 수행 설정
resolver: zodResolver(schema),
});
Controller는 React Hook Form에서 커스텀 컴포넌트나 제3자 UI 라이브러리를 폼에 연결할 때 사용하는 중간 관리자 역할!
Controller는 React Hook Form이 <input>이 아닌 커스텀 컴포넌트의 value와 onChange를 관리할 수 있게 해주는 컴포넌트
예를들어,
register()로 바로 연결이 가능한 컴포넌트는 연결에 문제가 되지 않는다.
<input {...register("email")} />
하지만, 내부적으로 value와 onChange를 직접 제어하는 컴포넌트는 register로는 동작하지 않는다.
<Input
value={someValue}
onChange={someHandler}
/>
이때, 이 역할을 Controller가 대신 연결을 맡아준다.
<Controller
name="email" //폼 필드 이름
control={control} // useForm에서 가져온 control
render={({ field }) => ( //render 함수로 Input 연결
<Input {...field} />
)}
/>
field는 { value, onChange, onBlur, ref } 등의 속성이 포함되어 있어서, <Input /> 컴포넌트에 그대로 넘기면 폼 연동이 된다.
생년월일을 처리할때 썼던 zod + react-hook-form 예시
import { z } from "zod";
const birthDateSchema = z
.string()
.refine((value) => {
// YYYYMMDD 형태로 정규화된 날짜여야 함
const isValid = /^\d{8}$/.test(value);
if (!isValid) return false;
// 간단한 유효 날짜 체크
const y = Number(value.slice(0, 4));
const m = Number(value.slice(4, 6));
const d = Number(value.slice(6, 8));
const date = new Date(`${y}-${m}-${d}`);
return (
date.getFullYear() === y &&
date.getMonth() + 1 === m &&
date.getDate() === d
);
}, {
message: "올바른 생년월일을 입력해주세요",
});
const schema = z.object({
date: birthDateSchema,
});
type FormValues = z.infer<typeof schema>;
const {
control,
handleSubmit,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(schema),
});
const [displayBirthDate, setDisplayBirthDate] = useState("");
const formatBirthDate = (input: string) => {
// 숫자만 추출 후 YYYY / MM / DD 형태로 변환
const digits = input.replace(/\D/g, "").slice(0, 8);
const parts = [digits.slice(0, 4), digits.slice(4, 6), digits.slice(6, 8)];
return parts.filter(Boolean).join(" / ");
};
const normalizeBirthDate = (formatted: string) => {
// YYYYMMDD 형태로 정규화
return formatted.replace(/\D/g, "").slice(0, 8);
};
<Controller
name="date"
control={control}
render={({ field: { onChange, value, ...rest } }) => {
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const formatted = formatBirthDate(e.target.value);
const normalized = normalizeBirthDate(formatted);
setDisplayBirthDate(formatted);
onChange(normalized); // 실제 form에는 YYYYMMDD만 저장 -> 백엔드에 따라 다름
};
return (
<Input
{...rest}
value={displayBirthDate}
onChange={handleInputChange}
inputtitle="생년월일"
placeholder="YYYY / MM / DD"
inputMode="numeric"
maxLength={14}
className={`h-[68px] w-full font-B02-M ${
errors.date ? "border-warning" : ""
}`}
undertext={errors.date?.message}
undertextClassName={errors.date ? "text-warning" : ""}
/>
);
}}
/>
나처럼 기존에 react-hook-form만 쓰던 분이라면, zod를 곁들이는 것만으로도 코드가 더 안정적이고 관리하기 쉬워진다. 위에 생년월일 예시 코드는 진짜 내가 썼던 코드인데 확실히 유효성 검사 관련해서도 쉽게 처리할 수 있었다. 하지만 같은 코드가 계속 반복되고,에러 메세지만 바뀌는 식으로 되어있어서 해당 반복 코드에 대한 리팩토링은 필요할것 같다.