파이어베이스에 관한 내용을 배우면서 과제로 수행했던 내용이 있었다.
로그인 및 이메일 계정 생성, 로그인과 로그아웃 및 계정 정보 수정, 이메일 인증, 비밀번호 재설정 및 계정 탈퇴 등 많은 기능에 대해 배웠고 일부는 따로 시간을 내서 직접 구현했다.
여기에서 이메일을 생성하는 과정에서 이메일의 중복 여부를 체크하는 과정에서 많은 난관을 겪었기 때문에, 해당 부분에 대해 어떤 고민을 했는지 정리하면서 전체적인 초기 코드도 정리하려고 한다.
처음에는 위와 같은 과제를 받았을 때 익숙하지는 않아도 시간을 들이면 다 해결할 수 있을 것이라고 생각했다. 하지만 이 사용 가능한 이메일이라고 체크하는 부분이 의외로 쉽지가 않았다.
해당 부분의 구현이 왜 어려운지에 대해서 서술하고자 한다.
우선 위에 설명한 기능을 구현하기 쉽지 않은 이유에 대해 알아보고자 한다.
해당 기능을 구현하려면 우선 어떻게 진행해야 할까? 처음에는 아래와 같은 방법으로 생각했다.
결국 이 문제를 해결하기 위한 방법은 해당 이메일로 가입한 이메일이 존재하느냐, 존재하지 않느냐 판별하는 것이 중요하다.
그 방식에 대해 고민해 보기 위해서 해당 기능이 존재하는지 확인해 보았다.
우선 공식 문서 기준으로 이메일과 관련된 함수는 다음과 같은 것이 전부이며, 이 중에서 그나마 사용할 수 있는 것은 FetProvidersForEmailAsync(string email) 정도였다.
즉 이 기능을 사용해서 중복이 될 경우에 에러코드를 판별해서 중복이다 에러일 경우 팝업을 띄우고, 이 코드를 통과했을 경우에는 사용 가능하다는 것을 출력한다. 이와 같은 방식을 떠올렸다.
다만 위의 방법을 썼을 때 원하는 대로 적용이 되지 않았기 때문에 이와 같이 따로 리서치를 해 보았다.
문제는 이와 같았다.
파이어베이스는 이메일 열거 보호 기능이란 것이 있다. 이 이메일 열거 보호 기능이란, 간단히 설명하자면 다음과 같다.
이메일 열거 보호
특정 이메일의 로그인 및 가입이 실패했을 때, 해당 사유를 감추는 것.
가입자의 보안 상의 보호를 위해, 해당 이메일이 가입되어 있는지/이메일 형식이 맞는지/비밀번호만 틀린 건지 등의 여러 로그인/가입 실패 사유를 공개하는 것 자체가 보안 상의 위협이 될 수 있으므로 공개하지 않는다.
이에 따라서 FetProvidersForEmailAsync(string email) 를 통해서 이메일의 가입 여부를 판별하는 것 또한 이메일 열거 보호 기능의 의도에는 저촉되는 것이다.
따라서 해당 기능을 통해 에러 코드의 내용을 확인해보려고 디버그를 찍어봤는데, 디버그가 전혀 찍히지 않고 넘어가는 것을 확인할 수 있었다.
(가입 가능한 이메일을 넣어도 그냥 통과해버리고, 가입 불가능한 이메일을 넣어도 그냥 통과해버린다)
왜 사용도 불가능한 기능이 공식 문서에서는 적혀 있었는지, 그 사용 방법에 대해서는 후술할 예정이다.
파이어베이스 공식 문서를 뒤져봐도 이메일의 중복 여부를 조회하는 방법에 대해서는 안 적혀 있다 보니, 결국 챗GPT에도 한 번 방법을 물어봤다. 그 결과 한 가지 메서드에 대해서 알게 되었다,
FetchSignInMethodsForEmailAsync(email) 이라는 메서드라는 것에 대해 알게 되었는데, 막상 이걸 직접 써 보려니 적용이 되지 않는 것을 확인했다.
애초에 없는 메서드라고 뜨고, 검색을 해 봐도 Firebase 공식 문서에서는 해당 내용에 대해 뜨지 않았다. 아무리 챗GPT가 내용을 잘못 전달할 수 있더라도 아예 없는 내용을 만들어낼 리는 없는데, 원인이 뭔지 리서치를 하다가 결국 내용을 찾아냈다.
이메일 링크 인증 방법에서, FetchSignInMethodsForEmailAsync(email) 라는 방식은 이메일 열거 보호로 인해 사용이 중지된 기능이란 것을 알게 된 것이다. 또한 여기서 이메일 열거 보호에 대한 문서도 상세내용을 읽어보았다.
그러니까 이메일 열거 보호에 따른 보안 상의 문제로 아예 이메일의 중복 여부를 체크할 수 없게 처리했다는 것이다. 그러므로 이메일이 중복일 경우에 오류로 생성을 막을 수는 있지만, 그게 이메일이 중복되서인지 원인을 체크가 불가능하고 사용이 가능할 때에도 체크가 불가능하다는 것이다.
이거 생각보다 해결하기 매우 어려운 문제라는 것을 점점 깨닫고 있었다.
결국엔 방법을 틀어 다음과 같은 방법을 생각했다.
CreateUserWithEmailAndPasswordAsync(string email, string password) 를 사용할 때에는 기본적으로 오류가 걸린다.
InnerException 자체로는 오류의 사유를 판별할 수 없지만 .Flatten()을 통해서 오류를 구체적으로 드러내는 것이 가능하다.
간단하게 그 방식에 대한 코드를 다루자면 아래와 같다.
FirebaseManager.Auth.CreateUserWithEmailAndPasswordAsync(idInput.text, passInput.text)
.ContinueWithOnMainThread(task =>
{
if (task.IsCanceled)
{
Debug.LogError("이메일 가입 취소됨");
return;
}
if (task.IsFaulted)
{
Debug.LogError($"이메일 가입 실패함. 이유 {task.Exception.InnerException}");
foreach (var e in task.Exception.Flatten().InnerExceptions)
{
if (e is FirebaseException firebaseEx)
{
var errorCode = (AuthError)firebaseEx.ErrorCode;
switch (errorCode)
{
case AuthError.EmailAlreadyInUse:
Debug.LogError("이미 사용 중인 이메일입니다.");
ShowPopUp("Already used Email.");
break;
case AuthError.InvalidEmail:
Debug.LogError("유효하지 않은 이메일 형식입니다.");
ShowPopUp("Invalid Email.");
break;
case AuthError.WeakPassword:
Debug.LogError("비밀번호가 너무 약합니다. 6자 이상 입력해주세요.");
ShowPopUp("Too weak Password.");
break;
default:
Debug.LogError($"기타 오류 발생: {errorCode}");
ShowPopUp("Unidentified Error");
break;
}
}
else
{
Debug.LogError($"예상치 못한 오류: {e.Message}");
ShowPopUp("Unidentified Error");
}
}
}
});
}
오류 코드를 드러날 수 있게 .Flatten() 으로 오류 내용을 구체적으로 처리하고 이 오류코드의 내용에 따라 Switch문에 따라 처리를 했다. 이렇게 하면 실제로 이메일이 중복될 때 가입을 방지하고 중복된다는 팝업을 띄우는 것은 가능하다. 하지만 한 가지 문제가 더 생긴다.
가입이 가능한 시점에서 이미 이메일을 생성해버리기 때문에 가입이 가능합니다 라고 띄우고 나서 가입을 시키는 방식이 아니라, 그 전에 이메일 계정을 생성시켜버린다. 저 이메일 계정을 생성하는 과정 자체를 일시적으로 멈추는 것이 불가능하단 소리다.
이 문제 때문에 아주 골머리를 앓았고, 결과적으로 아래와 같이 방식을 설정했다.
/// <summary>
/// 가입 시도를 함.
/// </summary>
private void SignUpAttempt()
{
string email = idInput.text;
if (string.IsNullOrEmpty(email))
{
ShowPopUp("Enter Email.");
return;
}
FirebaseManager.Auth.CreateUserWithEmailAndPasswordAsync(idInput.text, passInput.text)
.ContinueWithOnMainThread(task =>
{
if (task.IsCanceled)
{
Debug.LogError("이메일 가입 취소됨");
return;
}
if (task.IsFaulted)
{
Debug.LogError($"이메일 가입 실패함. 이유 {task.Exception.InnerException}");
foreach (var e in task.Exception.Flatten().InnerExceptions)
{
if (e is FirebaseException firebaseEx)
{
var errorCode = (AuthError)firebaseEx.ErrorCode;
switch (errorCode)
{
case AuthError.EmailAlreadyInUse:
Debug.LogError("이미 사용 중인 이메일입니다.");
ShowPopUp("Already used Email.");
break;
case AuthError.InvalidEmail:
Debug.LogError("유효하지 않은 이메일 형식입니다.");
ShowPopUp("Invalid Email.");
break;
case AuthError.WeakPassword:
Debug.LogError("비밀번호가 너무 약합니다. 6자 이상 입력해주세요.");
ShowPopUp("Too weak Password.");
break;
default:
Debug.LogError($"기타 오류 발생: {errorCode}");
ShowPopUp("Unidentified Error");
break;
}
}
else
{
Debug.LogError($"예상치 못한 오류: {e.Message}");
ShowPopUp("Unidentified Error");
}
}
}
// 보완 필요 - 이메일을 생성 후 체크해서 삭제하는 방식이라 비효율적
// 해당 방식은 데이터베이스로 등록된 이메일과 대조하여
// 등록되지 않은 이메일인지 확인하는 방식으로 전환 예정
tempUser = task.Result.User;
ShowConfirmPopUp("Valid Email.\nCreate Account?");
Debug.Log("가입성공");
});
}
/// <summary>
/// 사용가능한 이메일일 시 팝업을 닫으면서 승인
/// </summary>
private void ConfirmSignUp()
{
if (tempUser != null)
{
tempUser = null;
popupConfirmPanel.SetActive(false);
}
}
/// <summary>
/// 사용가능한 이메일일 시 팝업을 닫으면서 해당 계정 삭제
/// </summary>
private void RevertSignUp()
{
if (tempUser != null)
{
tempUser.DeleteAsync();
tempUser = null;
popupConfirmPanel.SetActive(false);
}
}
이와 같이 최종적으로 코드를 정리하였으며, 방식은 다음과 같다.
이와 같은 방식으로 이메일의 중복 여부를 판별하고, 생성 가능할 때의 팝업을 띄우고 생성 여부를 결정하는 팝업을 띄우는 방식을 구현했다.
이와 같은 방법에는 다음과 같은 한계점이 있다.
그렇다면 다른 해결 방법은 없을까? 해결할 수 있는 방법은 크게 다음과 같다.
사실 이 문제에서 가장 간단한 방법은, 이메일 열거 보호 기능을 끄는 방법이다.
이메일 열거 보호를 끄고 나서 FetchProvidersForEmailAsync() 를 사용하면 이메일의 중복 여부를 판별할 수 있으며, 여기서 이메일이 중복일 경우 1, 중복이 아닐 경우에 0이 출력된다고 한다.
하지만 솔직히 나는 굳이 보안 상의 이유로 권장으로 둔 이메일 열거 보호를 끄면서까지 이걸 구현해야 하나 싶어서 안 한 것이기에, 그렇게 사용하고 싶지 않은 방법이었다.
다음으로 생각할 수 있는 방법은, 이메일이 생성이 완료되었을 때 해당 계정을 파이어베이스 데이터베이스에 저장하는 방식이다. 이에 따라 저장된 이메일 리스트를 만들어 놓고, 누군가가 이메일을 생성할 때마다 해당 데이터베이스에 있는 이메일 목록과 대조하여 같은 이메일이 있는지 찾은 다음, 없으면 생성 가능하다고 띄운 다음 생성 버튼을 누르면 생성시키는 방식을 사용하는 것이다.
이 방법을 사용하려면 이메일 주소의 형식이나 여러가지 조건에 대해 전부 직접 구현한 다음에 굴려야 할 것 같은 어려움이 있겠지만, 생각해 본 방법 중에서 가장 정석에 가까운 방법이라고 생각이 된다.