라이브러리/Firebase

Firebase 이메일 회원가입 로직 구현: 이메일 인증과 사용자 경험 최적화

눙엉 2024. 6. 2. 13:28

Firebase에서는 여러 방법을 통한 인증서비스를 제공하고 있습니다. 그중 이메일을 활용한 회원가입 기능을 추가하려 합니다.

회원가입 폼 모달, 회원가입 성공 모달

 

 

제가 원하는 로직대로 이메일 회원가입을 진행하기 위해 Firebase에서 제공하는 메서드를 그대로 사용할 수 없는 문제를 해결하기 위한 과정을 공유드립니다.

 

처음에 진행하려고 했던 이메일 회원가입 로직은 아래와 같습니다.

  1. 이메일 입력
  2. 이메일 인증 버튼 클릭
  3. 입력한 이메일로 인증 번호 메일 수신
  4. 인증 번호 입력 칸에 확인한 인증 번호 입력
  5. 인증 완료 버튼 클릭
  6. 이메일 인증 완료 후 나머지 회원가입에 필요한 정보(이름, 닉네임, 비밀번호, 기타 등등) 입력
  7. 회원가입 버튼 클릭
  8. 회원가입 완료

 

코드를 작성하기 전 Firebase에서 제공하는 여러 메서드 중 이메일 회원가입을 진행할 때 사용 할 수 있다고 생각되는 메서드는 아래의 리스트와 같습니다.

  • createUserWithEmailAndPassword
  • sendSignInLinkToEmail
  • sendEmailVerification

 

회원가입을 하기 위해 사용하는 메서드에 대한 문제점을 개인적인 생각으로 판단해 보았습니다.

  • createUserWithEmailAndPassword
    이메일과 비밀번호만으로 회원가입을 할 수 있습니다. 하지만 다른 인증 절차 없이 회원가입이 가능한 문제점이 있습니다.
  • sendSignInLinkToEmail
    입력한 이메일로 인증 링크를 전달받고 해당 링크를 클릭해 인증을 하는 방식입니다. 해당 방법은 로그인 시 항상 현재의 웹사이트를 떠나서 인증을 받고 다시 돌아와야 하는 불편함이 있습니다. 사용자로서 해당 경험은 유저의 사용성을 많이 해친다고 판단이 되었습니다.
  • sendEmailVerification
    회원가입을 완료한 이메일이 유효한 이메일인지 인증하는 메서드입니다. 해당 메서드를 사용하기 위해서는 회원가입을 먼저 완료해야 되는 문제가 있습니다. 하지만 저는 가입하려 하는 이메일이 유효한지 확인 후 해당 이메일로 가입을 진행하고 싶었습니다. 그리고 인증 방법으로 이메일을 통해 인증 코드를 전달받는 것이 아니라 인증 링크를 전달받아 해당 링크를 클릭 함으로써 인증을 진행하는 방식입니다.

 

Firebase에서 제공하는 3가지의 메서드 중 메서드 기능을 그대로 사용해서 원하는 이메일 회원가입 로직을 만족할 수는 없다고 판단되었고 여러 메서드를 적절히 섞어서 이메일 회원가입을 진행하도록 해보았습니다.

 

 

제가 원하는 회원가입 로직은 회원가입을 완료하기 전 이메일이 실제로 존재하는 검사하고 존재한다면 회원가입을 성공적으로 마칠 수 있도록 하려 했습니다. 그럼 위에서 찾은 메서드 중 이메일의 유효성 검사를 하기 위한 sendEmailVerification를 사용한 로직을 작성해 보았습니다.

  1. 이메일 입력
  2. 이메일 인증 버튼 클릭
  3. 입력한 이메일과 임의의 비밀번호를 사용해 사용자 모르게 먼저 회원가입을 완료합니다. (sendEmailVerification을 사용하려면, 먼저 회원가입이 완료되어 있어야 합니다. 다만, 회원가입이 완료되었다는 점은 사용자가 화면에서 즉시 확인할 수 없습니다.)
  4. sendEmailVerification 메서드를 사용해 입력한 이메일로 이메일 인증 메일 발송
  5. 입력한 이메일로 전달받은 인증 메일 내 인증 링크 클릭 후 인증 완료
  6. 나머지 회원가입에 필요한 정보(이름, 닉네임, 비밀번호, 기타 등등) 입력
  7. 회원가입 버튼 클릭
  8. 미리 회원가입 되어 있던 이메일 계정의 임의의 비밀번호를 updatePassword 메서드를 사용해 사용자가 입력한 비밀번호로 변경
  9. 회원가입 완료

 

