React Hook Form의 성능
성능은 이 라이브러리가 만들어지게 된 주요 이유 중 하나입니다. React Hook Form은 비제어 폼(uncontrolled form)에 의존하며, 이 때문에 register
함수가 ref
를 캡처하고, 제어 컴포넌트는 Controller
또는 useController
를 통해 리렌더링 범위를 갖습니다. 이 접근 방식은 사용자가 입력 필드에 타이핑하거나 폼의 루트에서 다른 폼 값이 변경될 때 발생하는 리렌더링 횟수를 줄입니다. 컴포넌트는 제어 컴포넌트보다 오버헤드가 적기 때문에 페이지에 더 빠르게 마운트됩니다. 참고로, 이 레포 링크에서 간단한 비교 테스트를 확인할 수 있습니다.
접근 가능한 입력 오류와 메시지를 만드는 방법
React Hook Form은 비제어 컴포넌트(Uncontrolled Components)를 기반으로 하여, 접근 가능한 커스텀 폼을 쉽게 구축할 수 있게 해줍니다.
(비제어 컴포넌트에 대한 더 자세한 정보는 컴포넌트 간 상태 공유를 참고하세요.)
import React from "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">이름</label><inputid="firstName"aria-invalid={errors.firstName ? "true" : "false"}{...register("firstName", { required: true })}/>{errors.firstName && <span role="alert">이 필드는 필수입니다</span>}<input type="submit" /></form>)}
클래스 컴포넌트와 함께 사용할 수 있나요?
기본적으로는 지원하지 않습니다. 클래스 컴포넌트에서 사용하려면 래퍼를 만들어서 사용해야 합니다.
클래스 컴포넌트 내부에서는 훅을 사용할 수 없지만, 클래스 컴포넌트와 훅을 사용하는 함수 컴포넌트를 하나의 트리에서 혼합하여 사용할 수 있습니다. 컴포넌트가 클래스인지 훅을 사용하는 함수인지는 단순히 그 컴포넌트의 구현 방식일 뿐입니다. 장기적으로는 훅이 React 컴포넌트를 작성하는 주요 방식이 될 것으로 예상됩니다.
폼을 초기화하는 방법
폼을 초기화하는 방법은 두 가지가 있습니다:
-
HTMLFormElement.reset()
이 메서드는 폼의 리셋 버튼을 클릭하는 것과 동일한 동작을 수행합니다.
input
,select
,checkbox
값만 초기화합니다. -
React Hook Form API:
reset()
React Hook Form의
reset
메서드는 모든 필드 값을 초기화하고, 폼 내의 모든errors
도 함께 지웁니다.
폼 값 초기화 방법
React Hook Form은 비제어 폼을 기반으로 동작하기 때문에, 각 필드에 defaultValue
나 defaultChecked
를 지정할 수 있습니다. 하지만 일반적으로 useForm
에 defaultValues
를 전달하여 폼을 초기화하는 방법을 더 많이 사용하고 권장합니다.
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, // 변경된 필드는 유지하고, defaultValues만 업데이트},})}
ref 사용법 공유하기
React Hook Form은 입력 값을 수집하기 위해 ref
가 필요합니다. 하지만 ref
를 다른 목적으로 사용하고 싶을 수도 있습니다. 예를 들어, 화면으로 스크롤하거나 포커스를 맞추는 경우가 있습니다.
import { useRef, useImperativeHandle } from "react"import { useForm } from "react-hook-form"type Inputs = {firstName: stringlastName: string}export default function App() {const { register, handleSubmit } = useForm<Inputs>()const firstNameRef = useRef<HTMLInputElement>(null)const onSubmit = (data: Inputs) => console.log(data)const { ref, ...rest } = register("firstName")const onClick = () => {firstNameRef.current!.value = ""}useImperativeHandle(ref, () => firstNameRef.current)return (<form onSubmit={handleSubmit(onSubmit)}><input {...rest} ref={firstNameRef} /><button type="button" onClick={onClick}>clear</button><button>Submit</button></form>)}
ref에 접근할 수 없는 경우
ref
없이도 입력 필드를 register
할 수 있습니다. 실제로 setValue
, setError
, trigger
를 수동으로 호출할 수 있습니다.
참고: ref
가 등록되지 않았기 때문에 React Hook Form은 입력 필드에 이벤트 리스너를 등록할 수 없습니다. 이는 값을 수동으로 업데이트하고 오류를 처리해야 함을 의미합니다.
import React, { 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")}, [])return (<form onSubmit={handleSubmit(onSubmit)}><inputname="firstName"onChange={(e) => setValue("firstName", e.target.value)}/><inputname="lastName"onChange={(e) => {const value = e.target.valueif (value === "test") {setError("lastName", "notMatch")} else {setValue("lastName", e.target.value)}}}/><button>Submit</button></form>)}
첫 번째 키 입력이 동작하지 않는 이유는 무엇인가요?
value
를 사용하지 않았는지 확인하세요. 올바른 속성은 defaultValue
입니다.
React Hook Form은 비제어 입력(uncontrolled inputs)에 초점을 맞추고 있습니다. 이는 onChange
를 통해 state
로 입력 value
를 변경할 필요가 없다는 의미입니다. 사실, value
는 전혀 필요하지 않습니다. 초기 입력 값에는 defaultValue
만 설정하면 됩니다.
import { useForm } from "react-hook-form/dist/index.ie11" // V6import { useForm } from "react-hook-form/dist/react-hook-form.ie11" // V5'// Resolversimport { yupResolver } from "@hookform/resolvers/dist/ie11/yup"
React Hook Form, Formik, Redux Form 비교
먼저, 이 세 라이브러리는 모두 동일한 문제를 해결하려고 합니다: 폼 구축 경험을 가능한 한 쉽게 만드는 것입니다. 하지만 이 세 가지 라이브러리 사이에는 근본적인 차이점이 있습니다. react-hook-form
은 비제어 입력(uncontrolled inputs)을 염두에 두고 설계되었으며, 최고의 성능과 최소한의 리렌더링을 제공하려고 합니다. 또한, react-hook-form
은 React Hooks를 기반으로 만들어졌으며, 훅으로 사용되기 때문에 컴포넌트를 임포트할 필요가 없습니다. 아래는 세 라이브러리의 주요 차이점을 정리한 표입니다:
React Hook Form | Formik | Redux Form | |
---|---|---|---|
컴포넌트 | 비제어(uncontrolled) & 제어(controlled) | 제어(controlled) | 제어(controlled) |
렌더링 | 최소한의 리렌더링과 계산 최적화 | 로컬 상태 변경에 따른 리렌더링 (입력 시마다 리렌더링) | 상태 관리 라이브러리(Redux) 변경에 따른 리렌더링 (입력 시마다 리렌더링) |
API | 훅(Hooks) | 컴포넌트 (RenderProps, Form, Field) + 훅 | 컴포넌트 (RenderProps, Form, Field) |
패키지 크기 | 작음 react-hook-form@7.27.0 8.5KB | 중간 formik@2.1.4 15KB | 큼 redux-form@8.3.6 26.4KB |
유효성 검사 | 내장, Yup, Zod, Joi, Superstruct 및 커스텀 유효성 검사 지원 | 직접 구현 또는 Yup | 직접 구현 또는 플러그인 |
학습 곡선 | 낮음에서 중간 | 중간 | 중간 |
watch vs getValues vs state
- watch: 모든 입력 또는 특정 입력의 변경 사항을 이벤트 리스너를 통해 구독하고, 구독된 필드에 따라 리렌더링을 수행합니다. 실제 동작을 확인하려면 이 codesandbox를 참고하세요.
- getValues: 커스텀 훅 내부에 저장된 값을 참조로 가져옵니다. 빠르고 가볍게 동작하며, 이 메서드는 리렌더링을 트리거하지 않습니다.
- local state: React의 로컬 상태는 단순히 입력 상태뿐만 아니라 무엇을 렌더링할지도 결정합니다. 이는 각 입력 변경 시마다 트리거됩니다.
삼항 연산자로 기본값이 제대로 변경되지 않는 이유는 무엇인가요?
React Hook Form은 여러분의 전체 폼과 입력 필드를 제어하지 않습니다. 그렇기 때문에 React는 실제 입력 필드가 교체되었는지 인식하지 못합니다. 이 문제를 해결하려면 입력 필드에 고유한 key
prop을 제공하면 됩니다. Kent C. Dodds가 작성한 이 글에서 key prop에 대해 더 자세히 알아볼 수 있습니다.
import React from "react"import { useForm } from "react-hook-form"export default function App() {const { register } = useForm()return (<div>{watchChecked ? (<input {...register("input3")} key="key1" defaultValue="1" />) : (<input {...register("input4")} key="key2" defaultValue="2" />)}</div>)}
모달 또는 탭 폼 작업 방법
React Hook Form은 각 입력 내부에 입력 상태를 저장함으로써 네이티브 폼 동작을 따릅니다(단, useEffect
에서 커스텀 register
를 사용하는 경우는 제외). 일반적인 오해 중 하나는 입력 상태가 마운트되거나 언마운트된 입력과 함께 유지된다는 것입니다. 예를 들어 모달이나 탭 폼을 작업할 때 이런 오해가 발생할 수 있습니다. 대신, 올바른 해결책은 각 모달이나 탭 내부에 새로운 폼을 구축하고, 제출 데이터를 로컬 또는 전역 상태로 캡처한 후 결합된 데이터로 작업을 수행하는 것입니다.
또는 useForm
을 호출할 때 더 이상 사용되지 않는 옵션인 shouldUnregister: false
를 사용할 수도 있습니다.
import { useForm, Controller } from "react-hook-form"function App() {const { control } = useForm()return (<Controllerrender={({ field }) => <input {...field} />}name="firstName"control={control}defaultValue=""/>)}
여러분의 지원에 감사드립니다
React Hook Form이 프로젝트에서 유용하다면, GitHub에서 스타를 눌러 지원해 주세요.