본문으로 건너뛰기

자주 묻는 질문

React Hook Form의 성능

성능은 이 라이브러리를 만든 주된 이유 중 하나입니다. React Hook Form은 비제어 폼을 사용하므로 register 함수가 ref를 직접 캡처하며 value/onChange를 사용하지 않습니다. 제어 상태를 유지해야 하는 컴포넌트에서는 Controller 또는 useController가 전체 폼이 아닌 해당 필드로 리렌더링을 격리합니다. 이 접근 방식은 사용자가 입력에 타이핑하거나 폼 또는 애플리케이션 루트의 다른 폼 값이 변경될 때 발생하는 리렌더링 횟수를 줄입니다. 오버헤드가 적기 때문에 제어 컴포넌트보다 페이지에 더 빠르게 마운트됩니다. 간단한 비교 테스트는 이 저장소 링크를 참고하세요.


접근성 있는 입력 오류와 메시지를 만드는 방법

React Hook Form은 비제어 컴포넌트를 기반으로 하므로 접근성 있는 사용자 정의 폼을 쉽게 만들 수 있습니다. (비제어 컴포넌트에 관한 자세한 내용은 컴포넌트 간 State 공유하기를 참고하세요). React의 제어 및 비제어 입력 가이드도 참고하세요.

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

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

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

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

클래스 컴포넌트에서도 동작하나요?

기본적으로는 동작하지 않습니다. 필요한 경우 래퍼를 만들어 클래스 컴포넌트에서 사용할 수 있습니다.

클래스 컴포넌트 안에서는 훅을 사용할 수 없지만, 하나의 트리에서 클래스와 훅을 사용하는 함수 컴포넌트를 함께 사용할 수 있습니다. 컴포넌트가 클래스인지 훅을 사용하는 함수인지는 해당 컴포넌트의 구현 세부 사항일 뿐입니다. 장기적으로는 훅이 React 컴포넌트를 작성하는 주된 방식이 될 것으로 예상합니다.


React의 <Activity /> 컴포넌트에서도 동작하나요?

v7.85.0부터 동작합니다. <Activity mode="hidden">으로 폼을 숨기면 효과와 구독이 마운트 해제되고 다시 표시될 때 재마운트됩니다. 이전에는 복원 후 useWatch 값과 formState.isSubmitting이 오래된 상태로 남았습니다. 이제 React Hook Form은 재마운트할 때 내부 구독을 다시 동기화하므로 추가 설정 없이도 <Activity />의 표시 여부가 변경되는 동안 올바른 상태를 유지합니다.


폼을 재설정하는 방법

폼을 지우는 방법은 두 가지입니다.

  • HTMLFormElement.reset()

    이 메서드는 폼의 재설정 버튼을 클릭하는 것과 같은 동작을 합니다. input/select/checkbox 값만 지웁니다.

  • React Hook Form API: reset()

    React Hook Form의 reset 메서드는 모든 필드 값을 재설정하고 폼 안의 모든 errors도 지웁니다.


폼 값을 초기화하는 방법

React Hook Form은 비제어 폼을 사용하므로 개별 필드에 defaultValue 또는 defaultChecked를 지정할 수 있습니다. 하지만 defaultValuesuseForm에 전달해 폼을 초기화하는 방식이 더 일반적이며 권장됩니다.

function App() {
const { register, handleSubmit } = useForm({
defaultValues: {
firstName: "bill",
lastName: "luo",
},
})

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

비동기 기본값에는 다음 방법을 사용할 수 있습니다.

  • 비동기 defaultValues

    function App() {
    const { register, handleSubmit } = useForm({
    defaultValues: async () => {
    const response = await fetch("/api")
    return await response.json() // return { firstName: '', lastName: '' }
    },
    })
    }
  • 반응형 values

    function App() {
    const { data } = useQuery() // data returns { firstName: '', lastName: '' }
    const { register, handleSubmit } = useForm({
    values: data,
    resetOptions: {
    keepDirtyValues: true, // keep dirty fields unchanged, but update defaultValues
    },
    })
    }

ref 사용을 공유하는 방법

React Hook Form은 입력값을 수집하기 위해 ref가 필요합니다. 하지만 화면에 스크롤하거나 포커스하는 등 다른 용도로 ref를 사용해야 할 수 있습니다.

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

type Inputs = {
firstName: string
lastName: string
}

export default function App() {
const { register, handleSubmit } = useForm<Inputs>()
const firstNameRef = useRef<HTMLInputElement | null>(null)
const onSubmit = (data: Inputs) => console.log(data)
const { ref, ...rest } = register("firstName")
const onClick = () => {
if (firstNameRef.current) firstNameRef.current.value = ""
}

return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
{...rest}
ref={(e) => {
ref(e) // pass the element to RHF
firstNameRef.current = e // keep your own reference
}}
/>
<button type="button" onClick={onClick}>
clear
</button>
<button>Submit</button>
</form>
)
}