사용자는 이메일 인증만으로 회원가입이 되었다는 것을 알 수 없도록 유도하였고, 원하던 로직대로 회원가입을 진행 할 수 있을 것만 같았습니다. Firebase에서 제공하는 메서드만으로는 원하는 로직을 구현할 수 없었지만, 여러 메서드를 조합하여 방법을 찾게 되었습니다. 하지만 여러 가지의 문제점을 발견하게 되었습니다. 발견한 문제점은 아래와 같습니다.

  • 이메일 인증을 받기 위해 검증되지 않은 이메일과 임의의 비밀번호로 회원가입을 먼저 진행해야 하며, 심지어 사용자는 미리 회원가입이 되었다는 점을 인지하지 못하는 상황입니다.
  • 사용자가 이메일 인증까지만 진행 후 페이지 이탈하는 경우 이메일 인증 시 미리 회원가입이 되었기 때문에 추후 다시 회원가입 플로우를 진행할 때 해당 이메일은 이미 가입이 되어 있어 중복 가입을 시도하는 것으로 간주됩니다.
  • 사용자가 이메일 인증 후 회원가입 플로우를 진행하다 페이지를 이탈하는 경우 미리 생성된 계정을 삭제해주어야 합니다. 그러나 사용자가 회원가입 플로우를 이탈한 지 확인할 수 있는 방법이 떠오르지 않았습니다.

 

처음 생각해던 로직에서 회원가입을 완료하기 전 이메일 유효성 검사를 진행하는 것은 무리일 것이라고 판단이 되었습니다. 그래서 회원가입을 완료 후 이메일 인증을 하는 방향으로 로직을 수정하기로 했습니다.

  1. 이메일, 비밀번호, 기타 등등 입력
  2. 회원가입 버튼 클릭
  3. 회원가입 완료
  4. 이메일 인증 요청 모달 열기
  5. sendEmailVerification 메서드를 사용해 입력한 이메일로 이메일 인증 메일 발송
  6. 입력한 이메일로 전달받은 인증 메일 내 인증 링크 클릭
  7. 이메일 인증 요청 모달 내에서 getAuth 메서드를 사용해 이메일 인증이 완료되었는지 확인
  8. 해당 계정의 인증이 완료되었으면 이메일 인증 요청 모달 닫기
  9. 만약 해당 계정의 이메일 인증이 완료 되지 않고 이메일 인증 요청 모달을 닫은 경우 추후 로그인 시 위 4번 로직부터 반복

 

위와 같은 로직으로 이메일 회원가입을 진행하게 된다면 이전에 진행하려 했던 로직에서의 문제점들을 해결할 수 있다고 생각했고 사이드 이펙트도 없다고 판단이 되어 최종적으로 해당 로직으로 이메일 회원가입을 진행하게 되었습니다.

 

 

지금부터는 이메일 회원가입을 진행하는 로직의 코드입니다. react hook form과 tanstack query를 사용해서 코드를 작성하였고, 회원가입을 진행하는 스크린이 다를 것으로 판단되어 중요한 로직을 위주로 첨부드리는 점 이해부탁드리며 궁금한 사항은 댓글로 남겨주시면 추가로 설명드리겠습니다.

// 이메일 회원가입 API
const emailSignUpAPI = async (req: EmailSignUpQueryModel) => {
  const auth = getAuth();

  const { user } = await createUserWithEmailAndPassword(
    auth,
    req.email,
    req.password
  );

  await sendEmailVerificationAPI();

  return { user, email: req.email, password: req.password };
};

// 이메일 로그인 API
const emailSignInAPI = async (req: EmailSignInQueryModel) => {
  const auth = getAuth();

  const { user } = await signInWithEmailAndPassword(
    auth,
    req.email,
    req.password
  );

  return user;
};

// 회원가입 후 생성된 회원 데이터를 firestore에 저장하는 API
const createUserAPI = async (req: CreateUserQueryModel) => {
  await setDoc(doc(db, "user", req.body.uid), req.body);
};

// 이메일 회원가입 useMutation
const useEmailSignUp = () => {
  return useMutation({
    mutationFn: (req: EmailSignUpQueryModel) => emailSignUpAPI(req),
    onSuccess: async ({ user: { uid }, email, password }) => {
      const nickname = format.string.createRandomNickname();
      
      await createUserAPI({
        body: { uid, email, nickname, password, profileImageUrl: null },
      });
    },
  });
};

