본문으로 건너뛰기

시작하기

설치

명령어 하나만 실행하면 React Hook Form 설치가 완료됩니다.

npm install react-hook-form

예시

다음 코드는 기본 사용법을 보여 줍니다.

import { useForm, SubmitHandler } from "react-hook-form"

type Inputs = {
example: string
exampleRequired: string
}

export default function App() {
const {
register,
handleSubmit,
watch,
formState: { errors },
} = useForm<Inputs>()
const onSubmit: SubmitHandler<Inputs> = (data) => console.log(data)

console.log(watch("example")) // watch input value by passing its name

return (
/* "handleSubmit" will validate your inputs before invoking "onSubmit" */
<form onSubmit={handleSubmit(onSubmit)}>
{/* register your input into the hook by invoking the "register" function */}
<input defaultValue="test" {...register("example")} />

{/* include validation with required or other standard HTML validation rules */}
<input {...register("exampleRequired", { required: true })} />
{/* errors will return when field validation fails */}
{errors.exampleRequired && <span>This field is required</span>}

<input type="submit" />
</form>
)
}

React Web 동영상 튜토리얼

이 동영상 튜토리얼에서는 React Hook Form의 기본 사용법과 개념을 설명합니다.

필드 등록하기

React Hook Form의 핵심 개념 중 하나는 컴포넌트를 훅에 **register**하는 것입니다. 그러면 컴포넌트의 값을 폼 검증과 제출에 모두 사용할 수 있습니다.

참고: 등록 과정에서 키로 사용할 name은 각 필드에 반드시 있어야 합니다.

import { useForm, SubmitHandler } from "react-hook-form"

enum GenderEnum {
female = "female",
male = "male",
other = "other",
}

interface IFormInput {
firstName: string
gender: GenderEnum
}

export default function App() {
const { register, handleSubmit } = useForm<IFormInput>()
const onSubmit: SubmitHandler<IFormInput> = (data) => console.log(data)

return (
<form onSubmit={handleSubmit(onSubmit)}>
<label>First Name</label>
<input {...register("firstName")} />
<label>Gender Selection</label>
<select {...register("gender")}>
<option value="female">female</option>
<option value="male">male</option>
<option value="other">other</option>
</select>
<input type="submit" />
</form>
)
}

검증 적용하기

React Hook Form은 기존 HTML 폼 검증 표준에 맞춰 폼 검증을 간편하게 만듭니다.

지원하는 검증 규칙은 다음과 같습니다.

  • required
  • min
  • max
  • minLength
  • maxLength
  • pattern
  • validate

각 규칙의 자세한 내용은 register 절을 참고하세요.

import { useForm, SubmitHandler } from "react-hook-form"

interface IFormInput {
firstName: string
lastName: string
age: number
}

export default function App() {
const { register, handleSubmit } = useForm<IFormInput>()
const onSubmit: SubmitHandler<IFormInput> = (data) => console.log(data)

return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("firstName", { required: true, maxLength: 20 })} />
<input {...register("lastName", { pattern: /^[A-Za-z]+$/i })} />
<input type="number" {...register("age", { min: 18, max: 99 })} />
<input type="submit" />
</form>
)
}

기존 폼 연동하기

기존 폼을 연동하는 방법은 간단합니다. 컴포넌트의 ref를 등록하고 관련 props를 입력에 할당하는 것이 중요합니다.

import { forwardRef } from "react"
import {
FieldPath,
useForm,
UseFormRegister,
SubmitHandler,
} from "react-hook-form"

interface IFormValues {
"First Name": string
Age: number
}

type InputProps = {
label: FieldPath<IFormValues>
register: UseFormRegister<IFormValues>
required: boolean
}

// The following component is an example of your existing Input Component
const Input = ({ label, register, required }: InputProps) => (
<>
<label>{label}</label>
<input {...register(label, { required })} />
</>
)

// you can use forwardRef to pass the ref too
const Select = forwardRef<
HTMLSelectElement,
{ label: string } & ReturnType<UseFormRegister<IFormValues>>
>(({ onChange, onBlur, name, label }, ref) => (
<>
<label>{label}</label>
<select name={name} ref={ref} onChange={onChange} onBlur={onBlur}>
<option value="20">20</option>
<option value="30">30</option>
</select>
</>
))

const App = () => {
const { register, handleSubmit } = useForm<IFormValues>()

const onSubmit: SubmitHandler<IFormValues> = (data) => {
alert(JSON.stringify(data))
}

return (
<form onSubmit={handleSubmit(onSubmit)}>
<Input label="First Name" register={register} required />
<Select label="Age" {...register("Age")} />
<input type="submit" />
</form>
)
}

UI 라이브러리와 연동하기

React Hook Form은 외부 UI 컴포넌트 라이브러리와 쉽게 연동할 수 있습니다. 컴포넌트가 입력의 ref를 노출하지 않는다면 등록 과정을 처리하는 Controller 컴포넌트를 사용해야 합니다.

import Select from "react-select"
import { useForm, Controller, SubmitHandler } from "react-hook-form"
import { Input } from "@mui/material"

