구글 간편로그인에 대해서 간단히 찾아보니까, 구글 클라우드 콘솔을 통해서 바로 만드는 방법과 파이어베이스의 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)가 초기화 될 수 있도록 함수를 따로 빼서 걸어주었다.
또, firebaseAuth와 googleSighInClient도 같이 초기화 해주었다.
이때, 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함수를 사용했다.
[트러블 슈팅] 로그인 과정 중에 뒤로가기 등을 누르면, 로그인 프로세스만 종료되는 게 아니라 강제 종료가 되기 때문에 addOnFailureListener와 try-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
}
}ProfileViewModel.kt에서는 toObject함수를 통해서 파이어스토에서 가져온 정보를 UserDTO 인스턴스로 전환하여 저장하고 그것을 _userData에 담아서 저장한다.ProfileFragment.kt에서 viewModel.userData.observe 유저데이터를 옵저빙 하여 화면에 뿌려준다. 이때, 닉네임(이름)과 이메일 부분은 개인정보에 가깝기 때문에 파이어 스토어에 저장될 때 암호화 처리를 해주었기 때문에 복호화해서 가져오는 작업을 해줬다.