시작하기
설치
명령어 하나만 실행하면 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>
)
}
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")) // 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>
)
}
import { useForm } from "react-hook-form"
export default function App() {
const { register, handleSubmit } = useForm()
const onSubmit = (data) => console.log(data)
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("firstName")} />
<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>
)
}
import { useForm } from "react-hook-form"
export default function App() {
const { register, handleSubmit } = useForm()
const onSubmit = (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>
)
}
import { forwardRef } from "react"
import { useForm } from "react-hook-form"
// The following component is an example of your existing Input Component
const Input = ({ label, register, required }) => (
<>
<label>{label}</label>
<input {...register(label, { required })} />
</>
)
// you can use forwardRef to pass the ref too
const Select = forwardRef(({ 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()
const onSubmit = (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>
)
}
import Select from "react-select"
import { useForm, Controller } from "react-hook-form"
import { Input } from "@mui/material"
const App = () => {
const { control, handleSubmit } = useForm({
defaultValues: {
firstName: "",
select: {},
},
})
const onSubmit = (data) => console.log(data)
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name="firstName"
control={control}
render={({ field }) => <Input {...field} />}
/>
<Controller
name="select"
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>
)
}
import { useForm, Controller } from "react-hook-form"
import { Checkbox } from "@mui/material"
function App() {
const { handleSubmit, control, reset } = useForm({
defaultValues: {
MyCheckbox: false,
},
})
const onSubmit = (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>
)
}
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { z } from "zod"
import { Form, FormControl, FormField } from "@/components/ui/form"
import { Input } from "@/components/ui/input"
const FormSchema = z.object({
username: z.string().min(2, {
message: "Username must be at least 2 characters.",
}),
})
export function InputForm() {
const form = useForm({
resolver: zodResolver(FormSchema),
defaultValues: {
username: "",
},
})
function onSubmit(data: z.output<typeof FormSchema>) {
console.log(data)
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormControl>
<Input {...field} />
</FormControl>
)}
/>
<button type="submit">Submit</button>
</form>
</Form>
)
}
참고: 위 shadcn/ui 예시에는 @hookform/resolvers와 zod를 설치해야 합니다.
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 { TextField } from "@mui/material"
import { useController, useForm } from "react-hook-form"
function Input({ control, name }) {
const {
field,
fieldState: { invalid, isTouched, isDirty },
formState: { touchedFields, dirtyFields },
} = useController({
name,
control,
rules: { required: true },
})
return (
<TextField
onChange={field.onChange} // send the value to hook form
onBlur={field.onBlur} // notify when the input is touched or blurred
value={field.value} // input value
name={field.name} // send the input name
inputRef={field.ref} // send input ref, so we can focus on the input when an error appears
/>
)
}
전역 상태와 연동하기
이 라이브러리는 상태 관리 라이브러리에 의존할 필요가 없지만, 필요한 경우 쉽게 연동할 수 있습니다.
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>
)
}
스키마 검증
스키마 기반 폼 검증도 지원합니다. 선택적인 설정으로 schema를 useForm에 전달할 수 있습니다. 입력 데이터를 스키마에 맞춰 검증하고 errors 또는 유효한 결과를 반환합니다. 지원하는 라이브러리는 다음과 같습니다.
- Yup
- Zod
- Superstruct
- Joi
- Vest
- Class Validator
- io-ts
- Nope
- computed-types
- typanion
- Ajv
- TypeBox
- ArkType
- Valibot
- TypeSchema
- effect-ts
- VineJS
fluentvalidation-ts
- ata-validator
다음을 구현하는 모든 라이브러리 Standard Schema
전체 최신 목록은 @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의 다음 발표에서는 몇 가지 아이디어와 설계 패턴을 소개합니다.