[TIL] Android 앱 개발 최종 : 포트폴리오 프로젝트 16

지혜·2024년 3월 21일

Android_TIL

목록 보기
69/70

✏240321 목요일 TIL(Today I learned) 오늘 배운 것 ~
✏240322 금요일 TIL(Today I learned) 오늘 배운 것

📖Firebase를 활용한 구글 로그인 (Authentication)

  • 구글 간편로그인에 대해서 간단히 찾아보니까, 구글 클라우드 콘솔을 통해서 바로 만드는 방법과 파이어베이스의 Authentication을 통해 간편로그인을 구현하는 2가지 방법이 있었다.

  • 우리는 이미 파이어베이스를 사용하고 있기도 하고, 파이어베이스에서 자체적으로 보안 기능을 내장하고 있기도 하다고 해서 별 고민 없이 Authentication로 간편 로그인을 구현하였다.

     //LoginActivity.kt
    private lateinit var firebaseAuth: FirebaseAuth
    private lateinit var googleSighInClient: GoogleSignInClient
    private lateinit var googleLoginResult: ActivityResultLauncher<Intent>
    
     override fun onCreate(savedInstanceState: Bundle?) {
           initActivityResult()
           firebaseAuth = FirebaseAuth.getInstance()
           val googleSignInOptions = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
               .requestIdToken(getString(R.string.google_web_client_id))
               .requestId()
               .requestEmail()
               .requestProfile()
               .build()
           googleSighInClient = GoogleSignIn.getClient(this, googleSignInOptions)
    
           binding.cvGoogle.setOnClickListener {
               googleSignIn()
           }
     }
     
         private fun initActivityResult() {
           googleLoginResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
               if (result.resultCode == Activity.RESULT_OK) {
                   val data = result.data
                   try {
                       val task = GoogleSignIn.getSignedInAccountFromIntent(data)
                       val account = task.getResult(ApiException::class.java)
                       firebaseAuthWithGoogle(account.idToken)
                   } catch (e: ApiException) {
                       // Google 로그인 실패 처리
                       Timber.tag("GoogleLoginError").d("Google 로그인 실패: ${e.statusCode}")
                       finish()
                   }
               } else {
                   // 사용자가 로그인을 취소했거나 결과가 OK가 아닐 때의 처리
                   Timber.tag("GoogleLoginError").d("로그인 사용자 취소 또는 실패")
                   finish()
               }
           }
       }
    
       private fun firebaseAuthWithGoogle(idToken: String?) {
           val credential = GoogleAuthProvider.getCredential(idToken, null)
           binding.clLoginLoading.visibility = View.VISIBLE
           val db = Firebase.firestore
           firebaseAuth.signInWithCredential(credential).addOnSuccessListener { result ->
               val documentRef = db.collection("users").document("Google${result.user?.uid}")
               val encryptedNickName = result.user?.displayName?.take(10)?.let { encrypt(it, AES_KEY) }
               val encryptedUserEmail = result.user?.email?.let { encrypt(it, AES_KEY) }
               val userModel = hashMapOf(
                   "userId" to "Google${result.user?.uid}",
                   "nickName" to encryptedNickName,
                   "profileImage" to "${result.user?.photoUrl}",
                   "userEmail" to encryptedUserEmail,
                   "bookmarked" to null
               )
               documentRef.get().addOnCompleteListener { task ->
                   if (task.isSuccessful) {
                       val documentSnapshot = task.result
                       if (!documentSnapshot.exists()) {
                           profileImgUpload(Uri.parse(result.user?.photoUrl.toString()), "Google${result.user?.uid}")
                           documentRef.set(userModel)
                           Toast.makeText(this, "로그인 성공!", Toast.LENGTH_SHORT).show()
                       }
                   }
                   Toast.makeText(this, "환영합니다.", Toast.LENGTH_SHORT).show()
                   EncryptedPrefs.saveMyId("Google${result.user?.uid}")
                   binding.clLoginLoading.visibility = View.GONE
                   finish()
               }
           }
               .addOnFailureListener {
                   Timber.tag("GoogleLoginError").d(it.toString())
                   binding.clLoginLoading.visibility = View.GONE
                   finish()
               }
       }
    
       private fun googleSignIn() {
           val signInIntent = googleSighInClient.signInIntent
           googleLoginResult.launch(signInIntent)
       }
    • 파이어베이스에서 Authentication을 활성화 시키는 방법은 어렵지 않으니 바로 코드 설명으로 들어가 보자면,

    • onCreate에서 initActivityResult(googleLoginResult)가 초기화 될 수 있도록 함수를 따로 빼서 걸어주었다.
      또, firebaseAuthgoogleSighInClient도 같이 초기화 해주었다.
      이때, googleSignInOptions을 선언하여 필요한 값인 고유아이디(UUID로 활용), 이메일주소, 프로필이미지 를 받아올 수 있도록 설정값을 저장했다.

    • initActivityResult에서는 구글 로그인 버튼을 클릭 했을 때, 구글 로그인 창을 통해 선택한 구글 간편 로그인 아이디의 값(데이터)들을 저장해서 firebaseAuthWithGoogle에 넘겨준다.

    • firebaseAuthWithGoogle을 통해 직접 로그인 절차를 수행하게 되는데, 로그인이 되는 동안 binding.clLoginLoading.visibility = View.VISIBLE를 통해 로딩화면이 보여지도록 설정해주었다.
      후에는, 카카오 로그인 절차와 동일하게 존재하는 유저인지 확인하기 위해서 파이어스토어를 연결해주고, 첫 로그인 시에 유저 정보를 저장할 수 있도록 userModel = hashMapOf()을 만들어주었다.
      if (!documentSnapshot.exists()) 를 통해 존재하지 않는 유저라면 documentRef.set(userModel) 유저모델 값을 새롭게 저장한다.
      존재한다면 EncryptedPrefs.saveMyId("Google${result.user?.uid}")로 SharedPrefernce에 유저 아이디 값을 저장하고 로그인 통과를 시켜준다.
      binding.clLoginLoading.visibility = View.GONE 로그인이 통과/실패 된다면 로딩 중 화면을 없앤 후 원래 페이지로 돌아가도록 finish함수를 사용했다.

    • [트러블 슈팅] 로그인 과정 중에 뒤로가기 등을 누르면, 로그인 프로세스만 종료되는 게 아니라 강제 종료가 되기 때문에 addOnFailureListenertry-catch를 통해서 앱 크래시가 나지 않도록 에러 처리를 했다. 이 포스트의 코드에는 나오지 않지만, 카카오 로그인 시에도 오류 메시지로 사용자에게 혼란을 주지 않기 위해 로그처리를 해주었다.

    • [트러블 슈팅] 원래는 첫 로그인 시 프로필 이미지를 받아올 때, 프로필 URI를 스토리지에 먼저 업로드 하고, 스토리지에 업로드 된 이미지의 URI를 받아오는 방식으로 코드를 짰었다. 그랬더니 StorageException has occurred. 오류가 발생해서 profileImgUpload함수를 콜백함수로 변경하여 사용했는데, 처음 로그인을 할 때 5초 이상의 시간이 걸렸다.
      -> 팀원들과 상의한 결과, 처음에는 그냥 간편로그인 시 제공해주는 프로필 URI를 바로 유저데이터에 올리고, 스토리지에는 따로 프로필이미지를 업로드하여 파일을 미리 생성하는 방법을 사용하기로 했다.


