[Android] 네이버 간편 로그인(Kotlin)
이번 프로젝트에서 네이버 지도 API를 사용하기 때문에 로그인도 네이버 간편 로그인을 사용하기로 했습니다.
저는 설정했던 과정과 작성했던 코드와 그에 대한 설명을 적어볼까 합니다.
앱을 등록하는 글은 다른 블로그에도 많더라고요.
Gradle 설정
저는 arr 파일을 다운로드하고 gradle에 추가하는 방법을 선택했습니다.
다운로드한 파일을 project 탭으로 전환시켜서 lib 폴더에 복사해 줍니다.
이후 해당 arr 파일을 dependency에 추가해 줍니다.
dependencies{
implementation(files("libs/oauth-5.9.1.aar"))
}
이후에는 사용하는 라이브러리를 추가해주셔야 합니다.
저 같은 경우 안 쓸 거 같은 라이브러리를 제외하다가, gradle 충돌이 많이 발생해서 제공했던 라이브러리를 겹치는 라이브러리 빼고
모두 추가해 줬습니다.
implementation("com.airbnb.android:lottie:3.1.0")
implementation("org.jetbrains.kotlin:kotlin-stdlib:1.6.21")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9")
implementation("androidx.appcompat:appcompat:1.3.1")
implementation("androidx.legacy:legacy-support-core-utils:1.0.0")
implementation("androidx.browser:browser:1.4.0")
implementation("androidx.constraintlayout:constraintlayout:1.1.3")
implementation("androidx.security:security-crypto:1.1.0-alpha06")
implementation("androidx.core:core-ktx:1.3.0")
implementation("androidx.fragment:fragment-ktx:1.3.6")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0")
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation("com.squareup.moshi:moshi-kotlin:1.11.0")
implementation("com.squareup.okhttp3:logging-interceptor:4.2.1")
이렇게 하면 gradle 설정은 모두 끝났습니다.
다음은 UI 상에서 버튼을 Naver Login에 맞게 설정해 줍니다.
<com.navercorp.nid.oauth.view.NidOAuthLoginButton
android:id="@+id/btn_naver_login"
android:layout_width="wrap_content"
android:layout_height="50dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
이렇게 하면 사진을 넣을 필요 없이 네이버에서 권장하는 버튼의 모양으로 사용가능합니다.
NaverIdLoginSdk 초기화
저는 애플리케이션에서 초기화해 줬습니다.
class MimoApplication : Application() {
override fun onCreate() {
super.onCreate()
Timber.plant(Timber.DebugTree())
NaverIdLoginSDK.initialize(
this,
BuildConfig.CLIENT_ID,
BuildConfig.CLIENT_SECRET,
"사용자",
)
}
}
네이버 로그인 sdk를 초기화하는데 client_id, client_secret, 사용자 이름이 필요합니다.
해당 정보는 네이버 앱을 등록하는 과정에서 정보를 얻을 수 있습니다.
저는 해당 정보가 git에 올라가는 것을 방지하기 위해 local.properites에 저장했습니다.
NAVER_CLIENT_ID="CleintID~~~~"
NAVER_CLIENT_SECRET="ClientSecret~~~"
이렇게 로컬 프로퍼티에 저장한 정보를 사용하기 위해선 app 모듈의 gradle에서 추가적인 작업이 필요합니다.
그러기 위해서 다음과 같은 작업이 필요합니다.
buildConfig 설정
buildFeatures {
buildConfig = true
}
val properties = Properties()
properties.load(project.rootProject.file("local.properties").inputStream())
val clientId = properties["NAVER_CLIENT_ID"] ?: ""
val clientSecret = properties["NAVER_CLIENT_SECRET"] ?: ""
위 코드는 앱 모듈 gradle에서 안드로이드 아래에 작성해 줍니다.
로컬 프로퍼티의 파일을 들고 와서 선언한 변수에 매핑시켜 주는 작업입니다.
이렇게 하면 중요한 정보를 숨기면서 사용할 수 있습니다.
매핑한 정보를 buildConfig에 넣어줍니다.
defaultConfig {
applicationId = "com.mimo.android"
minSdk = 26
targetSdk = 34
versionCode = 1
versionName = "1.0"
buildConfigField("String", "CLIENT_ID", "$clientId")
buildConfigField("String", "CLIENT_SECRET", "$clientSecret")
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
이후 그래 들을 동기화하면 buildConfig 파일에 다음과 같은 정보가 추가됩니다.
로그인
네이버 로그인을 실행하기 위해서 다음 코드를 실행시켜줘야 합니다.
NaverIdLoginSDK.authenticate()
해당 파라미터로는 context와 launcer or callback을 사용하는데 저는 callback을 사용했습니다.
콜백은 3가지의 함수를 오버라이딩 해줘야 합니다.
- 성공했을 경우
- 실패했을 경우
- 에러가 발생한 경우
각자가 개발한 시나리오에 맞게 성공 처리 혹은 에러 처리를 해주시면 됩니다.
처음 네이버 로그인을 구현하기 위한 콜백과 그에 대한 이벤트 처리를 Activity와 ViewModel에서 작성하고 보니
두 클래스의 볼륨이 커진다는 느낌이 들었습니다.
그래서 제가 설계한 구조는 다음과 같습니다.
Activity와 viewModel 사이에 LoginManager를 통해서 ViewModel은 로그인에 대한 결과를 반환받는 구조입니다.
아직 hilt가 적용 전이라 구조에 대해서 많기 고민해 봤는데, 우선적으로 Object로 구현하기로 했습니다.
(생각해 보니 클래스로 구현하는 것이 메모리상으로 훨씬 좋을 것 같습니다)
Activity에서 LoginManager login 함수 호출
with(binding) {
btnNaverLogin.setOnClickListener {
NaverLoginManager.login(this@LoginActivity)
}
}
네이버 로그인을 진행하기 위해서 context를 파라미터로 받기 때문에 context를 파라미터로 전달했습니다.
LoginManger를 설명하기 전 API 통신에 대한 응답을 효과적으로 처리하기 위해 ApiResponse를 커스텀해서 구현해 봤습니다.
ApiResponse
sealed class ApiResponse<out T : Any?> {
data class Success<out T : Any?>(
val data: T,
) : ApiResponse<T>()
data class Error(
val errorCode: Int = 0,
val errorMessage: String = "",
) : ApiResponse<Nothing>()
data object Failure : ApiResponse<Nothing>()
}
API 통신은 크게 성공(200), 실패, 에러로 나눴고, 그에 따른 객체를 제네릭 타입으로 구현해 봤습니다.
이제 클라이언트에서 필요한 정보를 ApiResponse로 감싸면, 일관적인 API 통신 처리를 할 수 있을 것 같아서 다음과 같이 작성했습니다.
로그인에 대한 응답으론 저는 accessToken과 refreshToken을 이용하기 때문에 다음과 같이 LoginResponse를 구현해 봤습니다.
data class LoginResponse(
val accessToken: String = "",
val refreshToken: String = "",
)
NaverLoginManager
object NaverLoginManager {
private val _loginResult = MutableStateFlow<ApiResponse<LoginResponse>>(ApiResponse.Failure)
val loginResult: StateFlow<ApiResponse<LoginResponse>> = _loginResult
private val oauthLoginCallback = object : OAuthLoginCallback {
override fun onSuccess() {
_loginResult.value = ApiResponse.Success(
LoginResponse(
accessToken = NaverIdLoginSDK.getAccessToken() ?: "",
refreshToken = NaverIdLoginSDK.getRefreshToken() ?: "",
),
)
}
override fun onFailure(httpStatus: Int, message: String) {
_loginResult.value = ApiResponse.Error(
errorCode = httpStatus,
errorMessage = message,
)
}
override fun onError(errorCode: Int, message: String) {
onFailure(errorCode, message)
}
}
fun login(context: Context) {
NaverIdLoginSDK.authenticate(context, oauthLoginCallback)
}
}
loginResult는 viewModel에서 collect 하기 위해서 LoginResponse를 ApiResponse가 감싸는 형태인 stateFlow로 설정했습니다.
stateFlow는 초기값 설정이 필요하기 때문에 Failure로 설정했습니다.
이제 Activity에서 로그인이 호출되고, 익숙한 네이버 로그인 화면이 나오고 로그인에 대한 결과 처리를 callback에서 해야 합니다.
성공할 경우는 액세스토큰과 리프래시 토큰을 담고 있는 LoginResponse를 감싼 Success로 값을 설정하고,
실패할 경우 errorCode와 message를 보유한 Error로 설정했습니다.
이제 해당 loginResult를 viewModel에서 collect 하고, 그에 따른 viewEvent를 view에 전달해야 합니다.
LoginEvent
sealed interface LoginEvent {
data object Success : LoginEvent
data class Error(
val errorCode: Int = 0,
val errorMessage: String = "",
) : LoginEvent
}
LoginEvent는 성공과 실패가 있고, 실패할 경우 사용자에게 전달하기 위해 errorCode와 errorMessage를 가진 Error가 있습니다.
지금 와서 보니 viewEvent도 제네릭타입으로 설정해서 재사용할 수 있다면 훨씬 좋았을 것 같습니다..
LoginViewModel
private val _event = MutableSharedFlow<LoginEvent>()
val event: SharedFlow<LoginEvent> = _event
private fun observerLoginResponse() {
viewModelScope.launch {
NaverLoginManager.loginResult.collectLatest { loginResponse ->
when (loginResponse) {
is ApiResponse.Success -> {
_event.emit(LoginEvent.Success)
}
is ApiResponse.Error -> {
_event.emit(
LoginEvent.Error(
errorCode = loginResponse.errorCode,
errorMessage = loginResponse.errorMessage,
),
)
}
is ApiResponse.Failure -> {}
}
}
}
}
viewModel은 LoginManager의 loginResult 값을 collect 합니다.
만약 collect 될 경우 그에 따른 분기 처리를 해줘야 합니다.
실패할 경우 viewEvent로 Error를 emit, 성공할 경우는 Success를 emit 합니다.
LoginActiviy
Activity에서는 전달받은 event에 따라서 UI를 업데이트합니다.
private fun collectLoginEvent() {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
loginViewModel.event.collectLatest { loginEvent ->
when (loginEvent) {
is LoginEvent.Success -> {
val intent = Intent(this@LoginActivity, MainActivity::class.java)
startActivity(intent)
}
is LoginEvent.Error -> {
showMessage(loginEvent.errorMessage)
}
}
}
}
}
}
Flow는 생명주기를 관리해줘야 하는 점에서 repeatOnLifeCycle을 추가해 줬습니다.
이제 성공할 경우 MainActivity로 이동하고, 실패할 경우 toastMessage를 출력했습니다.
다음글은 DataStore를 이용한 자동로그인입니다.
최대한 각자의 책임에 맞게 분리를 해보려고 했는데 네이버 로그인을 처음 사용해 봐서 조금 어려웠던 것 같습니다.
대부분의 글이 Activity에서 처리하는 내용이 많아서 다른 방식으로 처리해 봤습니다.
참고
https://developers.naver.com/docs/login/android/android.md#android
https://ppeper.github.io/kotlin/kotlin-generic/