[Android] Firebase 전화번호 인증 + MVVM 패턴 적용

그렌실·2022년 8월 21일

안드로이드를 공부하면서 mvvm패턴에 대해 많이 듣고 공부하였지만 이번 미니 프로젝트를 진행하면서 좀 더 익히게 된 것 같다.

https://medium.com/firebase-tips-tricks/how-to-create-a-clean-firebase-authentication-using-mvvm-37f9b8eb7336

위 블로그 글은 Medium에 올려져 있는 firebase auth + mvvm 패턴으로 구글 인증->로그인 과정을 구현한 것인데 나는 이것을 공부한 뒤 전화번호 인증으로 바꾸어 보았다.

내가 공부한 내용을 정리해보도록 하겠다.

내가 만든 내용은 간단히 정리하자면

-전화번호를 입력하고 유효한 번호인지 아닌지 판별
-유효하다면 핸드폰으로 sms 인증코드(6자리)를 보낸다.
-인증번호를 입력한 뒤 인증번호가 맞으면 MainActivity 진입

요렇게 정리할 수 있겠다. 막상 정리하고 보니 별거 아닌것 같다.

1. 인증번호 발송 버튼 클릭 시

 private fun sendNumber() {
        val phonenumber = binding.edittextPhonenumber.text.toString()
        if(isValidNumber(phonenumber)) {
            authViewModel!!.sendVerifyNumber(this, national_phonenumber!!)
            }
        else{
            Log.i("AuthActivity:","Invalid PhoneNumber")
        }
    }

-위 함수는 전화번호를 입력한 뒤 호출되는 함수로써 내가 입력한 전화번호를 입력 받아 isValidNumber()를 통해 유효한 번호인지 검사한 뒤 인증번호를 보낸다.

2. ViewModel의 sendVerifyNumber 함수

 fun sendVerifyNumber(context: AuthActivity, phoneNumber: String) {
        authRepository.sendVerifyNumber(context,phoneNumber)
    }

-ViewModel 안에서는 repository의 함수를 호출하는 연결하는 방식으로 진행하였다. 이 과정에서 따로 livedata나 옵저버 배턴을 쓸 필요는 없다고 느꼈다.(어차피 repo 의 firebase 콜백함수가 코드보냈을 때 코드가 인증되었을 때 등의 이벤트를 감지해주기 떄문)

3. repository의 sendVerifyNumber 함수

fun sendVerifyNumber(context: AuthActivity, phoneNumber: String) {

        val options = PhoneAuthOptions.newBuilder(firebaseAuth)
            .setPhoneNumber(phoneNumber)       // Phone number to verify
            .setTimeout(60L, TimeUnit.SECONDS) // Timeout and unit
            .setActivity(context)                 // Activity (for callback binding)
            .setCallbacks(callbacks)          // OnVerificationStateChangedCallbacks
            .build()
        PhoneAuthProvider.verifyPhoneNumber(options)

    }

-내가 입력한 phonenumber로 내가 설정한 60L(60초)만큼의 제한 시간을 주어 인증번호를 보낸다.

-PhoneAuthProvider.verifyPhoneNumber(options)를 호출하게 되면 특이사항이 없을 시 firebase의 callback 함수를 타게 된다. callback 함수는 아래에서 설명하도록 하겠다.

private val callbacks  = object : PhoneAuthProvider.OnVerificationStateChangedCallbacks() {
        override fun onVerificationCompleted(credential: PhoneAuthCredential) {
            Log.d("AuthRepository", "onVerificationCompleted:$credential")
            //signInWithPhoneAuthCredential(credential)
        }
        override fun onVerificationFailed(e: FirebaseException) {
            // This callback is invoked in an invalid request for verification is made,
            // for instance if the the phone number format is not valid.
            Log.w("AuthRepository", "onVerificationFailed", e)

            if (e is FirebaseAuthInvalidCredentialsException) {
                // Invalid request
            } else if (e is FirebaseTooManyRequestsException) {
                // The SMS quota for the project has been exceeded
            }
        }
        override fun onCodeSent(
            verificationId: String,
            token: PhoneAuthProvider.ForceResendingToken
        ) {
            Log.d("AuthRepository", "onCodeSent:$verificationId")
            // Save verification ID and resending token so we can use them later
            storedVerificationId = verificationId
            resendToken = token
        }
    }

-repository 최상단에 콜백을 선언해주었고 이름에서 쉽게 어떤 역할을 하는지 알 수 있겠지만 순서대로 인증이 성공하였을 때, 실패하였을 때, 인증번호를 보냈을 때 탄다.

-onCodeSent를 타게 된다면 자신이 입력한 핸드폰 번호에 실제로 6자리 인증번호가 날라오게 되고 verificationId는 인증번호의 유효성을 체크해주는 id값이라고 보면 되고 resendToken은 재전송시 필요한 토큰이다. 나중에 인증을 체크하고 재전송을 보내기 위해 전역변수에 따로 저장해두었다.

4. 다음 버튼 클릭 시

-인증번호가 왔다면 화면에 인증번호를 6자리를 입력하고 다음버튼을 눌러보자.

