firebase로 이메일 로그인, 인증을 구현하면서 맞닥뜨린 문제들

김세현·2022년 10월 10일
6

개인 프로젝트를 진행하며 로그인을 구현해야 했다.

로그인을 구현하려면 인증을 담당하는 서버가 필요한데 나에겐 따로 서버를 구현할 여유가 없었다.

이전에 팀 프로젝트를 진행하며 서버 코드를 작성한 경험이 있긴 하지만 오래 전이었고,

혼자서 개발해야 했기 때문에 클라이언트 뿐만 아니라 서버쪽 코드까지 구현하는 것은 부담이기도 했다.

이런 나에게 firebase는 훌륭한 대안이었다.

firebase는 아래와 같은 방법들을 통해 사용자를 쉽게 인증할 수 있도록 도와준다.

그리고 나는 이 중에서 이메일을 선택했다.

그리고 약 2주 정도 firebase를 통해 사용자 인증을 구현했다. (사실 아직도 완료되지 않았다.)

firebase를 이용해서 회원가입, 로그인 등을 구현하는 것에 2주란 시간을 쏟았다는 것이

누군가는 이해되지 않을 수도 있다. (firebase로 인증을 구현하는 것이 그다지 어려운 것은 아니기 때문이다.)

나름대로의 변명을 하자면, firebase를 통해 인증을 처리하는 일련의 과정이 너무 애매하다고 생각했다.

그래서 이 문제들을 어떻게 처리해야 하는 것인지 고민하며 방법을 찾는 데 시간을 많이 쏟았다.


첫 번째 문제, 이미 가입된 유저만이 이메일 인증을 할 수 있는 것.

아래 공식 문서의 코드를 보면 firebasesendEmailVerification이라는 메소드를 통해 사용자에게 인증 메일을 전송한다.

하지만, 이 메소드의 인자로 전달되는 것이 현재 로그인된 사용자의 정보이다.

회원가입을 위한 이메일 인증을 진행하는데, 현재 로그인된 사용자의 정보가 필요하다?

나는 여기서 애매함을 느꼈다.

이메일을 통한 회원가입, 로그인이라 하면 기본적으로 사용자가 기존에 사용하던 이메일 아이디를 그대로 사용하는 것이 일반적이다.

만약, 내가 기존에 사용하는 이메일이 example@google.com이라 했을 때, 이 아이디를 그대로 사용할 수 있어야 이메일 로그인이 의미가 있는 것이지

내가 사용하지도 않으며, 내 아이디도 아닌 test@google.com로 사이트에 가입하고 이용한다면 많이 불편할 것이고

실제 test@google.com 이메일을 사용하는 사용자는 이로 인해 또다른 불편함을 겪을 것이다.

그렇기 때문에 이메일 인증이라는 과정을 거쳐 이메일의 실소유주인지 확인해야 한다고 생각했다.

나는 이러한 이유로 이메일 인증이라는 과정은 회원가입 이후가 아닌, 회원가입 중에 수행되어야 의미가 있는 작업이라고 생각했다.

하지만, 위에서 보았듯이 이메일 인증을 하기 위한 sendEmailVerification 메소드는 현재 로그인한 사용자의 정보가 인자로 전달 되어야 한다.

사이트에 로그인하기 위해 회원가입을 하고, 회원가입을 하기 위해 인증을 하는데

인증을 위해 이미 로그인한 사용자의 정보가 필요하다니 내 상식에서는 말이 안 되었다.

firebase로 이메일 인증을 구현하기 위한 대안

내가 작성한 코드의 흐름은 다음과 같다.

  1. 사용자가 이메일을 입력한다.
  2. 사용자가 인증 버튼을 클릭한다.
  3. 인증 버튼 클릭시, 임의의 비밀번호를 난수로 생성하고 이를 암호화하여 입력된 이메일, 암호화된 비밀번호를 통해 임의로 회원가입을 시킨다.
  4. firebase는 회원 가입시 자동으로 로그인 된다. 따라서 로그인된 사용자 정보를 얻을 수 있다.(아래의 공식문서)
  5. 이렇게 얻게된 사용자 정보를 통해 sendEmailVerification를 호출하며 사용자 정보를 전달한다.
  6. 입력된 이메일로 인증 메일이 전송된다.
  7. 이메일 인증이 완료된다면 미리 가입시킨 임의의 계정을 삭제시킨다.
  8. 이메일 인증 이후 사용자가 최종적으로 회원가입버튼을 눌렀을 때 실제 가입이 완료된다.

