마스터Q&A 안드로이드는 안드로이드 개발자들의 질문과 답변을 위한 지식 커뮤니티 사이트입니다. 안드로이드펍에서 운영하고 있습니다. [사용법, 운영진]

Navigation Component와 firebase를 이용한 로그인 상태변화 질문.

0 추천

https://www.masterqna.com/android/99827/%EB%A1%9C%EA%B7%B8%EC%9D%B8%ED%99%94%EB%A9%B4%EA%B3%BC-navigation-popupto-%EA%B4%80%EB%A0%A8

이전질문입니다.

-----------

결론만 먼저 말씀드리면 구현을 하는데 성공은 했는데 지금 몇가지 이상한점이 있고 궁금한점이 있습니다.

조건부 탐색  |  Android 개발자  |  Android Developers

기본 개념은 위 링크를 참고했는데 여기에는 ViewModel의 LiveData인 user를 어떻게 처음에 firebase에서 가져와 작성해야하는지 안나와있어서.. 어떻게해야할지 고민하고 찾다가 

Advanced Android in Kotlin 06.2: Android Conditional Navigation with Login (google.cn)

(코드는 8번파트로 이동하셔서 Git 을 참고)

개발자 codelab에 다행히 비슷한 것을 구현한 것이 있어서 코드는 이곳꺼를 보면서 이해하고 사용하려고 했습니다.

 

1.

이곳 코드는 되기는한데 ViewModel에서 AuthenticationState를 강제로 UNAUTHENTICATED로 설정해주지 않으면 앱을 실행했을때 로그인화면으로 이동하지 않았습니다..

그러니까 앱을 시작할때 로그인을 하지 않았는데도 user 검사에서 null이 아니다(값이있다)라는 뜻이겠죠.

실제로 FirebaseUserLiveData 클래스와 ViewModel 클래스에서 user를 디버깅해보니

제가 이전에 로그인 기능을 구현한다고 만들어뒀던 아이디와 토큰값이 있었습니다.

그래서 FireBase에가서 계정값을 다 지워줬는데도 여전히 존재하네요.. cleanbuild와 Rebuild까지해봤는데도 그러네요..

왜이런지 모르겠습니다.

아래는 제가 사용한 코드입니다. (FirebaseUserLiveData는 그대로 사용)

class LoginViewModel : ViewModel() {
    sealed class AuthenticationState {
        object AUTHENTICATED: AuthenticationState()
        object UNAUTHENTICATED : AuthenticationState()
    }
    val authenticationState = FirebaseUserLiveData().map { user ->
        if (user != null) {
            AuthenticationState.AUTHENTICATED
        } else {
            AuthenticationState.UNAUTHENTICATED
        }
    }
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    loginVM.authenticationState.observe(viewLifecycleOwner) { authenticationState ->
        when (authenticationState) {
            LoginViewModel.AuthenticationState.AUTHENTICATED ->
                Toast.makeText(context, "로그인 되어있음",Toast.LENGTH_SHORT).show()
            LoginViewModel.AuthenticationState.UNAUTHENTICATED -> {
                findNavController().navigate(R.id.login_nav)
            }
        }
    }
}

2.

codelab 코드에서는 FirebaseUserLiveData라는 코드를 따로 만들어서 사용하던데 이유가 있을까요?

AuthStateListener를 사용하던데 FireBase 홈페이지를 보니 이것을 연결하면 기본 토큰의 상태가 변경될때마다 호출된다는데 계정의 토큰(UID)를 말하는 것이면.. 토큰은 고유한 값으로 알고있는데 변경될 일이있나요?

onActive()와 onInactive()도 화면 회전같은경우에만 쓰이는것같구요..

 

3.

그리고 이건 제가 codelab 코드를 보기전에 시도한 코드인데 이것은 왜안될까요?

ViewModel

class LoginViewModel : ViewModel() {
    val user = MutableLiveData<UserAccount>().map { user ->
        if(user != null) {
            AuthenticationState.AUTHENTICATED
        }
        else
            AuthenticationState.UNAUTHENTICATED
    }

    sealed class AuthenticationState {
        object AUTHENTICATED : AuthenticationState()
        object UNAUTHENTICATED : AuthenticationState()
    }
}

 

 

Fragment

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    loginVM.user.observe(viewLifecycleOwner) { user ->
        when(user) {
            LoginViewModel.AuthenticationState.AUTHENTICATED ->
                Toast.makeText(context, "로그인 되어있음", Toast.LENGTH_SHORT).show()
            else ->
                findNavController().navigate(R.id.login_nav)
        }
    }
}

 

이 코드에서는 아예 if else문을 건너뜁니다. user를 임의로 null로 설정해보아도 그대로 넘어갑니다.