private fun next() {
        //다음 눌렀을 때 -> 여기서 내가 입력한 인증번호를 넘겨줌..
        authViewModel!!.verifyPhoneNumberWithCode(binding.edittextAuth.text.toString())
        authViewModel!!.authenticatedUserLiveData!!.observe(this) { authenticatedUser ->
            if (authenticatedUser.isNew) {
                //파이어스토어에 들어가게함
                createNewUser(authenticatedUser)
            } else {
                goToMainActivity(authenticatedUser)
            }
        }
    }

-credential(인증번호 인증서 같은 역할)을 생성하기 위해 viewmodel의 verifyPhoneNumberWithCode를 호출한다. 이 함수는 내가 입력한 인증번호를 입력하면 제대로 인증번호 6자리를 입력했는지 안했는지 처리 해주는 함수라고 보면 된다.

-또한 처리가 잘 되었을 때에는 authViewModel 내의 LiveData 값인 User data 중에 isNew의 값이 true 이면 새롭게 가입하는 계정이고 false면 이미 존재하는 계정이다. (이 내용은 repository 와 User Class를 확인해봐야 자세히 알 수 있다)

  • 따라서 isNew의 값에 따라 새롭게 계정을 만들지 아니면 바로 uid를 갖고 메인으로 진입시킬 지 결정하는 분기문이필요하다.

5. ViewModel의 verifyPhoneNumberWithCode 함수

fun verifyPhoneNumberWithCode(number: String) {
        authenticatedUserLiveData = authRepository.verifyPhoneNumberWithCode(number)
    }

-이 함수가 이제 본격적으로 내가 입력한 핸드폰 번호와 인증코드의 유효성이 맞는지 아닌지를 체크하고 -> 최종적으로는 firestore에 User 데이터를 liveData 넣는 과정이다.

-liveData를 사용하면 메모리 누수 등의 이점이 있다고 한다.(User 데이터가 계속 생성되는 등)

6. Repository의 verifyPhoneNumberWithCode 함수

fun verifyPhoneNumberWithCode(number: String) : MutableLiveData<User> {
        val phoneCredential = PhoneAuthProvider.getCredential(
            storedVerificationId!!,
            number)
        return signInWithPhoneAuthCredential(phoneCredential)
    }

-repository 에서 이 함수가 호출 되면 인증번호가 유효성이 맞는지 callback 함수가 호출 된다.(onVerificationCompleted 또는 onVerificationFailed)

fun signInWithPhoneAuthCredential(credential: PhoneAuthCredential) :MutableLiveData<User> {
        val authenticatedUserMutableLiveData = MutableLiveData<User>()
        firebaseAuth.signInWithCredential(credential)
            .addOnCompleteListener {  authTask ->
                if (authTask.isSuccessful) {
                    val isNewUser =
                        authTask.result.additionalUserInfo!!.isNewUser
                    val firebaseUser = firebaseAuth.currentUser
                    if (firebaseUser != null) {
                        val uid = firebaseUser.uid
                        val name = firebaseUser.displayName
                        val email = firebaseUser.email
                        val user = User(uid, name, email)
                        user.isNew = isNewUser
                        authenticatedUserMutableLiveData.setValue(user)
                    }
                } else {
                    Log.i("AuthRepository", authTask.result.toString())
                }
            }
        return authenticatedUserMutableLiveData
    }

-fireabaseAuth 상의 인증이 성공한다면 livedata를 온전히 return 할 것이고 그렇지 않다면 비어있는 최초의 authenticatedUserMutableLiveData를 리턴 할 것이다.

class User : Serializable {
    var uid: String? = null
    var name: String? = null
    var email: String? = null

    @Exclude
    var isAuthenticated = false

    @Exclude
    var isNew = false

    @Exclude
    var isCreated = false

    constructor() {}
    internal constructor(uid: String?, name: String?, email: String?) {
        this.uid = uid
        this.name = name
        this.email = email
    }
}

-User Class 는 위와 같이 IsAuth,isNew,isCreated 등은 false로 초기화 한다. 후에 생성될 떄 true로 설정하여 firestore에 저장한다.

7. Main으로 넘어와서

private fun next() {
        //다음 눌렀을 때 -> 여기서 내가 입력한 인증번호를 넘겨줌..
        authViewModel!!.verifyPhoneNumberWithCode(binding.edittextAuth.text.toString())
        authViewModel!!.authenticatedUserLiveData!!.observe(this) { authenticatedUser ->
            if (authenticatedUser.isNew) {
                //파이어스토어에 들어가게함
                createNewUser(authenticatedUser)
            } else {
                goToMainActivity(authenticatedUser)
            }
        }
    }

-Main 내의 observe 함수가 liveData의 값을 실시간으로 감지하여 새로 계정을 생성하던가 메인으로 바로 태우던가 결정한다. (위에 얘기한 부분)

자세한 코드는

2개의 댓글

comment-user-thumbnail
2024년 6월 27일

혹시 에뮬레이터로 실행하면 push 알림이 원래 안오나요?

1개의 답글