🧠 react-hook-form 작동 원리 간단 정리
register()로 input을 등록
register는 input을 react-hook-form의 내부 상태(formState)에 등록하고, value/validation/error 관리를 가능하게 해주는 핵심 함수입니다.
- input, select 같은 DOM 요소를 ref로 추적
- 브라우저의 내장 폼 상태 (value, validity 등) 를 그대로 사용 → 리렌더링이 거의 없음
<input {...register("email", { required: true })} />
→ 이 코드는 email이라는 필드를 내부 formState에 등록하면서, 필수 입력 조건을 체크하도록 합니다.
useForm() 내부에서 필드 상태를 추적
- 각 필드의
value, error, touched 상태 등을 formState로 관리 → formState는 폼의 에러, 제출 상태, 유효성 통과 여부, 터치 여부 등을 추적하는 객체입니다.
setError, clearErrors 등으로 수동 제어 가능
- 사용 예시
const { register, handleSubmit, formState: { errors, isSubmitting, isValid, touchedFields }, } = useForm({ mode: "onChange" });
- 활용 예시
- 에러 메시지 출력
{errors.email &&{errors.email.message}}
<button type="submit" disabled={!isValid}>제출</button>
{isSubmitting && 제출 중...}
handleSubmit(onSubmit)
- 리렌더링 최적화
- 기존의
useState나 onChange 방식은 input이 변경될 때마다 컴포넌트가 리렌더링 됩니다.
react-hook-form은 DOM 참조(ref) 를 통해 값 추적 → 리렌더링 최소화
→ 성능에 아주 유리함
📦 사용 스택
- Next.js (App Router)
- Firebase Authentication
- React Hook Form
☑️ 구현 목표
- 회원가입 시 이메일/비밀번호/비밀번호 확인 유효성 검사
- 로그인 시 이메일/비밀번호 유효성 검사
- Firebase에서 발생하는 에러코드를 기반으로 사용자에게 맞춤 피드백 제공
☑️ 회원가입 기능 흐름 및 유효성 검사
- 입력 필드
- 유효성 검사 조건
- 비밀번호 일치 검사
- Firebase
createUserWithEmailAndPassword 호출
⚒️ 구현 코드
FormField 는 input을 공통으로 묶어 커스텀한 컴포넌트 입니다.
type FormData = { email: string; password: string; confirmPassword: string };
export default function SignUpForm() {
const route = useRouter();
const {
register,
handleSubmit,
formState: { errors },
setError,
} = useForm<FormData>();
const onSubmit = async (data: FormData) => {
// 유효성 검사1 : 비밀번호 확인: 비밀번호와 일치해야 함
if (data.password !== data.confirmPassword) {
setError("confirmPassword", {
type: "manual",
message: "비밀번호가 일치하지 않습니다.",
});
return;
}
try {
await createUserWithEmailAndPassword(auth, data.email, data.password);
route.push("/login");
} catch (error) {
const firebaseError = error as FirebaseError;
console.error("🔥 FirebaseError.code:", firebaseError.code);
if (firebaseError.code === "auth/email-already-in-use") {
console.error("error", error);
setError("email", {
type: "manual",
message: "이미 사용 중인 이메일입니다.",
});
} else {
setError("email", {
type: "manual",
message: "회원가입에 실패했습니다.",
});
}
}
};
return (
<div className={s.authPage}>
<form onSubmit={handleSubmit(onSubmit)}>
<FormField
id="email"
label="이메일"
type="email"
placeholder="이메일"
{...register("email", {
required: "이메일을 입력해주세요.",
pattern: {
value: /\S+@\S+\.\S+/,
message: "유효한 이메일 형식을 입력해주세요.",
},
})}
error={errors.email?.message}
/>
<FormField
id="password"
label="비밀번호"
type="password"
placeholder="비밀번호"
{...register("password")}
error={errors.password?.message}
/>
<FormField
id="confirmPassword"
label="비밀번호 확인"
type="password"
placeholder="비밀번호 다시 입력"
{...register("confirmPassword")}
error={errors.confirmPassword?.message}
/>
<ActionButtons actions={[{ label: "회원가입", type: "submit", variant: "fillButton" }]} />
</form>
</div>
);
}
☑️ 로그인 흐름 및 유효성 검사
- 입력 필드
- Firebase
signInWithEmailAndPassword 호출
- 에러코드별 메시지 분기
⚒️ 구현 코드
type FormData = { email: string; password: string };
export default function LoginForm() {
const route = useRouter();
const {
register,
handleSubmit,
setError,
formState: { errors },
} = useForm<FormData>();
const onSubmit = async (data: FormData) => {
try {
await signInWithEmailAndPassword(auth, data.email, data.password);
route.push("/");
} catch (error) {
const firebaseError = error as FirebaseError;
console.log("❌ Firebase 로그인 실패", firebaseError.code); // 디버깅용
if (firebaseError.code === "auth/user-not-found") {
setError("email", {
type: "manual",
message: "등록되지 않은 이메일입니다.",
});
} else if (firebaseError.code === "auth/wrong-password") {
setError("password", {
type: "manual",
message: "비밀번호가 올바르지 않습니다.",
});
} else {
setError("email", {
type: "manual",
message: "로그인 중 알 수 없는 오류가 발생했습니다.",
});
}
}
};
return (
<div className={s.authPage}>
<form onSubmit={handleSubmit(onSubmit)}>
<FormField
id="email"
label="이메일"
type="email"
placeholder="이메일"
{...register("email", {
required: "이메일을 입력해주세요.",
pattern: {
value: /\S+@\S+\.\S+/,
message: "유효한 이메일 형식을 입력해주세요.",
},
})}
error={errors.email?.message}
/>
<FormField
id="password"
label="비밀번호"
type="password"
placeholder="비밀번호"
{...register("password", {
required: "비밀번호를 입력해주세요.",
})}
error={errors.password?.message}
/>
<ActionButtons actions={[{ label: "로그인", type: "submit", variant: "fillButton" }]} />
</form>
<p className={s.signupTxt}>
회원이 아니신가요? <Link href={"/signup"}>회원가입하기</Link>
</p>
</div>
);
}
☑️ 에러 메시지 UI 처리 방식
- input 하단에 에러 메세지를 표시
FormField 컴포넌트에서 error prop으로 처리
{error && <p className={styles.error}>{error}</p>}
🎯 마무리 & 회고
- React Hook Form + Firebase 조합은 깔끔하지만, 에러 코드 분기와 사용자 피드백 설계가 중요 (아마 더 추가해야할 듯)
- UX 디테일(깜빡임, 로딩 타이밍)까지 고려하여 전역 상태관리가 필요