📖유저데이터 정보 불러오기 뷰모델로 전환

  • 화면이 로드될 때마다 직접 ProfileFragment에서 파이어베이스에 연결된 데이터들을 불러왔더니, 매번 새로 데이터가 들어오는 모습이 보여져서 화면이 깜빡 거리는 것처럼 보인다는 피드백이 들어왔다. 그래서 이참에 데이터를 뷰모델로 저장해서 불러올 수 있도록 코드를 리팩토링 했다.
    //package com.brandon.campingmate.data.remote.dto
    data class UserDTO(
       val userId: String? = null,
       val userEmail: String? = null,
       val nickName: String? = null,
       val profileImage: String? = null,
       val bookmarked: List<String>? = null
    )
    
    //ProfileViewModel.kt
    private val _userData: MutableLiveData<UserDTO?> = MutableLiveData()
    val userData: LiveData<UserDTO?> get() = _userData
    
    fun getUserData(userID: String) {
           val db = FirebaseFirestore.getInstance()
           val docRef = db.collection("users").document(userID)
           docRef.get().addOnSuccessListener {
               val item = it.toObject(UserDTO::class.java)
               _userData.value = item
           }
               .addOnFailureListener {
                   Timber.tag("LoadUserDataFail").d(it.toString())
               }
       }
       
     //ProfileFragment.kt 의 initLogin() 함수 안
    viewModel.userData.observe(viewLifecycleOwner) {
                   val decryptedNickName = it?.nickName?.let { it1 -> decrypt(it1, AES_KEY) }
                   val decryptedEmail = it?.userEmail?.let { it1 -> decrypt(it1, AES_KEY) }
                   if (profileImgUri == null) {
                       ivProfileImg.scaleType = ImageView.ScaleType.CENTER_CROP
                       Glide.with(binding.root).load(it?.profileImage).into(ivProfileImg)
                       ivProfileImg.visibility = View.VISIBLE
                       if (llEditConfirm.visibility == View.GONE) {
                           tvProfileName.textSize = 20f
                           tvProfileName.text = decryptedNickName
                       }
                       tvProfileEmail.text = decryptedEmail
                   }
               }
    • UserDTO의 경우, 이미 다른 파일에서도 사용하고 있는 곳들이 있어서, 기존에 생성된 것을 그대로 활용하여 사용하기로 했다. 파이어스토어에있는 유저 데이터 필드 이름과 동일하게 작성되어있다.
    • ProfileViewModel.kt에서는 toObject함수를 통해서 파이어스토에서 가져온 정보를 UserDTO 인스턴스로 전환하여 저장하고 그것을 _userData에 담아서 저장한다.
    • 그리고 ProfileFragment.kt에서 viewModel.userData.observe 유저데이터를 옵저빙 하여 화면에 뿌려준다. 이때, 닉네임(이름)과 이메일 부분은 개인정보에 가깝기 때문에 파이어 스토어에 저장될 때 암호화 처리를 해주었기 때문에 복호화해서 가져오는 작업을 해줬다.
profile
파이팅!

0개의 댓글