Firebase를 활용한 로그인 & 회원가입 구현하기 (feat. React Hook Form)

2025. 5. 13. 20:25·Next

🧠 react-hook-form 작동 원리 간단 정리

  1. register()로 input을 등록
  • register는 input을 react-hook-form의 내부 상태(formState)에 등록하고, value/validation/error 관리를 가능하게 해주는 핵심 함수입니다.
  • input, select 같은 DOM 요소를 ref로 추적
  • 브라우저의 내장 폼 상태 (value, validity 등) 를 그대로 사용 → 리렌더링이 거의 없음
<input {...register("email", { required: true })} />
→ 이 코드는 email이라는 필드를 내부 formState에 등록하면서, 필수 입력 조건을 체크하도록 합니다.
  1. 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 && 제출 중...}
  1. handleSubmit(onSubmit)
  • 유효성 검사 통과 시 onSubmit 실행
  • 실패 시 `errors` 객체에 에러 정보 저장
    <form onSubmit={handleSubmit((data) => console.log(data))}>
  1. 리렌더링 최적화
  • 기존의 useState나 onChange 방식은 input이 변경될 때마다 컴포넌트가 리렌더링 됩니다.
  • react-hook-form은 DOM 참조(ref) 를 통해 값 추적 → 리렌더링 최소화
    → 성능에 아주 유리함

📦 사용 스택

  • Next.js (App Router)
  • Firebase Authentication
  • React Hook Form

☑️ 구현 목표

  1. 회원가입 시 이메일/비밀번호/비밀번호 확인 유효성 검사
  2. 로그인 시 이메일/비밀번호 유효성 검사
  3. Firebase에서 발생하는 에러코드를 기반으로 사용자에게 맞춤 피드백 제공

☑️ 회원가입 기능 흐름 및 유효성 검사

  1. 입력 필드
  2. 유효성 검사 조건
  3. 비밀번호 일치 검사
  4. 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>
  );
}

☑️ 로그인 흐름 및 유효성 검사

  1. 입력 필드
  2. Firebase signInWithEmailAndPassword 호출
  3. 에러코드별 메시지 분기

⚒️ 구현 코드

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 디테일(깜빡임, 로딩 타이밍)까지 고려하여 전역 상태관리가 필요

'Next' 카테고리의 다른 글

Next 기반 학원비 통계 컴포넌트 개발 회고  (0) 2025.07.10
Next.js + Redux로 확장 가능한 모달 시스템 만들기 (with App Router)  (0) 2025.05.06
[Next] 페이지별 레이아웃 설정하기  (1) 2024.11.18
[Next] API Routes  (3) 2024.11.15
[Next] 프리페칭(Pre-fetching)  (0) 2024.11.07
'Next' 카테고리의 다른 글
  • Next 기반 학원비 통계 컴포넌트 개발 회고
  • Next.js + Redux로 확장 가능한 모달 시스템 만들기 (with App Router)
  • [Next] 페이지별 레이아웃 설정하기
  • [Next] API Routes
FE Dev. 굼지
FE Dev. 굼지
굼지의 웹 개발 레시피 입니다.
  • FE Dev. 굼지
    굼지의 웹 개발 레시피
    FE Dev. 굼지
    • 분류 전체보기 (14)
      • FrontEnd (3)
      • React (3)
      • Next (8)
      • React-Native (0)
  • 블로그 메뉴

    • 홈
    • 태그
  • 최근 글

FE Dev. 굼지
Firebase를 활용한 로그인 & 회원가입 구현하기 (feat. React Hook Form)
상단으로

티스토리툴바