React Hook Form은 form을 보다 효율적이고 편하기 관리하게 도와주는 library다. controlled form input을 직접 관리해 본다면 form update에 따른 optimization과 form error 관리를 위해 손이 상당히 많이 가는 작업임을 알 수 있다. 그리고 React Hook Form을 통해 개발자는 form state 관리, form update에 따른 optimization 그리고 form error 관리를 보다 수월하게 할 수 있다.
useForm
useForm은 form을 관리하기 위해 사용되는 가장 기본적인 hook이다. 다음 예제를 살펴보자
import React from "react";
import { useForm } from "react-hook-form";
type MemberForm = {
name: string;
age: string;
address: string;
};
const HookFormPage = () => {
const { register, handleSubmit } = useForm<MemberForm>();
const handleFormSubmit = (memberForm: MemberForm) => {
console.log({ memberForm });
};
const handleFormError = (errors: FieldErrors<MemberForm>) => {
console.log({ errors });
};
return (
<div>
<form onSubmit={handleSubmit(handleFormSubmit, handleFormError)}>
<div>
<label htmlFor="form-name">name : </label>
<input id="form-name" {...register("name", { required: true })} />
</div>
<div>
<label htmlFor="form-age">age : </label>
<input id="form-age" {...register("age")} />
</div>
<div>
<label htmlFor="form-address">address : </label>
<input id="form-address" {...register("address")} />
</div>
<div>
<button>Submit</button>
</div>
</form>
</div>
);
};
export default HookFormPage;
우리가 관리하는 form의 type은 MemberForm type이므로 useForm에 해당 type을 전달한다. 그리고 useForm은 각 input을 연동할 때 쓰는 register 함수와 submit시 사용되는 handelSubmit 함수를 return한다.
register 함수는 name과 onBlur, onChange, ref 값을 return하므로 register("name") 함수는 다음 object를 return한다
{
name: "name",
onBlur: async (event) => {…}
onChange: async (event) => {…}
ref: (ref) => {…}
}
register 함수는 첫 번째 인자 값으로 관리할 input의 name을 전달 받는데 useForm hook에 form에 대한 type을 설정하면 register 함수의 첫 번째 인자로 전달할 수 있는 값은 해당 type이 가진 property에 한정된다.
즉, 위의 예제에서 register에 전달할 수 있는 값은 name, age, address로 한정되며 ( MemberForm type ) 다른 값을 전달하면 type오류가 발생한다.
handleSubmit은 form을 submit할 때 사용된다. 첫 번째 인자로는 input의 값이 모두 정상적일 때 실행될 함수를 전달하고 두 번째 인자로는 input 값에 정상적이 않을 때 오류를 처리할 함수를 전달한다.
Validation 적용하기
Form을 구성하는 각각의 input에 요구 되는 validation이 다를 수 있다. register 함수의 두 번째 인자인 option object를 통해 흔히 사용되면 필수 값, 최소, 최대 값에 대한 validation 설정 또는 개발자가 원하는 로직을 적용할 수 있는 valdiate 함수를 전달하여 input의 validation을 적용할 수 있다.
<div>
<label htmlFor="form-name">name : </label>
<input id="form-name" {...register("name", { required: true })} />
</div>
<div>
<label htmlFor="form-age">age : </label>
<input id="form-age" {...register("age", { min: 20 })} />
</div>
<div>
<label htmlFor="form-address">address : </label>
<input id="form-address" {...register("address", { maxLength: 7 })} />
</div>
위의 예제에서 name은 필수 값, 그리고 age는 20보다 높은 수, 그리고 address는 7글자가 최대 값으로 제한을 걸었다. 만약 위의 조건이 맞지 않는 input 값을 넣고 submit을 시도하면 handleSubmit의 두 번째 인자로 전달한 handleFormError 함수가 실행된다.
만약 새로운 validation logic을 적용하고 싶다면 validate 함수를 통해 custom validation logic을 적용할 수 있다. validate 함수에 validation 함수를 전달하면 함수는 현재 input의 value와 form 전체 value를 인자로 전달받는다
const handleInputValidation = (inputVal: string, formVal: MemberForm) => {
if (inputVal.length > 7) {
return "max length is 7";
}
if (inputVal.trim() === "") {
return "input value is required";
}
return false;
};
<div>
<label htmlFor="form-address">address : </label>
<input
id="form-address"
{...register("address", {
validate: handleInputValidation,
})}
/>
</div>
다른 유형의 input 연동
물론 기본 input element 뿐만 아니라 select이나 checkbox, radio 형태의 input에도 적용이 가능하다. 여러가지 input 타입의 테스트를 위해 useForm이 관리하는 form type을 다음과 같이 업데이트 한다.
type MemberForm = {
name: string;
age: string;
address: string;
city: string;
hobbies: string[];
agreeToPolicy: boolean;
gender: string;
};
Select element에 적용하는 방법은 다음과 같다. 아래에서 선택된 값이 submit시 city field값에 담기게 된다.
<div>
<label htmlFor="form-city">city : </label>
<select id="form-city" {...register("city")}>
<option value="busan">busan</option>
<option value="seoul">seoul</option>
</select>
</div>
Checkbox에 적용하는 방법은 다음과 같다. 아래와 같이 적용하면 여러 개의 값을 선택하고 submit하면 해당 field 값은 선택된 값의 array가 된다.
<div>
<label htmlFor="form-hobby-read">read : </label>
<input
type="checkbox"
id="form-hobby-read"
value="read"
{...register("hobbies")}
/>
<label htmlFor="form-hobby-movie">movie : </label>
<input
type="checkbox"
id="form-hobby-movie"
value="movie"
{...register("hobbies")}
/>
</div>
만약 checkbox를 동의 여부 용도와 같이 하나의 state만 체크하는 목적으로 사용한다면 다음과 같이 적용할 수 있다.
<div>
<label htmlFor="form-agree-policy">agree : </label>
<input
type="checkbox"
id="form-agree-policy"
{...register("agreeToPolicy")}
/>
</div>
Radio type input은 다음과 같이 적용한다.
<div>
<label htmlFor="form-gender-male">male : </label>
<input
{...register("gender")}
type="radio"
id="form-gender-male"
value="male"
/>
<label htmlFor="form-gender-female">female : </label>
<input
{...register("gender")}
type="radio"
id="form-gender-female"
value="female"
/>
</div>
Child Component와 연결하기
만약 useForm hook이 parent component에 선언되어 있고 input이 하위 component에 선언되어 있는 상황이라면 다음과 같이 register와 label을 prop으로 전달받아 사용할 수 있다. 전달할 때 아래의 예제와 같이 react-hook-form에서 export하는 Path와 UseFormRegister type에 form type을 전달하면 전달 받는 prop 타입을 form type에 맞게 강제할 수 있다
...
import { Path, UseFormRegister } from "react-hook-form";
type Props = {
label: Path<MemberForm>;
register: UseFormRegister<MemberForm>;
};
const Input = ({ label, register }: Props) => {
return (
<div>
<label htmlFor={`form-${label}`}>{label} : </label>
<input id={`form-${label}`} {...register(label)} />
</div>
);
};
export default Input;
const HookFormPage = () => {
const { register, handleSubmit } = useForm<MemberForm>();
return (
<div>
<form onSubmit={handleSubmit(handleFormSubmit, handleFormError)}>
<Input label="name" register={register} />
...
</form>
</div>
}
위에서 살펴본 예제들은 useForm을 사용하는 기본적인 예제들이다. useForm은 위에서 살펴본 예제 외에 다양한 옵션을 설정하고 return하는 값을 이용해 form의 상태와 error를 관리한다. 다음 포스트에서 useForm의 옵션과 return value에 대해 자세히 살펴보자