[TYPESCRIPT] zod + react-hook-form

차슈·2025년 6월 17일

TYPE SCRIPT

목록 보기
2/2
post-thumbnail

react-hook-form은 써봤는데, zod는 처음 써봐서 zod에 대한 정리를 하려고 한다!

react-hook-formzod은 React에서 폼을 간편하게 관리하고, 유효성 검사를 수행하는 데 유용한 라이브러리다. 두 개의 라이브러리를 함께 사용하면, 폼 입력 관리와 검증 과정을 매우 효율적으로 처리할 수 있다.

보통 TypeScript에서,zod를 사용하는 것이 타입을 검증하는 면에서 훨씬 유용하다는 장점이 있다.


설치

npm install react-hook-form zod @hookform/resolvers

기본 사용법

1. ✅ Zod 스키마 정의하기

import { z } from "zod";

const schema = z.object({
  // email 필드는 문자열이며,이메일 형식
  email: z.string().email(),

  // password 필드는 문자열이며, 최소 6자 이상
  password: z.string().min(6),
});

2. ✅ React Hook Form에 연결하기

import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";

const {
  register,           // 각 input 요소에 연결하는 함수 
  handleSubmit,      
  formState: { errors }, // 유효성 검사-에러처리
} = useForm({
  // 유효성 검사를 Zod 스키마를 기반 수행 설정
  resolver: zodResolver(schema),
});

3. ✅ 폼 연결! 이때 Controller나 Input에 연결


Controller란?

Controller는 React Hook Form에서 커스텀 컴포넌트나 제3자 UI 라이브러리를 폼에 연결할 때 사용하는 중간 관리자 역할!

Controller는 React Hook Form이 <input>이 아닌 커스텀 컴포넌트의 value와 onChange를 관리할 수 있게 해주는 컴포넌트

❓왜 필요한가

예를들어,
register()로 바로 연결이 가능한 컴포넌트는 연결에 문제가 되지 않는다.

<input {...register("email")} />

하지만, 내부적으로 valueonChange를 직접 제어하는 컴포넌트는 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 /> 컴포넌트에 그대로 넘기면 폼 연동이 된다.

✅ 언제 써야 하나?

  • 내부에 value, onChange를 가진 커스텀 input 컴포넌트
  • React DatePicker 등 외부 라이브러리 UI
  • 포맷팅 등 처리 로직이 들어가는 입력 컴포넌트 (ex. 생년월일, 전화번호)

생년월일을 처리할때 썼던 zod + react-hook-form 예시

📌 Zod 스키마 정의

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>;

🧩 React Hook Form 연결 + 입력값 포맷팅

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를 곁들이는 것만으로도 코드가 더 안정적이고 관리하기 쉬워진다. 위에 생년월일 예시 코드는 진짜 내가 썼던 코드인데 확실히 유효성 검사 관련해서도 쉽게 처리할 수 있었다. 하지만 같은 코드가 계속 반복되고,에러 메세지만 바뀌는 식으로 되어있어서 해당 반복 코드에 대한 리팩토링은 필요할것 같다.

0개의 댓글