Skip to content

시작하기

React Hook Form을 사용한 간단한 폼 검증.

설치

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")) // 입력값을 이름으로 전달하여 확인
return (
/* "handleSubmit"은 "onSubmit"을 호출하기 전에 입력값을 검증합니다 */
<form onSubmit={handleSubmit(onSubmit)}>
{/* "register" 함수를 호출하여 입력을 훅에 등록합니다 */}
<input defaultValue="test" {...register("example")} />
{/* 필수 입력 또는 기타 표준 HTML 검증 규칙을 포함합니다 */}
<input {...register("exampleRequired", { required: true })} />
{/* 필드 검증이 실패하면 오류가 반환됩니다 */}
{errors.exampleRequired && <span>이 필드는 필수입니다</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")) // 입력값을 이름으로 전달하여 확인
return (
/* "handleSubmit"은 "onSubmit"을 호출하기 전에 입력값을 검증합니다 */
<form onSubmit={handleSubmit(onSubmit)}>
{/* "register" 함수를 호출하여 입력을 훅에 등록합니다 */}
<input defaultValue="test" {...register("example")} />
{/* 필수 입력 또는 기타 표준 HTML 검증 규칙을 포함합니다 */}
<input {...register("exampleRequired", { required: true })} />
{/* 필드 검증이 실패하면 오류가 반환됩니다 */}
{errors.exampleRequired && <span>이 필드는 필수입니다</span>}
<input type="submit" />
</form>
)
}

React 웹 비디오 튜토리얼

이 비디오 튜토리얼은 React Hook Form의 기본 사용법과 개념을 설명합니다.

필드 등록하기

React Hook Form의 핵심 개념 중 하나는 컴포넌트를 훅에 register 하는 것입니다. 이를 통해 폼 검증과 제출 시 해당 값에 접근할 수 있습니다.

참고: 각 필드는 등록 과정에서 키로 사용될 name 속성이 필수입니다.

import ReactDOM from "react-dom"
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>
)
}

기존 폼 통합하기

기존 폼을 통합하는 것은 간단해야 합니다. 중요한 단계는 컴포넌트의 refregister하고 입력 필드에 관련 props를 할당하는 것입니다.

import { Path, useForm, UseFormRegister, SubmitHandler } from "react-hook-form"
interface IFormValues {
"First Name": string
Age: number
}
type InputProps = {
label: Path<IFormValues>
register: UseFormRegister<IFormValues>
required: boolean
}
// 다음 컴포넌트는 기존 Input 컴포넌트의 예시입니다.
const Input = ({ label, register, required }: InputProps) => (
<>
<label>{label}</label>
<input {...register(label, { required })} />
</>
)
// ref를 전달하기 위해 React.forwardRef를 사용할 수도 있습니다.
const Select = React.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 { useForm } from "react-hook-form"
// 다음 컴포넌트는 기존 Input 컴포넌트의 예시입니다.
const Input = ({ label, register, required }) => (
<>
<label>{label}</label>
<input {...register(label, { required })} />
</>
)
// ref를 전달하기 위해 React.forwardRef를 사용할 수도 있습니다.
const Select = React.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 "@material-ui/core"
interface IFormInput {
firstName: string
lastName: string
iceCreamType: { label: string; value: string }
}
const App = () => {
const { control, handleSubmit } = useForm({
defaultValues: {
firstName: "",
lastName: "",
iceCreamType: {},
},
})
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 "@material-ui/core"
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 입력을 주로 사용합니다. 하지만 React-Select, AntD, MUI와 같은 외부 제어 컴포넌트를 사용하지 않기란 쉽지 않습니다. 이를 간단하게 만들기 위해, Controller라는 래퍼 컴포넌트를 제공합니다. 이 컴포넌트는 통합 과정을 간소화하면서도 커스텀 등록을 자유롭게 사용할 수 있도록 해줍니다.

컴포넌트 API 사용하기

import { useForm, Controller, SubmitHandler } from "react-hook-form"
import { TextField, Checkbox } from "@material-ui/core"
interface IFormInputs {
TextField: string
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 { TextField } from "@material-ui/core"
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} // 값을 react-hook-form에 전달
onBlur={field.onBlur} // 입력이 터치되거나 블러될 때 알림
value={field.value} // 입력 값
name={field.name} // 입력 이름 전달
inputRef={field.ref} // 입력 참조 전달, 오류 발생 시 입력에 포커스 가능
/>
)
}

