
소개
- 리액트 애플리케이션에서 폼 처리를 더욱 간편하게 만들어주는 라이브러리이다.
특징
- 폼 관리 : 폼 요소의 상태와 유효성 검사를 관리하기 위한 라이브러리로, 필요한 상태를 간단한 훅으로 제공한다.
- 훅 기반 : Hook API를 사용하여 컴포넌트 내에서 폼 상태를 관리한다. (ex. useForm, useFieldArray, useWatch)
- 커스텀 및 다양한 입력요소 지원 : 모든 종류의 입력 요소 (텍스트, 라디오 버튼, 체크박스, 셀렉트 박스 등)와 커스텀 입력 요소를 지원한다.
- 유효성 검사 : 내장된 유효성 검사 규칙 또는 사용자 정의 검사 함수를 사용하여 입력값을 유효성 검사할 수 있다.
- 폼 제출 처리 : handleSubmit 함수를 사용하여 폼 제출을 처리하고, 필요한 로직을 실행할 수 있다.
- 리액트 렌더링 최적화 : 비제어 컴포넌트 방식으로 불필요한 리렌더링을 줄여 성능을 향상시킨다.
장점
- 높은 성능 : 비제어 컴포넌트를 사용하여 리렌더링 횟수를 최소화한다.
- 작은 번들 사이즈 : 의존성이 없고 가벼운 라이브러리이다. (약 9KB)
- 간편한 통합 : 기존 프로젝트에 쉽게 추가할 수 있으며, 다양한 UI 라이브러리와 호환할 수 있다.
- TypeScript 지원 : 완벽한 타입스크립트 지원으로 타입 안정성을 보장한다.
📗 설치 및 기본 설정
설치 방법
npm install react-hook-form
yarn add react-hook-form
기본 구조
import { useForm } from "react-hook-form";
function MyForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm();
const onSubmit = (data) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("name")} />
<button type="submit">제출</button>
</form>
);
}
register
<input {...register("name")} />
<input
{...register("email", {
required: "이메일은 필수입니다.",
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: "올바른 이메일 형식이 아닙니다."
}
})}
/>
<select
{...register("category", {
required: true,
})}
className="block w-full rounded-md border-0 py-1.5"
>
<option value="">카테고리 선택</option>
{CATEGORY_ARR.map((category) => (
<option key={category} value={category}>
{category}
</option>
))}
</select>
- 폼 입력 요소를 react-hook-form에 등록하는 함수이다.
- ref를 통해 입력 요소를 추적하고 값을 관리한다.
- 유효성 검사 규칙을 설정할 수 있다.
handleSubmit
const onSubmit = async (data) => {
try {
const result = await axios.post("/api/stores", data);
if (result.status === 200) {
toast.success("맛집을 등록했습니다.");
router.replace(`/stores/${result.data.id}`);
}
} catch (e) {
toast.error("데이터 생성 중 문제가 생겼습니다.");
}
};
<form onSubmit={handleSubmit(onSubmit)}>
{}
</form>
- 폼 제출을 처리하는 함수이다.
- 유효성 검사를 통과한 경우에만 제출 함수를 실행한다.
- 두 개의 콜백 함수 사용할 수 있다. (성공 시, 실패 시)
const {
formState: { errors, isSubmitting, isDirty },
} = useForm();
{errors.name?.type === "required" && (
<div className="pt-2 text-xs text-red-600">
필수 입력 사항입니다.
</div>
)}
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? "처리 중..." : "등록하기"}
</button>
- 폼의 현재 상태를 담고 있는 객체이다.
errors, isDirty, isValid, isSubmitting 등의 정보를 제공
- errors : 각 필드의 유효성 검사 에러 정보를 담고 있다.
- isDirty : 사용자가 폼의 값을 변경했는지 여부를 나타낸다 (초기값과 다른 경우 true).
- dirtyFields : 변경된 필드들의 목록을 객체 형태로 제공한다.
- isValid : 모든 필드가 유효성 검사를 통과했는지 여부를 나타낸다.
- isSubmitting : 폼이 현재 제출 중인지 여부를 나타낸다 (비동기 제출 시 유용).
- isSubmitted : 폼이 제출되었는지 여부를 나타낸다.
- isSubmitSuccessful : 폼이 에러 없이 성공적으로 제출되었는지 여부를 나타낸다.
- submitCount : 폼이 제출된 횟수를 나타낸다.
- touchedFields : 사용자가 포커스했던 필드들의 목록을 나타낸다.
- isValidating : 유효성 검사가 진행 중인지 여부를 나타낸다 (비동기 검증 시).
setValue
const { setValue } = useForm();
const handleComplete = (data) => {
let fullAddress = data.address;
setValue("address", fullAddress);
};
useQuery(`store-${id}`, fetchStore, {
onSuccess: (data) => {
setValue("id", data.id);
setValue("name", data.name);
setValue("category", data.category);
setValue("phone", data.phone);
},
});
- 특정 필드의 값을 프로그래밍 방식으로 설정하는 함수이다.
- 외부 라이브러리나 API에서 받은 데이터를 폼에 반영할 때 유용하다.
reset
const { reset, resetField } = useForm();
<button onClick={() => reset()}>초기화</button>
const onSubmit = async (data) => {
const result = await axios.post("/api/comments", data);
if (result.status === 200) {
resetField("body");
}
};
- 폼 전체를 초기 상태로 되돌리는 함수이다.
- 특정 필드만 초기화하려면 resetField를 사용한다.
watch
const { watch } = useForm();
const watchedValue = watch("fieldName");
const allValues = watch();
{watch("category") === "한식" && (
<input {...register("subCategory")} />
)}
- 특정 필드의 값을 실시간으로 감시하는 함수이다.
- 입력 값에 따라 다른 필드를 동적으로 변경할 때 사용한다.
📙 유효성 검사 규칙
기본 규칙
<input
{...register("name", {
required: "이름은 필수입니다.",
minLength: {
value: 2,
message: "최소 2자 이상 입력해주세요."
},
maxLength: {
value: 20,
message: "최대 20자까지 입력 가능합니다."
}
})}
/>
<input
{...register("phone", {
required: true,
pattern: {
value: /^[0-9]{2,3}-[0-9]{3,4}-[0-9]{4}$/,
message: "올바른 전화번호 형식이 아닙니다."
}
})}
/>
- required : 필수 입력 항목으로 지정한다.
- min / max : 숫자의 최소/최대값을 설정한다.
- minLength / maxLength : 문자열의 최소/최대 길이를 설정한다.
- pattern : 정규식 패턴을 사용한 검증을 수행한다.
커스텀 검증
<input
{...register("password", {
validate: {
hasUpperCase: (value) =>
/[A-Z]/.test(value) || "대문자를 포함해야 합니다.",
hasNumber: (value) =>
/[0-9]/.test(value) || "숫자를 포함해야 합니다.",
minLength: (value) =>
value.length >= 8 || "최소 8자 이상이어야 합니다."
}
})}
/>
<input
{...register("username", {
validate: async (value) => {
const exists = await checkUsernameExists(value);
return !exists || "이미 사용 중인 아이디입니다.";
}
})}
/>
- validate : 사용자 정의 검증 함수를 작성 가능, 복잡한 검증 로직 구현에 사용한다.
에러 메시지 표시
{errors.name && (
<span className="text-red-600">
{errors.name.message}
</span>
)}
{errors.name?.type === "required" && (
<div className="pt-2 text-xs text-red-600">
필수 입력 사항입니다.
</div>
)}
{errors.name?.type === "minLength" && (
<div className="pt-2 text-xs text-red-600">
최소 2자 이상 입력해주세요.
</div>
)}
- errors 객체를 통해 검증 실패 시 에러 메시지를 표시할 수 있다.
📒 고급 기능 및 이슈 해결 방법
useFieldArray
import { useForm, useFieldArray } from "react-hook-form";
function DynamicForm() {
const { control, register } = useForm({
defaultValues: {
items: [{ name: "", price: 0 }]
}
});
const { fields, append, remove } = useFieldArray({
control,
name: "items"
});
return (
<form>
{fields.map((field, index) => (
<div key={field.id}>
<input {...register(`items.${index}.name`)} />
<input {...register(`items.${index}.price`)} />
<button type="button" onClick={() => remove(index)}>
삭제
</button>
</div>
))}
<button
type="button"
onClick={() => append({ name: "", price: 0 })}
>
항목 추가
</button>
</form>
);
}
- 동적으로 입력 필드를 추가/제거할 수 있는 훅이다.
- 배열 형태의 데이터를 다룰 때 유용하다.
useWatch
import { useForm, useWatch } from "react-hook-form";
function WatchExample() {
const { control, register } = useForm();
const watchedValue = useWatch({
control,
name: "category",
defaultValue: ""
});
return (
<div>
<select {...register("category")}>
<option value="">선택</option>
<option value="A">A 타입</option>
<option value="B">B 타입</option>
</select>
{watchedValue === "A" && (
<input {...register("aSpecific")} />
)}
{watchedValue === "B" && (
<input {...register("bSpecific")} />
)}
</div>
);
}
- watch보다 더 세밀한 제어가 가능한 훅이다.
- 특정 필드의 변경을 감지하여 다른 로직을 실행할 수 있다.
리렌더링 최적화
const allValues = watch();
const specificValue = watch("fieldName");
const specificValue = useWatch({
control,
name: "fieldName"
});
- watch 사용 시 불필요한 리렌더링이 발생할 수 있다.
- useWatch나 컨트롤러를 사용하여 최적화할 수 있다.
기본값 설정
const { register } = useForm({
defaultValues: {
name: "기본 이름",
email: "default@email.com"
}
});
useEffect(() => {
setValue("name", fetchedData.name);
setValue("email", fetchedData.email);
}, [fetchedData]);
useEffect(() => {
reset(fetchedData);
}, [fetchedData]);
- 수정 폼에서 기본값을 설정할 때는 useForm의 defaultValues를 사용하거나 setValue를 사용한다.
- defaultValues는 초기 렌더링 시에만 적용된다.
타입 지정
interface FormData {
name: string;
email: string;
age: number;
category: string;
}
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormData>();
<input {...register("name")} />
<input {...register("wrong")} />
- 제네릭을 사용하여 폼 데이터의 타입을 명확하게 지정한다.
- 타입 안정성을 보장하고 자동완성을 활용할 수 있다.
비동기 제출 처리
const {
handleSubmit,
formState: { isSubmitting },
} = useForm();
const onSubmit = async (data) => {
try {
await api.post("/endpoint", data);
toast.success("성공!");
} catch (error) {
toast.error("실패!");
}
};
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? "처리 중..." : "제출"}
</button>
- async/await를 사용하여 비동기 로직을 처리한다.
- 로딩 상태를 관리하여 사용자 경험을 개선한다.
📓 참고
react-hook-form 공식문서