React Hook Form은 React에서 폼을 쉽게 관리할 수 있도록 도와주는 라이브러리다. 비제어 컴포넌트 방식을 기반으로, 제어 컴포넌트 방식보다 간결한 코드로 폼을 관리할 수 있다.
공식 홈페이지에서는 가벼운 패키징, 직관적인 api, 리렌더링 최소화를 통한 성능 향상 등을 장점으로 소개하고 있다.
ref를 활용한 비제어 방식으로 동작yup, zod 등의 스키마 기반 검증 라이브러리를 쉽게 통합가능React에서 제어 컴포넌트와 비제어 컴포넌트는 폼 데이터(입력값) React에서 다루는지, DOM에서 다루는지에 따라 구분된다.
비제어 컴포넌트는 React에서 상태(state)를 직접 관리하지 않고, DOM 요소의 ref를 통해 값을 제어하는 방식으로 입력값 변경에 따른 리렌더링을 유발하지 않기 때문에 React Hook Form은 이 방식을 기본으로 채택하여 불필요한 렌더링을 방지한다.
import { useRef } from "react";
function UncontrolledForm() {
const inputRef = useRef();
const handleSubmit = (event) => {
event.preventDefault();
alert(`입력된 값: ${inputRef.current.value}`);
};
return (
<form onSubmit={handleSubmit}>
<input type="text" ref={inputRef} />
<button type="submit">제출</button>
</form>
);
}
제어 컴포넌트는 React의 useState를 사용하여 입력값을 상태로 관리하는 방식으로 입력값이 변경될 때마다 상태가 업데이트되고, 리렌더링이 발생한다.
import { useState } from "react";
function ControlledForm() {
const [value, setValue] = useState("");
const handleSubmit = (event) => {
event.preventDefault();
alert(`입력된 값: ${value}`);
};
return (
<form onSubmit={handleSubmit}>
<input type="text" value={value} onChange={(e) => setValue(e.target.value)} />
<button type="submit">제출</button>
</form>
);
}
React Hook Form은 기본적으로 useForm 훅을 사용해 비제어 컴포넌트를 다루는 방법을 소개한다. 이는 본인들이 내세우는 장점이 극대화 되는 상황이 비제어 컴포넌트를 사용하는 상황이기 때문이다.
register: value ref onChange 등 입력 필드 등록 함수handleSubmit: 콜백 함수를 받아 폼 제출 시 실행watch: 입력값에 대한 실시간 감지 함수setValue: 특정 필드 값을 설정하는 함수import { useForm } from "react-hook-form";
function FormExample() {
const { register, handleSubmit } = useForm();
const onSubmit = (data) => {
alert(`입력된 값: ${JSON.stringify(data)}`);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("name")} placeholder="이름 입력" />
<button type="submit">제출</button>
</form>
);
}
React Hook Form은 기본적으로 비제어 컴포넌트를 내세우지만 UI라이브러리 통합 등 제어 컴포넌트가 필요한 상황 또한 상당히 잘 지원한다.
Controller 컴포넌트는 React Hook Form에서 제어 컴포넌트가 필요할때 사용된다.
useController는 Controller와 동일한 기능을 제공하지만, 함수형 방식으로 사용할 수 있다.
field: 입력 필드의 제어 객체fieldState: 필드 상태 (예: 에러 여부)formState: 전체 폼 상태import { useForm, Controller } from "react-hook-form";
interface FormData {
name: string;
}
function ControlledInputForm() {
const { control, handleSubmit } = useForm<FormData>();
const onSubmit = (data: FormData) => {
alert(JSON.stringify(data));
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name="name"
control={control}
render={({ field }) => <input {...field} placeholder="이름 입력" />}
/>
<button type="submit">제출</button>
</form>
);
}
export default ControlledInputForm;
import { Control } from "react-hook-form";
declare interface FormData {
name: string;
}
function ControlledHookInputForm({control}:{control: Control<FormData>}) {
const { field } = useController({ name: "name", control });
const onSubmit = (data: FormData) => { alert(JSON.stringify(data)); };
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...field} placeholder="이름 입력" />
<button type="submit">제출</button>
</form>
);
}
React Hook Form의 register의 옵션값을 통해 유효성 검사를 쉽게 설정할 수 있다.
import { useForm } from "react-hook-form";
function ValidationForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm();
const onSubmit = (data) => {
alert(JSON.stringify(data));
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("name", { required: "이름을 입력하세요" })} />
{errors.name && <p>{errors.name.message}</p>}
<button type="submit">제출</button>
</form>
);
}
React Hook Form은 자체적인 유효성 검사 외에도 외부 라이브러리(yup, zod 등)를 통한 더욱 간단한 방법도 지원한다.
import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
const schema = yup.object().shape({
name: yup.string().required("이름을 입력하세요"),
});
function YupValidationForm() {
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: yupResolver(schema),
});
const onSubmit = (data) => {
alert(JSON.stringify(data));
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("name")} />
{errors.name && <p>{errors.name.message}</p>}
<button type="submit">제출</button>
</form>
);
}
특정 필드의 값에 따라 다른 필드의 유효성을 검사해야 하는 경우가 있다. 회원 가입을 예로들면 passwordConfirm 필드(비밀번호 확인)는 password(비밀번호)와 동일해야 한다.
password필드에 deps속성, passwordConfirm필드에 validate속성을 사용하고
watch를 통해 두 필드 값이 같은지 확인하는 방식으로 구현할 수 있다.
import { useForm } from "react-hook-form";
type FormData = {
password: string;
passwordConfirm: string;
};
function DependentValidationForm() {
const {
register,
handleSubmit,
watch,
formState: { errors },
} = useForm<FormData>({
mode: "all",
});
const onSubmit = (data: FormData) => {
alert(JSON.stringify(data));
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
type="password"
{...register("password", {
required: "비밀번호를 입력하세요",
minLength: {
value: 6,
message: "비밀번호는 최소 6자리 이상이어야 합니다",
},
deps: ["passwordConfirm"]
})}
placeholder="비밀번호"
/>
{errors.password && <p>{errors.password.message}</p>}
<input
type="password"
{...register("passwordConfirm", {
required: "비밀번호 확인을 입력하세요",
validate: (value) => value === watch("password") || "비밀번호가 일치하지 않습니다",
})}
placeholder="비밀번호 확인"
/>
{errors.passwordConfirm && <p>{errors.passwordConfirm.message}</p>}
<button type="submit">제출</button>
</form>
);
}
schema선언 당시 oneOf를 사용해 구현할 수 있다.
values: array, message): 배열에 해당하는 값들만 허용import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
const schema = yup.object().shape({
password: yup.string().required("비밀번호를 입력하세요").min(6, "비밀번호는 최소 6자리 이상이어야 합니다"),
passwordConfirm: yup
.string()
.oneOf([yup.ref("password"), null], "비밀번호가 일치하지 않습니다")
.required("비밀번호 확인을 입력하세요"),
});
type FormData = {
password: string;
passwordConfirm: string;
};
function DependentValidationForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormData>({
resolver: yupResolver(schema),
});
const onSubmit = (data: FormData) => {
alert(JSON.stringify(data));
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input type="password" {...register("password")} placeholder="비밀번호" />
{errors.password && <p>{errors.password.message}</p>}
<input type="password" {...register("passwordConfirm")} placeholder="비밀번호 확인" />
{errors.passwordConfirm && <p>{errors.passwordConfirm.message}</p>}
<button type="submit">제출</button>
</form>
);
}
yup을 사용하면 schema에서만 설정하면 되기 때문에 뷰와 분리하기도 수월하고, 수정하기도 편해지기에 유효성 검증에서 외부 라이브러리를 사용하는 것도 좋은 선택지가 될 것 같다.
Reference