interface IFormInput {
firstName: string
lastName: string
iceCreamType: { label: string; value: string }
}

const App = () => {
const { control, handleSubmit } = useForm<IFormInput>({
defaultValues: {
firstName: "",
lastName: "",
iceCreamType: { label: "", value: "" },
},
})

const onSubmit: SubmitHandler<IFormInput> = (data) => {
console.log(data)
}

return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name="firstName"
control={control}
render={({ field }) => <Input {...field} />}
/>
<Controller
name="iceCreamType"
control={control}
render={({ field }) => (
<Select
{...field}
options={[
{ value: "chocolate", label: "Chocolate" },
{ value: "strawberry", label: "Strawberry" },
{ value: "vanilla", label: "Vanilla" },
]}
/>
)}
/>
<input type="submit" />
</form>
)
}

제어 입력 연동하기

이 라이브러리는 비제어 컴포넌트와 네이티브 HTML 입력을 지향합니다. 하지만 shadcn/ui, React-Select, AntD, MUI 같은 외부 제어 컴포넌트를 사용해야 할 때가 있습니다. 이를 간소화하기 위해 사용자 정의 register를 자유롭게 사용하면서도 연동 과정을 단순화하는 래퍼 컴포넌트 Controller를 제공합니다.

컴포넌트 API 사용하기

import { useForm, Controller, SubmitHandler } from "react-hook-form"
import { Checkbox } from "@mui/material"

interface IFormInputs {
MyCheckbox: boolean
}

function App() {
const { handleSubmit, control, reset } = useForm<IFormInputs>({
defaultValues: {
MyCheckbox: false,
},
})
const onSubmit: SubmitHandler<IFormInputs> = (data) => console.log(data)

return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name="MyCheckbox"
control={control}
rules={{ required: true }}
render={({ field }) => <Checkbox {...field} />}
/>
<input type="submit" />
</form>
)
}

참고:shadcn/ui 예시에는 @hookform/resolverszod를 설치해야 합니다.

npm install @hookform/resolvers zod

Cannot find module '@hookform/resolvers/zod'(또는 zod/v4/core) 오류가 발생하면 대부분 버전이 맞지 않는 경우입니다. Zod v4를 지원하려면 @hookform/resolvers ^3.10 이상과 zod ^3.25 또는 zod v4를 함께 사용해야 하며, 패키지의 하위 경로 export를 올바르게 해석하도록 tsconfig.json"moduleResolution": "bundler"(또는 "node16"/"nodenext")가 필요합니다. 자세한 내용은 react-hook-form/resolvers#839를 참고하세요.

훅 API 사용하기

import { useForm, useController, UseControllerProps } from "react-hook-form"

type FormValues = {
FirstName: string
}

function Input(props: UseControllerProps<FormValues>) {
const { field, fieldState } = useController(props)

return (
<div>
<input {...field} placeholder={props.name} />
<p>{fieldState.isTouched && "Touched"}</p>
<p>{fieldState.isDirty && "Dirty"}</p>
<p>{fieldState.invalid ? "invalid" : "valid"}</p>
</div>
)
}

export default function App() {
const { handleSubmit, control } = useForm<FormValues>({
defaultValues: {
FirstName: "",
},
mode: "onChange",
})
const onSubmit = (data: FormValues) => console.log(data)

return (
<form onSubmit={handleSubmit(onSubmit)}>
<Input control={control} name="FirstName" rules={{ required: true }} />
<input type="submit" />
</form>
)
}

전역 상태와 연동하기

이 라이브러리는 상태 관리 라이브러리에 의존할 필요가 없지만, 필요한 경우 쉽게 연동할 수 있습니다.

import { useForm } from "react-hook-form"
import { connect } from "react-redux"
import updateAction from "./actions"

function App(props) {
const { register, handleSubmit, setValue } = useForm({
defaultValues: {
firstName: "",
lastName: "",
},
})
// Submit your data into Redux store
const onSubmit = (data) => props.updateAction(data)

return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("firstName")} />
<input {...register("lastName")} />
<input type="submit" />
</form>
)
}

// Connect your component with redux
export default connect(({ firstName, lastName }) => ({ firstName, lastName }), {
updateAction,
})(App)

오류 처리하기

React Hook Form은 폼 오류를 표시하는 errors 객체를 제공합니다. errors 타입은 주어진 검증 제약 조건을 반환합니다. 다음 예시는 required 검증 규칙을 보여 줍니다.

import { useForm } from "react-hook-form"

export default function App() {
const {
register,
formState: { errors },
handleSubmit,
} = useForm()
const onSubmit = (data) => console.log(data)

return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
{...register("firstName", { required: true })}
aria-invalid={errors.firstName ? "true" : "false"}
/>
{errors.firstName?.type === "required" && (
<p role="alert">First name is required</p>
)}

<input
{...register("mail", { required: "Email Address is required" })}
aria-invalid={errors.mail ? "true" : "false"}
/>
{errors.mail && <p role="alert">{errors.mail.message}</p>}

<input type="submit" />
</form>
)
}

