React Hook Form은 사용자 경험(UX)과 개발자 경험(DX)을 설계의 중심에 두고, 성능 최적화와 웹 접근성 개선을 통해 더 부드러운 상호작용과 효율적인 폼 개발을 가능하게 하는 폼 라이브러리다.
핵심 성능 최적화 전략
공식문서 예시
import { useForm } from "react-hook-form";export default function App() {const { register, handleSubmit, watch, formState: { errors } } = useForm();// 데이터 제출 시 실행될 함수const onSubmit = data => console.log(data);// 특정 입력 필드값의 변화를 실시간으로 관찰console.log(watch("example"));return (/* 1. handleSubmit은 유효성 검사 통과 시에만 onSubmit을 호출 */<form onSubmit={handleSubmit(onSubmit)}>{/* 2. register 함수를 통해 입력 필드를 훅에 등록 */}<input defaultValue="test" {...register("example")} />{/* 3. 표준 HTML 검증 규칙(required 등)을 사용하여 유효성 검사를 적용 */}<input {...register("exampleRequired", { required: true })} />{/* 4. 검증 실패 시 에러 객체를 통해 사용자에게 피드백을 제공 */}{errors.exampleRequired && <span>This field is required</span>}<input type="submit" /></form>);}
React Hook Form의 핵심은 입력 컴포넌트를 폼에 등록해 상태와 검증을 맡기는 것이다. 이를 통해 입력창의 값을 추적하고, 유효성 검사와 제출 데이터를 관리할 수 있다.
이때, 각 필드는 등록 시 고유한 name을 키로 가져야 한다.
import { useForm } from "react-hook-form";export default function App() {const { register, handleSubmit } = useForm();// ...
React Hook Form은 기존 HTML 표준 검증 방식을 그대로 지원한다. 따라서, 학습 비용이 낮고 직관적이다.
<input {...register("firstName", { required: true, maxLength: 20 })} /><input {...register("lastName", { pattern: /^[
formState의 errors 객체를 통해 실시간으로 에러 메시지를 표시할 수 있다. 접근성을 고려하여 aria-invalid 속성을 함께 사용하는 것이 좋다.
const { register, formState: { errors } } = useForm();<input{...register("mail", { required: "이메일 주소를 입력해주세요." })}aria-
더 복잡하고 강력한 검증이 필요하다면 Zod, Yup, Joi 등의 스키마 라이브러리를 사용할 수 있다. resolver 설정을 통해 폼 데이터 전체를 스키마 단위로 한 번에 검증한다.
import { zodResolver } from '@hookform/resolvers/zod';import * as z from 'zod';const schema = z.object({name: z.string().min(1,
이미 만들어진 커스텀 입력 컴포넌트와 통합할 수도 있다.
Prop 전달 방식
register 함수를 직접 하위 컴포넌트에 전달할 수 있다.
import { useForm } from "react-hook-form";const Input = ({ label, register, required }) => (<><label>{label}</
Ref 전달 방식
ref를 직접 노출하는 컴포넌트라면 ...register('name')을 그대로 사용할 수 있다.
import { useForm } from "react-hook-form";// React.forwardRef를 사용하여 ref를 하위 컴포넌트에 전달const Select = React.forwardRef(({ onChange, onBlur, name, label }, ref) => (<>
React 19 이전 버전에서는 forwardRef가 필수였지만, React 19부터는 ref가 일반 props로 취급되므로 코드가 훨씬 간결해졌다.
import { useForm } from "react-hook-form";// forwardRef 없이 ref를 일반 props로 받을 수 있다.const Select = ({ onChange, onBlur, name, label, ref }) => (<><
MUI, Ant Design, React-Select처럼 ref에 직접 접근하기 어렵거나 자체적인 상태 관리가 필요한 제어 컴포넌트들은 Controller를 사용해 연결한다.
이미 개발된 컴포넌트에 React Hook Form을 연결할 때 유용하다.
import { useForm, Controller } from "react-hook-form";import { TextField, Checkbox } from "@material-ui/core";function App() {const { control, handleSubmit } =
실제 사용 예시
// Controller로 TimeLimitDropdown 컴포넌트와 react-hook-form 연결<Controllercontrol={control}name={QNA_FORM_KEYS.timeLimit} // 폼 필드 이름 (예: "timeLimit")render={({ field: { onChange, value } }) => (
export function TimeLimitDropdown({onChange, // ← Controller가 제공하는 폼 상태 업데이트 함수selectedTime = DEFAULT_TIME_LIMIT, // ← Controller의 value (폼 상태)}: TimeLimitDropdownProps) {// 내부 상태 관리 (드롭다운 열림/닫힘)const [isOpen, setIsOpen] =
React Hook Form은 특정 상태 관리 라이브러리에 의존하지 않으므로, Redux, Zustand 등과 매우 쉽게 연동된다. 폼 데이터를 제출할 때 액션을 디스패치하기만 하면 된다.
import { useForm } from "react-hook-form";import { connect } from "react-redux";import updateAction from "./actions";// Redux와 react-hook-form을 함께 사용하는 폼 컴포넌트export default function App(props)
useForm은 폼 관리를 위한 커스텀 훅으로, 폼의 동작 방식을 결정하는 다양한 옵션 객체를 인자로 받는다.
| 속성 | 타입 | 설명 |
| mode | string | 제출 전 검증 전략을 설정 (기본값: onSubmit) |
| reValidateMode | string | 제출 후 에러가 있을 때 재검증 전략을 설정합니다. (기본값: onChange) |
| defaultValues | object | Promise | 폼의 초기값을 설정합니다. 동기/비동기 모두 지원합니다. |
| values | object | 외부 상태나 서버 데이터에 반응하여 폼 값을 실시간으로 업데이트합니다. |
| resetOptions |
사용자의 행동에 따라 언제 검증을 실행할지 결정한다.
| Mode | 설명 |
| onSubmit (기본값) | - 제출 시점에 검증 실행 - 실패 시 onChange에서 즉시 재검증 시작 |
| onBlur | - 포커스 잃을 때 검증 실행 |
| onChange | - 입력값 변경될 때마다 검증 실행 - 잦은 리렌더링 발생 |
| onTouched | - 첫 blur 후 매 change에서 검증 실행 |
| all | - blur와 change 모두에서 검증 실행 |
React Hook Form에서는 defaultValues를 통해 전체 폼의 상태를 한곳에서 관리하는 것을 권장한다.
기본 값의 동기식 및 비동기식 할당을 모두 지원한다. defaultValues: async () => fetch('/api')와 같이 서버 데이터를 직접 연결할 수 있다.
// 동기식 기본 값 설정useForm({defaultValues: {firstName: '',lastName: ''}})// 비동기식 기본 값 설정useForm({defaultValues:
주의사항:
shouldFocusError (기본값: true)
검증 실패 후 제출 시, 에러가 발생한 첫 번째 필드로 자동 포커스를 이동시킨다. 이때, 포커스 순서는 register() 호출 순서를 따른다.
단, ref가 등록된 필드만 가능하다. 아래와 같이 실제 DOM 요소와 연결되지 않은 필드는 포커스 이동이 불가능하다.
const CustomInput = ({ name }) => {// register를 컴포넌트 내부에서 호출하지만 ref 전달 안함register(name); // DOM 연결 실패return <input name={name} />;}
// 성공 - 실제 input 요소에 register 적용<input {...register("email", { required: true })} /> // ref 자동 연결<input {...register("name", { required: true })
마찬가지로, 커스텀 컴포넌트나 forwardRef 없이 만든 입력 요소는 포커스 대상에서 제외된다.
delayError (number)
delayError는 오류 메시지가 화면에 나타나는 시간을 밀리초(ms) 단위로 지연시켜준다. 사용자가 입력을 마칠 시간을 확보해주며, 검증 실패 시 오류 상태 표시를 지정된 시간만큼 늦춘다.
1. 빈칸 입력 → 제출 → 500ms 기다린 후 "필수" 표시2. "test" 입력 → 즉시 오류 메시지 제거 (지연 없음)3. 다시 빈칸 → 500ms 후 오류 재표시
사용자가 오류를 수정하면 오류 메시지는 지연 없이 즉시 제거된다. 이는 타이핑 중 불필요한 오류 깜빡임을 방지하고 사용자 경험을 개선한다.
Yup, Zod, Joi 등 외부 라이브러리를 사용하여 강력한 검증 규칙을 적용할 수 있다.
import React from "react";import { useForm } from "react-hook-form";import { zodResolver } from "@hookform/resolvers/zod";import * as z from "zod";const schema =
resolver를 사용하면 react-hook-form의 내장 검증 규칙인 required, min 등이 동작하지 않고, 검증은 전적으로 스키마에 위임된다.
// 내장 규칙 무시됨<input {...register("name", { required: true })} /> // 동작 안함// 스키마에서 처리const schema = z.object({name: z
스키마 검증이 필드 단위로 효율적으로 진행된다.
// name만 변경 → name 필드만 재검증<input {...register("name")} /><input {...register("email")} /> // 검증 안함
사용자 입력 시 한 필드씩 재검증하고 리렌더링한다.
// email만 변경 → email 필드만 리렌더링{errors.email && <span>{errors.email.message}</span>} // 다른 오류는 그대로
모든 검증을 스키마에 통합해야 한다.
const fullSchema = z.object({name: z.string().min(1).max(20),email: z.string().email(),age:
import { z } from 'zod';/*** 투표 폼 유효성 검사 제약 조건*/export const VALIDATION_CONSTRAINTS = {TITLE: {MAX_LENGTH: 50,}
| 새로운 values 업데이트 시 기존 사용자 입력값(dirty)이나 에러 유지 여부를 결정합니다. |
| shouldUnregister | boolean | 컴포넌트 언마운트 시 해당 값을 폼에서 제거할지 여부입니다. (기본값: false) |