[React] react-hook-form

dev_vming·2025년 12월 22일

React

목록 보기
2/2

📚 React-hook-form


📕 React-hook-form 이란?

소개

  • 리액트 애플리케이션에서 폼 처리를 더욱 간편하게 만들어주는 라이브러리이다.

특징

  • 폼 관리 : 폼 요소의 상태와 유효성 검사를 관리하기 위한 라이브러리로, 필요한 상태를 간단한 훅으로 제공한다.
  • 훅 기반 : 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>
    );
}

📘 useForm 주요 기능

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>
  • 폼 제출을 처리하는 함수이다.
  • 유효성 검사를 통과한 경우에만 제출 함수를 실행한다.
  • 두 개의 콜백 함수 사용할 수 있다. (성공 시, 실패 시)

formState

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();

// 주소 검색 API 결과를 폼에 반영
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>
)}

// type별 다른 메시지 표시
{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");

// 또는 useWatch 사용
const specificValue = useWatch({
    control,
    name: "fieldName"
});
  • watch 사용 시 불필요한 리렌더링이 발생할 수 있다.
  • useWatch나 컨트롤러를 사용하여 최적화할 수 있다.

기본값 설정

// 방법 1: defaultValues 사용 (초기 렌더링에만 적용)
const { register } = useForm({
    defaultValues: {
        name: "기본 이름",
        email: "default@email.com"
    }
});

// 방법 2: setValue 사용 (동적으로 변경 가능)
useEffect(() => {
    setValue("name", fetchedData.name);
    setValue("email", fetchedData.email);
}, [fetchedData]);

// 방법 3: reset 사용 (전체 폼 초기화)
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>();

// register 사용 시 자동완성 지원
<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 공식문서

profile
밍기적 개발하기🐛

0개의 댓글