ref에 접근할 수 없다면?

입력을 register할 때 ref가 없어도 됩니다. setValue, setError, trigger를 직접 사용할 수 있습니다.

참고: ref가 등록되지 않았으므로 React Hook Form은 입력에 이벤트 리스너를 등록할 수 없습니다. 따라서 값과 오류를 직접 업데이트해야 합니다.

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

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

useEffect(() => {
register("firstName", { required: true })
register("lastName")
}, [register])

return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
name="firstName"
onChange={(e) => setValue("firstName", e.target.value)}
/>
<input
name="lastName"
onChange={(e) => {
const value = e.target.value
if (value === "test") {
setError("lastName", { type: "notMatch" })
} else {
setValue("lastName", e.target.value)
}
}}
/>
<button>Submit</button>
</form>
)
}

첫 키 입력이 동작하지 않는 이유

value를 사용하고 있지 않은지 확인하세요. 올바른 속성은 defaultValue입니다.

React Hook Form은 비제어 입력에 초점을 맞추므로 입력 valuestate에서 onChange를 통해 변경할 필요가 없습니다. 실제로 value 자체가 필요하지 않습니다. 초기 입력값으로 defaultValue만 설정하면 됩니다.


React Hook Form, Formik, Redux Form 중 무엇을 사용해야 하나요?

먼저 모든 라이브러리는 폼 작성 경험을 최대한 간편하게 만든다는 같은 문제를 해결하려 합니다. 하지만 세 라이브러리에는 몇 가지 근본적인 차이가 있습니다. react-hook-form은 비제어 입력을 염두에 두고 만들어져 폼에 최상의 성능과 가능한 최소한의 리렌더링을 제공하려 합니다. 또한 react-hook-form은 React Hooks로 작성되고 훅으로 사용되므로 가져올 컴포넌트가 없습니다. 자세한 차이점은 다음과 같습니다.

React Hook FormFormikRedux Form
컴포넌트비제어제어제어제어
렌더링최소한의 리렌더링과 최적화된 계산로컬 상태 변경에 따라 리렌더링(입력에 타이핑할 때)상태 관리 라이브러리(Redux) 변경에 따라 리렌더링(입력에 타이핑할 때)
API컴포넌트(RenderProps, Form, Field) + 훅컴포넌트(RenderProps, Form, Field)
패키지 크기작음
Bundlephobia에서 최신 크기 확인
중간
Bundlephobia에서 최신 크기 확인

Bundlephobia에서 최신 크기 확인
검증내장
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
또는 직접 구현
직접 구현 또는 Yup직접 구현 또는 플러그인
학습 곡선낮음~중간중간중간

watch와 getValues, state 비교

  • watch: 이벤트 리스너를 통해 모든 입력 또는 지정한 입력의 변경을 구독하고 구독한 필드에 따라 리렌더링합니다. 실제 동작은 이 CodeSandbox를 참고하세요. 자세한 내용은 watch API 문서를 확인하세요.
  • getValues: 커스텀 훅 내부에 참조로 저장된 값을 빠르고 적은 비용으로 가져옵니다. 이 메서드는 리렌더링을 실행하지 않습니다.
  • 로컬 상태: React 로컬 상태는 입력 상태 이상의 정보를 나타내며 무엇을 렌더링할지도 결정합니다. 각 입력이 변경될 때마다 실행됩니다.

삼항 연산자로 기본값이 올바르게 변경되지 않는 이유

React Hook Form은 전체 폼과 입력을 제어하지 않으므로 React는 실제 입력이 교체되었음을 인식하지 못합니다. 입력에 고유한 key prop을 지정하면 이 문제를 해결할 수 있습니다. key prop에 관한 자세한 내용은 Kent C. Dodds의 글을 참고하세요.

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

export default function App() {
const { register, watch } = useForm()
const watchChecked = watch("checkbox")

return (
<div>
<input type="checkbox" {...register("checkbox")} />
{watchChecked ? (
<input {...register("input3")} key="key1" defaultValue="1" />
) : (
<input {...register("input4")} key="key2" defaultValue="2" />
)}
</div>
)
}

모달 또는 탭 폼을 다루는 방법

React Hook Form은 각 입력 안에 입력 상태를 저장해 네이티브 폼 동작을 따릅니다(registeruseEffect에서 사용자 정의한 경우 제외). 마운트되거나 마운트 해제된 입력에 입력 상태가 그대로 남는다는 오해가 흔하며, 모달이나 탭 폼을 다룰 때 자주 발생합니다. 올바른 해결 방법은 각 모달이나 탭 안에 새 폼을 만들고 제출 데이터를 로컬 또는 전역 상태에 저장한 다음 결합된 데이터를 처리하는 것입니다.

또는 shouldUnregister: false(기본값)를 사용해 useForm을 호출할 수 있습니다.

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

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

return (
<Controller
render={({ field }) => <input {...field} />}
name="firstName"
control={control}
defaultValue=""
/>
)
}