핵심은 3번 과정이다.
3번 과정에 대해서 설명하자면, 인증을 위해 일시적으로 회원가입을 시키는 것이다.
다만, 이 잠깐 동안에 누군가는 로그인을 시도할 수도 있는 가능성이 있기 때문에
예측할 수 있는 비밀번호를 사용하거나 고정된 값을 사용한다면 안 된다고 생각했다.
그렇기 때문에 임의의 난수를 생성하고, 이를 암호화하여 아무도 로그인할 수 없도록 했다.
코드는 아래와 같다.

 	//Math.random()보다 암호학적으로 안전한 난수를 생성한다.
    const array = new Uint32Array(1);
    const temporalPassword = window.crypto.getRandomValues(array)[0];
    const encryptedPassword = crypto.AES.encrypt(
      temporalPassword.toString(),
      `${process.env.REACT_APP_SECRET_KEY}`
    ).toString();
    //인증을 위해 가짜로 회원가입을 시키는 요청
    fakeSignup.sendRequest({
      email: test@google.com
      password: encryptedPassword,
    });

위와 같은 방법으로 회원가입시 이메일 인증을 처리하는 코드를 작성했지만

이 방법이 프로그래밍적으로?는 절대 맞다고 생각하지 않는다.

firebase가 기본적으로 제공하는 방법이 아닌, 내가 구현하고자 했던, 원하는 방법으로 이메일 인증을 하기 위해서

없어도 되는 3, 4번 과정이 생겨났고 3,4번 과정으로 인해 또 다시 7번이라는 과정이 생겨났으며

또 다시 7번 이라는 과정을 처리하기 위해 다양한 상황들을 고려해야 했고 그에 따른 로직들을 추가해야 했다. (인증을 눌렀지만, 가입을 하지 않고 페이지를 벗어나는 경우 등)

다만, firebase를 이용할 수 밖에 없는 현재 상황에서는 최선의 방법이라고 생각했다.


두 번째 문제, 이메일을 인증을 확인 및 처리하는 과정에서 보여줄 UI를 구현하며 맞닥뜨린 문제

내가 맞닥 뜨렸던 문제는 인증 메일을 전송한 뒤, 사용자가 인증 링크를 클릭하면 기존 앱에서는 어떻게 이를 확인하며 이에 따른 UI를 어떻게 적절히 보여줄 것인지였다.

즉, 인증이라는 이벤트가 발생했을 때 기존 앱에서 어떻게 이를 listening 하고 작업을 처리할 것인가에 대한 문제이다.

사실, 대부분의 앱에서는 firebase가 제공하는 메소드만으로도 충분히 이메일 인증 및 확인 과정을 처리할 수 있다.
다만, 이 문제는 firebase를 이용해 내가 원하는 흐름대로 인증 및 확인 작업을 처리 하려고 했기 때문에 발생한 문제이다.

내가 원하는 이메일 인증에 대한 작업 flow는 다음과 같다.

  1. 인증 이메일을 전송한다.
  2. 사용자는 링크를 클릭한다 -> 인증 완료
  3. 인증이 완료되면 기존 회원 가입 화면에서 이를 감지하고 UI가 변경될 수 있어야 한다.
  4. 이후 일반적인 흐름대로 사용자는 회원가입을 한다.

이때 문제가 발생했던 지점이 3번이다.

사용자가 이메일을 인증했을 때 이를 감지하고 이에 따라 적절히 앱의 UI를 변경하고 싶었기에 맞닥뜨린 문제이다.

내가 원했던 UI는 다음과 같다.

  1. 인증 버튼을 눌렀을 때,

  2. 인증이 완료되었을 때, (사용자가 인증 메일의 링크를 클릭했을 때)

다시 한 번 언급하자면, 분명 firebase가 제공하는 기본적인 기능만으로도 충분히 이메일 인증을 구현할 수 있다.
다만, 나는 나만의 흐름대로 인증 작업을 처리하고 싶었고, 그렇기 때문에 위에서 언급한 문제들을 맞닥뜨렸으며 이를 해결해야 했다.