이유가 뭔가요?

 

 

 

+++++++++++++++

private fun firebaseSignIn() {
        var email: String = binding.etId.text.toString()
        var pwd: String = binding.etPwd.text.toString()

        // Firebase Auth 진행
        mFirebaseAuth.signInWithEmailAndPassword(email, pwd)
            .addOnCompleteListener { task ->
                if(task.isSuccessful) { // 로그인 성공
                    findNavController().navigate(R.id.main_graph)
                    Toast.makeText(context,"로그인 성공", Toast.LENGTH_SHORT).show()
                }
                else // 실패
                    Toast.makeText(context, "로그인 실패", Toast.LENGTH_SHORT).show()
            }
    }

 

codeslave (3,940 포인트) 님이 2022년 1월 21일 질문
codeslave님이 2022년 1월 23일 수정

1개의 답변

+1 추천
 
채택된 답변

1. Firebase Auth는 세션을 캐싱합니다. 따라서 이미 로그인 한 적이 있다면, 디바이스에 토큰을 보관하고 있기 때문에, 로그아웃을 시키지 않으면, 앱을 실행하면 로그인을 하지 않아도 FirebaseUser가 존재하게 됩니다. 님의 앱이 무조건 로그인을 해야하는 앱이라면 초기값을 로그인이 안된 상태로 주셔야 겠죠.

2. Codelab에 사용된 FirebaseUserLiveData는 내부적으로 FirebaseAuthState 를 체크하기 때문에, 여기에 의존할 필요가 있다면, 좋은 구현방법이라고 생각합니다. 대부분의 경우에는 이 클래스가 유용할 것 같아 보이네요. 다만 Clean Architecture를 사용하는 프로젝트라면 Firebase 관련 코드를 ViewModel이 아닌, 데이터 레이어에 두고 싶을 것이기 때문에 LiveData가 아닌 Kotlin Flow 같을 걸 이용해 처리할 것 같습니다.

그리고 FirestateAuthState는 토큰 "상태" 변경을 포함하고 있기 때문에, 로그인 상태변경은 이걸 체크하시면 됩니다.

3. MutableLive는 초기값으로  null 을 갖기 때문에. 생성자에 값을 넘기지 않으면, observe하는 쪽에서 처리가 안되겠죠.
아래처럼 해보세요.

val user = MutableLiveData<UserAccount>(AuthenticationState.UNAUTHENTICATED).map { user ->
        if(user != null) {
            AuthenticationState.AUTHENTICATED
        }
        else
            AuthenticationState.UNAUTHENTICATED
    }
 

 

spark (227,830 포인트) 님이 2022년 1월 21일 답변
codeslave님이 2022년 1월 28일 채택됨
감사합니다. 질문 세가지만 더드릴게요..
1.
CalendarFragment에서 singout() 을 한번해주니 정상적으로 로그인화면으로 가기는하네요. 그런데
1번 답변에서 읽어보니 이게 꼭 자동로그인처럼 들리는데 이게 자동로그인을 의도한건지 아니면 그렇지 않은건지 궁금합니다.. 자동로그인이 의도된것이 맞다면 제가 만약 앱을 시작할때마다 로그인을 원할시에는 Calendar화면에서 로그아웃을 매번하는 로직을 설정해야하나요?

2.
로그인후에 로그인화면이 다시 뜹니다. 디버깅해보니 로그인을 성공하면 BottomNav(CalendarFragment)로 이동을하는데 여기서 다시 로그인여부를 검사할때, UNAUTHENTICATED 값을 가지고 있어서 로그인화면으로 이동을하더라구요. 저는 로그인을하면 user가 값을가지기때문에 ViewModel에서 AUTHENTICATED 값을 가져서 Calendar 화면을 유지할것이라고 생각했는데 말이죠..
어떠한 이유때문인가요?


3.
 https://developer.android.com/guide/navigation/navigation-conditional?hl=ko#kotlin  에서
