안드로이드 카카오/네이버 로그인 처리 (Feat. ViewModel)

송규빈·2024년 7월 2일
1

네이버 로그인

네이버 로그인을 구현하려면 NaverIdLoginSDK.authenticate(context, oauthLoginCallback)메서드를 사용하면 된다.
자세한 내용은 네이버 로그인 튜토리얼을 보면 된다.

구현 중 문제

비즈니스 로직 분리

소셜 로그인을 구현하기에 앞서 내 프로젝트는 MVVM 방식을 도입한 상태이고, 로그인 구현 로직도 비즈니스 로직이기에 ViewModel에서 처리를 해야한다고 생각했다.

프로젝트에서 지원하는 로그인의 형태는 카카오 로그인, 네이버 로그인, 이메일 로그인 이렇게 총 세가지이다.

카카오 로그인 같은 경우에 싱글톤으로 구성되어있는 카카오 Clicent를 통해 로그인이 수행되고 수행하는 메서드에는 인자로 context를 넣어야 하기에 ApplicationViewModel을 사용하였다.

@HiltViewModel
class LoginViewModel
@Inject constructor(
    application: Application,
    private val userPreferencesRepository: UserPreferencesRepository,
) : AndroidViewModel(application) {
	private val context = application.applicationContext

	private val _kakaoLoginUiState: MutableStateFlow<UiState> = MutableStateFlow(UiState.Loading)
    val kakaoLoginUiState: StateFlow<UiState> = _kakaoLoginUiState.asStateFlow()
    
   /**
     * 카카오 로그인
     */
    fun kakaoLogin() {
        viewModelScope.launch {
            _kakaoLoginUiState.value = handleKakaoLogin()
        }
    }
    
    /**
     * 카카오 로그인 처리
     *
     * @return [UiState]
     */
    private suspend fun handleKakaoLogin(): UiState =
        suspendCoroutine { continuation ->
            // 카카오계정으로 로그인 공통 callback 구성
            // 카카오톡으로 로그인 할 수 없어 카카오계정으로 로그인할 경우 사용됨
            val callback: (OAuthToken?, Throwable?) -> Unit = { token, error ->
                if (error != null) {
                    Log.e("PuumIn", "카카오계정으로 로그인 실패", error)
                    continuation.resume(UiState.Error)
                } else if (token != null) {
                    Log.d("TAG", "카카오계정으로 로그인 성공 ${token.accessToken}")
                    continuation.resume(UiState.Success)
                }
            }

            // 카카오톡이 설치되어 있으면 카카오톡으로 로그인, 아니면 카카오계정으로 로그인
            if (instance.isKakaoTalkLoginAvailable(context)) {
                instance.loginWithKakaoTalk(context) { token, error ->
                    if (error != null) {
                        // 사용자가 카카오톡 설치 후 디바이스 권한 요청 화면에서 로그인을 취소한 경우,
                        // 의도적인 로그인 취소로 보고 카카오계정으로 로그인 시도 없이 로그인 취소로 처리 (예: 뒤로 가기)
                        if (error is ClientError && error.reason == ClientErrorCause.Cancelled) {
                            return@loginWithKakaoTalk
                        }

                        Log.e("TAG", "카카오톡으로 로그인 실패", error)

                        // 카카오톡에 연결된 카카오계정이 없는 경우, 카카오계정으로 로그인 시도
                        instance.loginWithKakaoAccount(context, callback = callback)
                    } else if (token != null) {
                        Log.i("TAG", "카카오톡으로 로그인 성공 ${token.accessToken}")
                    }
                }
            } else {
                instance.loginWithKakaoAccount(context, callback = callback)
            }
        }
        
         /**
     * 네이버 로그인 처리
     *
     * @return
     */
    suspend fun handleNaverLogin(): UiState =
        suspendCoroutine { continuation ->
            val oauthLoginCallback: OAuthLoginCallback = object : OAuthLoginCallback {
                override fun onSuccess() {
                    continuation.resume(UiState.Success)
                }

                override fun onFailure(httpStatus: Int, message: String) {
                    val errorCode = getLastErrorCode().code
                    val errorDescription = getLastErrorDescription()
                    Log.e("Naver Login failed", "$errorCode: $errorDescription")
                    continuation.resume(UiState.Error)
                }

                override fun onError(errorCode: Int, message: String) {
                    onFailure(errorCode, message)
                    continuation.resume(UiState.Error)
                }
            }

            NaverIdLoginSDK.authenticate(context, oauthLoginCallback)
        }
}

에러

테스트를 해본 결과 Activity context 외부에서 startActivity()를 호출하려면 FLAG_ACTIVITY_NEW_TASK 플래그가 필요하다는 에러가 나왔다.

SDK 내부 코드

카카오

네이버


네이버와 카카오 모두 context를 사용하여 startActivity()를 호출하고 있었다.

원인

원인을 알아보면 Activity나 Fragment가 아닌 context에서 startActivity를 사용하기 위해서는 intent flag가 FLAG_ACTIVITY_NEW_TASK로 적용이 되어야 한다는 것이다.
참고

지금 내 코드를 보면 ApplicationContext를 인자로 넘기고 startActivity()를 호출하려고 해서 생긴 문제이다.

정책 이유

안드로이드가 저렇게 정책을 낸 이유가 있을 것인데 내 생각은 이렇다.
액티비티는 태스크에 속해있고, 태스크 내에서 Activity들이 쌓이는 스택 구조로 관리된다.

그렇기에 Activity Context가 아닌 Application Context를 사용하여 startActivity를 호출하게 되면 안드로이드 시스템은 어떤 태스크에 새로운 Activity를 배치해야 하는지 알 수 없다.

이로 인해 잠재적인 문제점들이 생기게 된다.
- 태스크 관리 오류
안드로이드 시스템은 Application 컨텍스트를 통해 새로운 Activity를 기존의 어느 태스크에 추가해야 할지 모른다. 이는 시스템이 올바르게 태스크와 백 스택을 관리하는 것을 방해할 수 있다.
- 앱 크래시
태스크와 백 스택 관리가 제대로 이루어지지 않으면 ActivityNotFoundException 또는 IllegalStateException과 같은 예외가 발생할 수 있다. 특히, Application 컨텍스트가 Activity 컨텍스트처럼 동작할 수 없기 때문에 이러한 예외가 발생한다.
- 비일관된 사용자 경험
새로운 Activity가 예기치 않은 태스크에 추가될 수 있다. 이는 사용자가 ‘뒤로’ 버튼을 눌렀을 때 예상치 못한 동작을 유발할 수 있고, 사용자 경험을 저하시킬 수 있다.

그렇기에 안드로이드 측에서 아예 정책으로 해놓고 예외를 발생시키게끔 한 것이다.

해결

그렇다면 어떻게 해결해야할까?
방법은 다양할 것이다.
나는 뷰모델에서는 카카오 로그인, 네이버 로그인 요청에 대한 State만을 처리하고 로그인 처리 로직은 별도의 SocialLoginManager라는 클래스를 만들어 소셜 로그인 구현에 대한 책임을 따로 주기로 하였다.


이렇게 처리함으로써 소셜 로그인 구현에 대한 책임은 Manager 클래스가 갖고, 뷰로직에서는 구현부는 모르는 상태로 원하는 소셜 로그인에 대한 메서드만 가져다가 사용할 수 있다.

profile
🚀 상상을 좋아하는 개발자

0개의 댓글