// 이메일 로그인 useMutation
const useEmailSignIn = () => {
  // 유저 데이터를 recoil을 사용해 전역상태로 관리
  const setUser = useSetRecoilState(userState);

  return useMutation({
    mutationFn: (req: EmailSignInQueryModel) => emailSignInAPI(req),
    onSuccess: async ({ uid }) => {
      // 이메일 로그인 성공 후 유저 전역상태 업데이트
      const user = await getUserAPI({ query: { uid } });

      setUser(user);
    },
  });
};

 

// 이메일 유효성 조건
const EMAILREGEX = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,24}$/;

// 이메일이 유효한지 테스트하는 함수
const validateEmail = (email: string): boolean => {
  return EMAILREGEX.test(email);
};

// 이메일 회원가입 mutate
const { mutate: emailSignUpMutate } = useEmailSignUp();
// 이메일 로그인 mutate
const { mutate: emailSignInMutate } = useEmailSignIn();

// 이메일과 비밀번호를 저장하는 formData
const { register, watch, handleSubmit } = useForm({
  defaultValues: { email: "", password: "" },
  mode: "onTouched",
});


// react hook form의 handleSubmit을 사용한 이메일 회원가입 함수
const handleClickSendCredntial = handleSubmit(({ email, password }) => {
  emailSignUpMutate(
    { email, password },
    {
      onSuccess: () => {
        // 이메일 회원가입 성공 후 가입한 정보로 이메일 로그인 진행
        const handleAfterSignUpFn = (): void => {
          emailSignInMutate({ email, password });
        };

        // 현재 프로젝트의 스크린이 모달로 진행되기때문에
        // 현재 열려있는 회원가입 모달을 닫은 후 회원가입 완료 모달 열기
        handleCloseModal();
        handleOpenModal(
          <SignUpCompleteModal
            email={email}
            afterSignUpPath="plan/create"
            handleAfterSignUpFn={handleAfterSignUpFn}
          />
        );
      },
    }
  );
});

 

<S.Input
  id="email"
  placeholder="이메일을 입력하세요."
  {...register("email")}
/>
<S.Input
  id="password"
  type="password"
  placeholder="비밀번호를 입력하세요."
  autoComplete="new-password"
  {...register("password")}
/>
<S.SignUpButton
  type="button"
  disabled={!watch("email") || !validateEmail(watch("email"))}
  onClick={handleClickSendCredntial}
>
  회원가입
</S.SignUpButton>

 

// SignUpCompleteModal 내부 로직

import React, { useEffect } from "react";
import { useRouter } from "next/navigation";
import { getAuth } from "firebase/auth";

const { push } = useRouter();

// 현재 로그인 되어있는 계정 가져오는 메서드
const auth = getAuth();

const { handleCloseModal } = useModal();

useEffect(() => {
  // useEffect를 사용해 해당 모달이 열리는 시점부터
  // setInterval로 현재 로그인 되어 있는 계정이
  // 이메일 인증을 진행했는지 확인하는 작업
  // 확인 후 인증을 성공했다면 setInterval를 삭제하고
  // 페이지 이동, handleAfterSignUpFn 동작, 현재의 모달 닫기
  const intervalId = setInterval(() => {
  auth.currentUser?.reload();
  if (auth.currentUser?.emailVerified) {
    clearInterval(intervalId);
    afterSignUpPath && push(afterSignUpPath);
    handleAfterSignUpFn && handleAfterSignUpFn();
    handleCloseModal();
    }
  }, 1500);
}, []);

 

 

추가적으로 로그인시 이메일 인증을 통과하지 않은 계정이라면 SignUpCompleteModal을 사용해 이메일 재인증을 유도하도록 해야 합니다. 아직 해당 부분의 로직은 구현되지 않아 구현 후 추가할 예정입니다.

 

간단하게 코드를 추가, 작성해보았습니다. 해당 코드가 최종적으로 원하는 로직대로 동작하지만 정답은 아닙니다. 질문이 있거나 잘못된 부분에 대해서는 댓글에 남겨주시면 다시 한번 확인해 보겠습니다.

 

이메일로 회원가입을 진행하고, 해당 이메일로 유효한 이메일인지 인증하는 작업까지 진행할 수 있게 되었습니다. 최대한 사용자의 경험을 불편하게 만들지 않고 해당 웹페이지의 이탈률을 줄이고 싶은 생각은 어느 정도 반영된 것 같습니다.

 

처음 기획했던 로직과는 조금 다르게 진행되었지만 주어진 메서드를 활용해서 여러 가지의 고민을 해보게 된 재미있는 에피소드가 된 것 같습니다.