서비스와 연동하기

React Hook Form을 서비스와 연동하려면 라이브러리에 내장된 제출 처리 기능을 사용할 수 있습니다. <Form /> 컴포넌트를 사용하면 폼 데이터를 API 엔드포인트나 다른 서비스로 쉽게 보낼 수 있습니다. Form 컴포넌트 자세히 알아보기.

import { Form } from "react-hook-form"

function App() {
const { register, control } = useForm()

return (
<Form
action="/api/save" // Send post request with the FormData
// encType={'application/json'} you can also switch to json object
onSuccess={() => {
alert("Your application is updated.")
}}
onError={() => {
alert("Submission has failed.")
}}
control={control}
>
<input {...register("firstName", { required: true })} />
<input {...register("lastName", { required: true })} />
<button>Submit</button>
</Form>
)
}

스키마 검증

스키마 기반 폼 검증도 지원합니다. 선택적인 설정으로 schemauseForm에 전달할 수 있습니다. 입력 데이터를 스키마에 맞춰 검증하고 errors 또는 유효한 결과를 반환합니다. 지원하는 라이브러리는 다음과 같습니다.

전체 최신 목록은 @hookform/resolvers 저장소를 참고하세요.

1단계: 프로젝트에 Yup을 설치합니다.

npm install @hookform/resolvers yup

2단계: 검증할 스키마를 준비하고 React Hook Form에 입력을 등록합니다.

import { useForm } from "react-hook-form"
import { yupResolver } from "@hookform/resolvers/yup"
import * as yup from "yup"

const schema = yup
.object({
firstName: yup.string().required(),
age: yup.number().positive().integer().required(),
})
.required()

export default function App() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm({
resolver: yupResolver(schema),
})
const onSubmit = (data) => console.log(data)

return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("firstName")} />
<p>{errors.firstName?.message}</p>

<input {...register("age")} />
<p>{errors.age?.message}</p>

<input type="submit" />
</form>
)
}

React Native

React Native에서도 같은 성능 향상과 개선점을 얻을 수 있습니다. 입력 컴포넌트와 연동하려면 Controller로 감쌀 수 있습니다.

import { Text, View, TextInput, Button } from "react-native"
import { useForm, Controller } from "react-hook-form"

export default function App() {
const {
control,
handleSubmit,
formState: { errors },
} = useForm({
defaultValues: {
firstName: "",
lastName: "",
},
})
const onSubmit = (data) => console.log(data)

return (
<View>
<Controller
control={control}
rules={{
required: true,
}}
render={({ field: { onChange, onBlur, value } }) => (
<TextInput
placeholder="First name"
onBlur={onBlur}
onChangeText={onChange}
value={value}
/>
)}
name="firstName"
/>
{errors.firstName && <Text>This is required.</Text>}

<Controller
control={control}
rules={{
maxLength: 100,
}}
render={({ field: { onChange, onBlur, value } }) => (
<TextInput
placeholder="Last name"
onBlur={onBlur}
onChangeText={onChange}
value={value}
/>
)}
name="lastName"
/>

<Button title="Submit" onPress={handleSubmit(onSubmit)} />
</View>
)
}

TypeScript

React Hook Form은 TypeScript로 제작되었으며 폼 값을 지원하도록 FormData 타입을 정의할 수 있습니다.

import { useForm } from "react-hook-form"

type FormData = {
firstName: string
lastName: string
}

export default function App() {
const {
register,
setValue,
handleSubmit,
formState: { errors },
} = useForm<FormData>()
const onSubmit = handleSubmit((data) => console.log(data))
// firstName and lastName will have correct type

return (
<form onSubmit={onSubmit}>
<label>First Name</label>
<input {...register("firstName")} />
<label>Last Name</label>
<input {...register("lastName")} />
<button
type="button"
onClick={() => {
setValue("lastName", "luo") // ✅
setValue("firstName", true) // ❌: true is not string
errors.bill // ❌: property bill does not exist
}}
>
SetValue
</button>
</form>
)
}

설계와 철학

React Hook Form의 설계와 철학은 사용자 및 개발자 경험에 초점을 맞춥니다. 성능을 세밀하게 조정하고 접근성을 개선해 더 원활한 상호작용 경험을 제공하는 것을 목표로 합니다. 성능 개선 사항에는 다음이 포함됩니다.

  • 프록시를 통한 폼 상태 구독 모델 도입
  • 불필요한 계산 방지
  • 필요한 경우 컴포넌트 리렌더링 격리

전반적으로 애플리케이션과 상호작용하는 사용자 경험을 개선합니다. 개발자를 위해 HTML 표준에 밀접하게 맞춘 내장 검증을 도입했으며, 강력한 검증 메서드와 스키마 검증의 네이티브 연동을 통해 기능을 확장할 수 있습니다. 또한 TypeScript를 활용해 폼의 타입을 엄격하게 검사하면 빌드 초기에 피드백을 얻어 견고한 폼 솔루션을 구축할 수 있습니다.

Bill의 다음 발표에서는 몇 가지 아이디어와 설계 패턴을 소개합니다.