firebase가 기본적으로 제공하는 이메일 인증을 확인하는 작업에 대한 흐름은 다음과 같다.

  1. 인증 메일의 내용으로 함께 전송될 작업 링크를 설정한다.
    -> 이 링크는 실제 앱에서 인증 작업을 처리하는 경로이다.
  2. 사용자는 이 링크를 클릭하면 인증이 완료 된다.

내가 기대했던 것은 1번의 링크를 클릭했을 때, 기존 회원 가입 화면에서 이를 감지하고 화면의 UI가 변경되는 것이었다.

하지만, 이 링크를 클릭했을 때 새로운 탭에서 이 경로에 해당하는 앱의 페이지가 켜졌다.

물론, 인증에 대한 작업은 정상적으로 처리된다.

나는 새로운 탭이 아닌 기존 화면(기존 앱)에서 이를 감지할 수 있었으면 했다.

그래서 처음으로 생각한 흐름은 다음과 같았다.

  1. 인증 링크를 클릭했을 때, 열려지는 새로운 탭에서는 "이메일 인증이 완료되었습니다. 3초 후에 창이 저절로 닫힙니다."라는 문구를 보여주고 3초 뒤에 창이 저절로 닫히게 한다.

  2. 이때, 로컬 스토리지에 인증 유무에 대한 값을 저장한다. (로컬 스토리지는 다른 탭에서 값을 공유할 수 있기 때문에)

  3. 기존 화면에서 이를 감지하여 UI를 변경한다.

하지만, 당연하게도 이 방법으로도 여전히 문제는 해결되지 않았다. (뭔가 그럴듯해 보였지만, 근본적인 문제는 여전히 남아있었다.)

근본적인 문제는 기존 화면에서 사용자가 인증 링크를 클릭한 순간을 감지할 수 있어야 한다는 것이다.

그래서 생각한 것이 인증 작업이 처리 되어야 하는 시간을 설정하고 (예를 들어 60초)

기존 화면에서는 60초 동안 setInterval 이라는 함수를 통해 주기적으로 인증이 되었는지 안 되었는지를 확인을 해야 할까? 였다.

하지만, 근본적인 해결책이 아니라고 생각했기 때문에 시도조차 하지 않았고 다른 방법을 시도했다.

또 다른 방법은 firebase의 동적 링크 (Dynamic Link)였다.

하지만, 이 방법 또한 해결책이 되지 못했다. (모바일 앱을 위한 기능이라고 보면된다.)

이 문제를 해결하기 위해 고민을 하던 중 우연히 스택 오버플로우의 질문을 보게 되었는데 내가 고민했던 것과 100% 같았다.
스택오버플로우 질문 및 답변

그리고 이에 대한 답변들이 모두 setTimeout이라 던지 setInterval을 통해 인증 이벤트를 감지하고 있었다.

이에 따라 내가 구현한 이메일 인증 흐름은 다음과 같다.

  1. 사용자가 인증버튼을 눌렀는지를 감지하는 상태 변수 flag = false 를 설정한다.
const [flag, setFlag] = useState(false);
  1. 이메일을 입력하고, 인증 버튼을 누르면 이메일 인증 메일이 전송되고 flag = true로 변경한다.
  2. useEffect 훅을 이용해 flag가 참이라면, setInterval 함수를 통해 주기적으로 현재 유저 정보를 조회하기 시작한다.
  useEffect(() => {
    if (flag) {
      const intervalId = setInterval(() => {
        auth.currentUser?.reload(); //현재 사용자 정보를 주기적으로 조회한다.
        		...
        }
      }, 1000);
    }
  }, [flag]);
  1. 유저 정보를 조회했을 때, 이메일 인증 유무를 나타내고 있는 필드가 true라면 인증이 완료되었다는 것이고, 이때, cleaerInterval 함수를 이용해 설정된 interval을 해제한다.
  useEffect(() => {
    if (flag) {
      const intervalId = setInterval(() => {
        auth.currentUser?.reload();
        if (auth.currentUser?.emailVerified) { // 이메일이 인증되었을 때,
          setEmailVerificationMessage({
            type: "normal",
            message: "인증이 완료되었습니다.",
          });
          clearInterval(intervalId);
        }
          ...
      }, 1000);
    }
  }, [flag]);

