-
React에서 form 관리하기 with react-hook-form, zodFrameworks, Platforms and Libraries/React 2025. 5. 31. 16:32
최근 운영 중인 서비스의 admin을 만들면서 form을 관리할 때 validation 체크를 좀 더 효율적으로 관리하기 위해 react-hook-form과 zod를 사용하였습니다. 해당 내용을 정리하며 react에서 react-hook-form과 zod을 이용하여 form을 관리하는 방식에 대해 살펴보겠습니다.
react-hook-form, zod 조합
react-hook-form?
react-hook-form은 React에서 간결하고 성능 좋은 폼 관리를 제공하는 라이브러리로 최소한의 리렌더링과 쉬운 유효성 검증을 지원합니다.
zod?
zod는 TypeScript 기반의 스키마 선언 및 유효성 검사를 위한 라이브러리로 정적 타입과 런타임 검증을 동시에 처리할 수 있습니다.
react-hook-form + zod
zod를 이용하여 생성된 TypeScript 기반의 스키마를 react-hook-form을 이용한 유효성 검증에 사용합니다. zod를 통해 form의 각 필드에 맞는 validation을 각각 지정하여 하나의 스키마로 관리하기 때문에 각 form의 필드에 일일이 validation 조건 체크 로직을 걸어두지 않아도 돼서 코드를 간결하게 작성할 수 있습니다.
자세한 내용은 사용 방법을 통해 살펴보겠습니다.
react-hook-form, zod 사용 방법
먼저 react-hook-form과 zod를 설치합니다. react-hook-form에서 zod를 같이 사용하기 위해 @hookform/resolvers도 같이 설치합니다.
npm install react-hook-form zod @hookform/resolvers # or yarn add react-hook-form zod @hookform/resolvers
zod를 사용한 스키마 생성에 대한 예제는 아래와 같습니다.
import * as z from 'zod'; export const Status = { SUCCESS: 'success', FAIL: 'fail', } as const; export type Status = (typeof Status)[keyof typeof Status]; export const ValidationForm = z.object({ title: z.string(), count: z.number(), isActive: z.boolean(), productCode: z.nativeEnum(Status), }); export type ValidationFormData = z.infer<typeof ValidationForm>;
예제를 위해 간단히 작성되었지만 스키마 생성 시 validation을 좀 더 상세하게 설정할 수도 있습니다.
... export const ValidationDetailForm = z.object({ title: z.string().min(1), email: z.string().email(), date: z.date(), essentialValue: z.string().nonempty(), phoneNumber: z .string() .regex(/^(01([0|1|6|7|8|9])-([0-9]{3,4})-([0-9]{4}))$|^([0-9]{10,11})$/), count: z.number().int().gte(1), productCode: z.union([z.nativeEnum(Status), z.literal("")]), list: z.array(z.string()), }); ...
최솟값 최댓값에 대한 validation, 정규 표현식에 대한 validation, 여러 type을 하나의 필드에 validation 적용 등 체이닝을 통해 더 상세한 validation 조건을 추가할 수 있습니다. 해당 내용은 zod의 스키마 정의 페이지에서 확인할 수 있습니다.
zod를 통해 생성된 스키마와 react-hook-form을 사용하여 form을 작성해 보겠습니다.
import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { ValidationForm, ValidationFormData, Status } from '@/util/validation'; export default function ExampleForm() { const { register, handleSubmit } = useForm<ValidationFormData>({ resolver: zodResolver(ValidationForm), }); const onSubmit = async (data: ValidationFormData) => { const parsedData = ValidationForm.safeParse(data); console.log(parsedData); }; return ( <div> <h1>Example Form</h1> <form onSubmit={handleSubmit(onSubmit)}> <div> <h3>title</h3> <input type="text" {...register('title')} /> </div> <div> <h3>count</h3> <input type="number" {...register('count')} /> </div> <div> <h3>isActive</h3> <input type="checkbox" {...register('isActive')} /> </div> <div> <h3>status</h3> <div> <input type="radio" value={Status.SUCCESS} {...register('productCode')} /> <label>success</label> </div> <div> <input type="radio" value={Status.FAIL} {...register('productCode')} /> <label>fail</label> </div> </div> <button type="submit">저장</button> </form> </div> ); }
react-hook-form을 통해 생성된 method인 register을 통해 HTML input에 맵핑하여 zod로 작성된 validation 조건 검증을 적용합니다. 모든 validation 조건이 충족이 되었을 경우 handleSubmit을 통해 검증된 form data가 전달됩니다.
Validation error text
form을 작성할 때 validation 조건이 충족되지 않았을 경우 보통 에러 텍스트를 노출합니다. 해당 내용도 react-hook-form과 zod를 통해 처리할 수 있습니다.
... export const ValidationForm = z.object({ title: z.string().min(1, '입력해주세요'), ... }); ...
... export default function ExampleForm() { const { register, formState: { errors }, handleSubmit, } = useForm<ValidationFormData>({ resolver: zodResolver(ValidationForm), }); ... return ( <div> <h1>Example Form</h1> <form onSubmit={handleSubmit(onSubmit)}> <div> <h3>title</h3> <input type="text" {...register('title')} /> {errors.title && ( <p className="text-error">{errors.title.message}</p> )} </div> ... <button type="submit">저장</button> </form> </div> ); }
zod 스키마 생성 시에 validation 조건에 적절한 에러 텍스트를 작성합니다. 그리고 component 내부에서 react-hook-form의 useForm을 통해 formState.errors로 각 필드에 맞는 에러 텍스트를 노출시킵니다.
HTML input이 아닌 커스텀 컴포넌트 case
실제로 서비스를 작업하다 보면 HTML input 대신에 외부 라이브러리 컴포넌트(MUI, date picker 등) 또는 자체 제작 컴포넌트를 사용하는 케이스가 있습니다. 해당 케이스에서 react-hook-form을 사용하는 방법에 대해 살펴보겠습니다.
외부 컴포넌트에서 react-hook-form으로 유효성 검증을 위해 Controller을 사용합니다. 예시를 위해 react-datepicker을 이용한 date 설정 케이스를 보겠습니다.
import { Controller, useForm } from 'react-hook-form'; import DatePicker from 'react-datepicker'; import { zodResolver } from '@hookform/resolvers/zod'; import { ValidationForm, ValidationFormData, Status } from '@/util/validation'; export default function ExampleForm() { const { control, register, formState: { errors }, handleSubmit, } = useForm<ValidationFormData>({ resolver: zodResolver(ValidationForm), }); ... return ( <div> <h1>Example Form</h1> <form onSubmit={handleSubmit(onSubmit)}> ... <div> <h3>date</h3> <Controller name="date" control={control} render={({ field: { value, onChange } }) => ( <DatePicker selected={value} onChange={onChange} /> )} /> </div> <button type="submit">저장</button> </form> </div> ); }
이전에 HTML input에선 register를 사용했지만 외부 컴포넌트의 경우 Controller wrapper을 통해 쉽게 연결할 수 있습니다.
마무리
개발 환경마다 form validation이나 컴포넌트 구성 방식에 차이가 있을 수 있어 이번 포스팅에서는 react-hook-form과 zod의 기본적인 사용 방식 위주로 작성되었습니다. 이번에 react-hook-form과 zod를 사용하면서 엄격한 validation 체크를 좀 더 쉽고 명확하게 구현할 수 있었습니다. React 환경에서 form 구성 시 react-hook-form과 zod 사용을 고려해 보는 것도 좋겠습니다.
참고 자료
'Frameworks, Platforms and Libraries > React' 카테고리의 다른 글
Zustand 맛보기 (0) 2025.02.28 Goodbye Recoil (2) 2024.10.31 useState 함수형 업데이트 (2) 2024.08.18 String to HTML, dangerouslySetInnerHTML (0) 2024.06.09 Recoil 도입기 (0) 2024.05.19