Hooks API 사용하기

import * as React from "react"
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 "@material-ui/core"
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} // 값을 hook form으로 전달
onBlur={field.onBlur} // 입력이 터치되거나 블러될 때 알림
value={field.value} // 입력 값
name={field.name} // 입력 이름 전달
inputRef={field.ref} // 입력 참조 전달, 오류 발생 시 입력에 포커스 가능
/>
)
}

글로벌 상태와 통합하기

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

import { useForm } from "react-hook-form"
import { connect } from "react-redux"
import updateAction from "./actions"
export default function App(props) {
const { register, handleSubmit, setValue } = useForm({
defaultValues: {
firstName: "",
lastName: "",
},
})
// 데이터를 Redux 스토어에 제출
const onSubmit = (data) => props.updateAction(data)
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("firstName")} />
<input {...register("lastName")} />
<input type="submit" />
</form>
)
}
// 컴포넌트를 Redux와 연결
connect(
({ firstName, lastName }) => ({ firstName, lastName }),
updateAction
)(YourForm)

에러 처리

React Hook Form은 폼에서 발생한 에러를 보여주기 위해 errors 객체를 제공합니다. errors의 타입은 주어진 유효성 검사 제약 조건을 반환합니다. 다음 예제는 필수 입력 유효성 검사 규칙을 보여줍니다.

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" // FormData와 함께 POST 요청 전송
// encType={'application/json'} JSON 객체로 전환할 수도 있음
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>
)
}

스키마 유효성 검증

Yup, Zod, Superstruct, Joi와 같은 스키마 기반 폼 유효성 검증을 지원합니다. useFormschema를 옵션으로 전달하면 입력 데이터를 스키마에 대해 검증하고 에러 또는 유효한 결과를 반환합니다.

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로 감싸면 됩니다.

Expo JS
import { Text, View, TextInput, Button, Alert } 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 * as React from "react"
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과 lastName은 올바른 타입을 가짐
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는 string 타입이 아님
errors.bill // ❌: bill 프로퍼티는 존재하지 않음
}}
>
SetValue
</button>
</form>
)
}

디자인과 철학

React Hook Form의 디자인과 철학은 사용자와 개발자 경험에 초점을 맞춥니다. 이 라이브러리는 성능을 세밀하게 조정하고 접근성을 개선하여 사용자에게 더 원활한 상호작용 경험을 제공하는 것을 목표로 합니다. 성능 향상을 위한 몇 가지 방법은 다음과 같습니다:

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

이러한 개선 사항은 사용자가 애플리케이션과 상호작용할 때 더 나은 사용자 경험을 제공합니다. 개발자 측면에서는 내장된 유효성 검사를 제공하며, HTML 표준과 밀접하게 연동되어 강력한 유효성 검사 방법과 스키마 유효성 검사를 기본적으로 통합할 수 있습니다. 또한, TypeScript를 활용하여 강력한 타입 검사를 지원함으로써 개발자가 견고한 폼 솔루션을 구축할 수 있도록 초기 빌드 단계에서 피드백을 제공합니다.

Bill이 진행한 다음 강연에서는 몇 가지 아이디어와 디자인 패턴을 소개합니다:

여러분의 지원에 감사드립니다

React Hook Form이 프로젝트에서 유용하다면, GitHub에서 스타를 눌러 지원해 주세요.