결과 화면


요약 및 결론


node.js를 공부하고 직접 서버 코드를 작성하는 것은 부담이었기에 firebase로 인증 과정을 처리하고자 했다.

하지만, firebase가 제공하는 기본적인 방법으로는 내가 원하는 방식의 인증 흐름을 구현할 수 없었다.

이때, 맞닥 뜨린 문제는 크게 두 가지였다.

  1. 회원 가입 중에 이메일 인증을 하지 못하는 문제
  2. 사용자가 이메일을 인증하는 순간을 감지하지 못하는 문제

이 문제들은 firebase가 기본적으로 제공하는 방법이 아닌 내가 원하는 방법으로 인증을 처리하고자 했기 때문에 발생했다.

그리고 다음과 같은 방법으로 해결했다.

  1. 회원가입 및 이메일을 인증하기 위해 임시로 사용자를 가입시키고 삭제한다.
  2. 인증하는 순간을 감지하기 위해 setInterval 함수를 사용해서 주기적으로 검사한다.

이 해결책들이 다소 비논리적인 코드로 작성되었기 때문에 근본적인 해결 방법은 아니라고 생각한다.

하지만, firebase를 이용해야 하는 상황에서 내가 원하는 흐름대로 인증을 처리하고자 했고,

당장 빠른 기능 개발을 위해서는 어쩔 수 없는 선택이었다.

firebase를 통해 인증을 처리하며 가장 많이 들었던 생각은 "이 시간에 node.js를 공부했어도 되지 않았을까" 였다.

profile
under the hood

8개의 댓글

comment-user-thumbnail
2023년 10월 12일

마치 이야기 나누듯 재밌게 읽었습니다! 공유 감사드려요.

1개의 답글
comment-user-thumbnail
2024년 1월 3일

첫번째 고민을 보면서 아래의 링크가 생각났습니다.
https://firebase.google.com/docs/auth/web/email-link-auth?hl=ko
이걸로 일단 입력한 이메일이 본인소유의 이메일이 확실함을 증명한 후
다시 email / password 방식으로 재가입?재인증? 연결? 처리를 해야할 것 같기도합니다.

추가참고
https://firebase.google.com/docs/auth/web/account-linking?hl=ko

저는 여전히도 계속 고민중입니다.
한가지 방식으로 로그인 한 후에 또 다른 방식으로 로그인을 하여도 가능하도록 하는 방법을 구글에서 제공해주고 있지만, 제공해주는 메서드들을 잘 사용해서 로직을 구현해야할 것 같아요.

1개의 답글
comment-user-thumbnail
2024년 4월 4일

시간이 많이 지났지만 저랑 같은 고민을 하고 계셨네요. (저는 현재진행형 🥹)
재밌게 술술 읽었습니다

1개의 답글
comment-user-thumbnail
2024년 4월 19일

좋은 글 감사합니다~
저도 한번 생각 해 봤는데요. email link로 sign in하는 방법도 있지만
임시메일주소를 사용 해 보는 방법도 생각 해 볼 수 있을 것 같아요
1. 사용자가 e-mail과 비밀번호로 회원가입
2. 해당 e-mail에 인증메일 보냄
3. 난수를 사용한 임시메일 주소를 생성
4. 유저의 uid + 원 e-mail주소 + 임시메일 주소 + 데이터 생성날짜를 함께 firestore에 저장
5. User의 e-mail주소를 임시메일주소로 변경
6. 해당 User를 singout시킴
7. 사용자가 로그인시도시 사용자가 입력한 메일주소가 firestore에 존재하는지 확인
7-1. 없으면 사용자가 입력한 메일주소로 로그인
7-2. 있으면 임시메일주소로 로그인
8. 사용자가 email verified 된 유저인지 확인 후, 인증되지 않았으면 메시지를 띄우고 signout시킴

cloud functions를 사용해서, firestore에서 일정기간마다 데이터의 생성날짜를 확인해서, verified 되지 않은 유저 및 데이터를 삭제함

오히려 너무 복잡해 진 것 같고 시도도 안 해 봤지만, 한번 끄적여 봤습니다

1개의 답글