savedStateHandle = findNavController().previousBackStackEntry!!.savedStateHandle
savedStateHandle.set(LOGIN_SUCCESSFUL, false)
이 나오는데 이게 사용자가 성공적으로 로그인했는지 나타내는 초깃값을 설정하고 사용자가 즉시 시스템 뒤로 버튼을 누르면 반환할 상태 라는데 이게 무슨말인지 잘이해가 안갑니다..
솔직히 이후 NavBackStackEntry 이 언급된 파트이후부터는 무엇을 말하고자하는지 이해가 잘안가요..
1.
네, 기본적으로 자동로그인을 염두에 두고 구현된 것이라고 보면 될 것 같습니다. 그리고 로그인화면에서 로그아웃을 시키는 건 좀 이상한듯 해요. 가장 큰 이유는 Configuration Change 와 Process Death에 대한 처리입니다. Configuration Change와 Process Death가 뭔지는 설명이 좀 길어지니 구글링을 해보시면 좋겠습니다.)
자동 로그인을 방지하기 윈한 방법은 두가지가 떠오르는데,(Single Activity라고 가정하겠습니다)
1) MainActivity에서 onSaveInstance에 로그인 상태를 저장하세요. 앱이 종료되거나 Configuration Change가 발생할 때, 이 함수가 호출될 겁니다.
onCreate에서 Intent를 검사해서 로그인 상태가 없거나 UNAUTHENTICATED 이면 로그인 화면으로 이동시키는 거죠. 물론 ViewModel과 연결된 코드도 작성해야 겠죠.
2) ViewModel(Activity scope)에서 SavedStateHandle을 사용해서 로그인 상태를 저장하세요. SavedStateHandle는 Configuration Change와 Process Death가 발생해도 값을 보관해 줍니다. 따라서 SavedStateHandle을 검사해서 값이 없거나 UNAUTHENTICATED이면 로그인 화면으로 이동하면 되겠죠. 두번째 방법이 구조적으로는 좀 더 좋을 것 같네요.

결과적으로, 자동로그인을 원하지 않으면, 로그인상태를 로컬 로그인 상태 + 파이어베이스 로그인 상태를 합쳐서 처리해야 할 것으로 보입니다. 파이이버에스의 AuthState Listener에서는 로그인, 로그아웃 후의 변경된 상태만 SaveStateHandle에 저장하고 네비게이션은 SaveStateHandle에만 의존을 하시는 게 맞을 것 같네요.

2.
증상이, Activity scope 으로 뷰모델을 공유한게 아니라, CalendarFragment의 뷰모델에서 다시 로그인 검사를 하고 있는 것 같네요. 제 말이 맞다면, LiveData의 값이  공유된게 아니기 때문에 당연히 로그인상태가 공유되지 않겠죠.

3.
네비게이션 컴포넌트는 내부적으로 SaveStateHandle을 사용합니다. 이건 LiveData Map라고 보시면 됩니다. 해당 문서에서는 네비게이션 컴포넌트를 사용할 때 프레그먼트 간에 어떻게 communication을 하는지에 대해 말하고 있는 겁니다. 즉 Fragment A-> Fragment B로 이동했을 때, A에서 B로부터 BackStackEntry의 savedStateHandle을 통해서 데이텅의 전달이 가능하다는 것입니다.

https://developer.android.com/guide/navigation/navigation-programmatic

위의 링크를 보시면 A와 B에서 각각 처리해줘야 할 부분들에 대해서 알려줍니다.
으으 감사합니다.. 선생님 그런데 제가 마지막에 Firebase 로그인(인증)하는 코드를 추가했습니다,LoginFragment에 작성된 코드구요.
이건 MVVM에서 View 단으로 두는게 맞나요(현자상태 유지유지)아니면 Model 단에 두어서 Repository에 두는게 맞는건가요?

https://developer.android.com/guide/navigation/navigation-conditional#kotlin
의 userViewModel.login(username, password).observe()를보면 login 인증검사를 viewmodel 혹은 repository에서 진행하는것같은데 FirebaseAuth도 그렇게 해도될까요?

추가적으로 위 링크의 로그인 로직과
https://github.com/googlecodelabs/android-kotlin-login-navigation/tree/master/app/src/main/java/com/example/android/firebaseui_login_sample
이 로직과 크게 차이는 없죠?
Firebase는 초기화 코드만 Application클래스에 있으면 되고 나머지는 다 데이터 레이어에 있는 게 맞다고 봅니다. 파이어베이스는 데이터 소스에 해당하니까요. 레이어를 분리하되 아키텍쳐를 심플하게 가져가고 싶다면, Repository 정도에서 처리해 주시면 좋을 것 같구, 레이어 분리의 필요성을 못 느끼시면 ViewModel에서 하시면 되구요. Clean Architecture식으로 하시려면, DataSource까지 만드셔야 겠지만, 프로젝트가 크지 않다면, 좀 과한 구조가 될 수도 있습니다.

흠... 그리고 죄송한데, Repository를 통째로 샘플 코드 링크를 주시면, 코드를 보기가 쉽지 않습니다. 제가 뭐라고 말씀드리기가 좀 힘들 것 같아요. 이 부분은 직접 비교하시거나, 구체적인 코드를 보여주시면 좋을 듯 합니다.
아아 감사합니드.. Application클래스라는게 프래그먼트 액티비티등을 지칭하는 건가요?

맞다면, 프래그먼트에서 초기화를하고 레퍼지토리나 뷰모델에 인스턴스를 보내는 방식으로 해야하겠네요..?
...