Skils/Android

[Android] 네이버 로그인 프로필 가져오기

재한 2024. 5. 1. 23:04

프로젝트 관련 글

[Android] 네이버 간편 로그인(Kotlin)

[Android] 자동 로그인 with DataStore(Kotlin)

 

저번 포스팅에서는 DataStore를 이용하면서 자동로그인을 구현해 봤습니다.

과정에서 매번 DataStore를 사용하는 곳에서 DataStore를 주입해줘야 하는 상황이 생겼습니다.

그 외에도 추후 개발에서 API와 Repository가 많이 생기기 때문에 개발 초반부에 Hilt를 적용하는 것이 생산성에 도움이 될 것이라고 판단하고 적용해 봤습니다.

 

기존의 코드는 Application에서 선언한 DataStore를 사용했지만, Hilt를 통해서 주입해 줬습니다.

@InstallIn(SingletonComponent::class)
@Module
object DataStoreModule {

    @Singleton
    @Provides
    fun providesDataStore(@ApplicationContext appContext: Context): DataStore<Preferences> {
        return PreferenceDataStoreFactory.create {
            appContext.preferencesDataStoreFile(BuildConfig.DATASTORE_NAME)
        }
    }
}

 

다음은 로그인을 한 후 유저의 정보를 저장하는 API를 구현해 봤습니다.

제가 작성한 서버 API의 명세서는 다음과 같습니다.

 

이를 위해서 서버에 요청할 Request Data를 구현합니다.

data class UserRequest(
    val userName: String = "",
    val userContact: String = "",
    val accessToken: String = "",
    val refreshToken: String = "",
)

id값은 디비에서 Auto-Increment로 설정했기 때문에 필요 없습니다.

 

반드시 서버의 UserDto와 변수명이 일치해야 합니다..(아니라면 실패합니다)

실제로 데이터와 요청도 정상적이었는데, 오타 때문에 실패해서 한참 헤맸습니다..

 

다음은 Android에서 레트로핏으로 호출할 SignUpApi를 작성합니다.

작성하기 전 Retrofit을 주입하기 위해 NetworkModule을 구현해야 합니다.

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
    @Singleton
    @Provides
    fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
        val apiKey = BASE_URL
        return Retrofit.Builder()
            .addConverterFactory(GsonConverterFactory.create(GsonBuilder().setLenient().create()))
            .baseUrl(apiKey)
            .client(okHttpClient)
            .build()
    }

    @Singleton
    @Provides
    fun provideOkHttpClient() = OkHttpClient.Builder().run {
        addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
        connectTimeout(120, TimeUnit.SECONDS)
        readTimeout(120, TimeUnit.SECONDS)
        writeTimeout(120, TimeUnit.SECONDS)
        build()
    }
}

 

 

 

SignUpApi입니다.

@POST("signUp")
suspend fun signUp(
    @Body userRequest: UserRequest,
): Response<Boolean>

서버에서는 성공했다면 True, 실패했다면 False를 반환하기 때문에 다음과 같이 작성했습니다.

 

API Module

@Module
@InstallIn(SingletonComponent::class)
object ApiModule {
    @Provides
    @Singleton
    fun provideUserApi(
        retrofit: Retrofit,
    ): UserApi = retrofit.create()
}

레트로핏의 확장함수인 create를 통해서 타입만 명시해 준다면 UserApi::class.java 코드를 작성해주시지 않아도 됩니다.

 

다음은 Data Layer의 설계입니다.

레포지토리 패턴을 이용해 Respository가 api를 호출하고 그에 대한 데이터를 viewModel에 전달하는 구조로 개발을 진행했습니다.

 

UserRepository

interface UserRepository {

    fun signUp(userRequest: UserRequest): Flow<ApiResponse<Boolean>>
}

 

UserRepositoryImpl

class UserRepositoryImpl @Inject constructor(private val api: UserApi) : UserRepository {
    override fun signUp(userRequest: UserRequest): Flow<ApiResponse<Boolean>> = flow {
        val response = api.signUp(userRequest)
        runCatching {
            response
        }.onSuccess {
            if (it.isSuccessful) {
                emit(ApiResponse.Success(data = true))
            } else {
                emit(ApiResponse.Failure)
            }
        }.onFailure {
            emit(
                ApiResponse.Error(
                    errorCode = response.code(),
                    errorMessage = it.message ?: "",
                ),
            )
        }
    }
}

api의 반환타입은 response입니다.

try-catch를 통해 예외 처리를 하는 것도 방법이지만, 저는 runCatching을 통해서 예외 처리를 했습니다.

public inline fun <T, R> T.runCatching(block: T.() -> R): Result<R> {
    return try {
        Result.success(block())
    } catch (e: Throwable) {
        Result.failure(e)
    }
}

runCatching도 내부에서 try - catch를 진행하고, onSuccess와 onFailure이라는 조금 더 직관적인 코드를 제시해 주기 때문에 더 선호합니다.

 

viewModel에서 API를 호출하기 전 Request Data의 값들을 가져와야 합니다.

그전에 저번에 작성했던 NaverLoginManager에서 로그인을 성공했다면 유저의 정보를 가져오는 코드를 구현해야 합니다.

private val profileCallback = object : NidProfileCallback<NidProfileResponse> {
    override fun onSuccess(response: NidProfileResponse) {
        _loginResult.value = ApiResponse.Success(
            LoginResponse(
                userName = response.profile?.name ?: "",
                userContact = response.profile?.mobile ?: "",
                accessToken = NaverIdLoginSDK.getAccessToken() ?: "",
                refreshToken = NaverIdLoginSDK.getRefreshToken() ?: "",
            ),
        )
    }

    override fun onFailure(httpStatus: Int, message: String) {
        _loginResult.value = ApiResponse.Error(
            errorCode = ErrorCode.FAILED_LOGIN,
            errorMessage = message,
        )
    }

    override fun onError(errorCode: Int, message: String) {
        onFailure(errorCode, message)
    }
}

profileCallback은 네이버 로그인에서 제공해 주는 콜백으로, 로그인을 성공했다면 유저의 정보를 가져올 수 있습니다.

당연하게도 해당  콜백은 로그인에 성공했다면 호출해 줍니다.

private val oauthLoginCallback = object : OAuthLoginCallback {
    override fun onSuccess() {
        NidOAuthLogin().callProfileApi(profileCallback)
    }

    override fun onFailure(httpStatus: Int, message: String) {
        _loginResult.value = ApiResponse.Error(
            errorCode = ErrorCode.FAILED_LOGIN,
            errorMessage = message,
        )
    }

    override fun onError(errorCode: Int, message: String) {
        onFailure(errorCode, message)
    }
}

 

LoginViewModel

이제 viewModel에서 signUp을 호출해 줍니다.

private fun observerLoginResponse() {
        viewModelScope.launch {
            NaverLoginManager.loginResult.collectLatest { loginResponse ->
                when (loginResponse) {
                    is ApiResponse.Success -> {
                        userSignUp(loginResponse.data)
                    }

                    is ApiResponse.Error -> {
                        _event.emit(
                            LoginEvent.Error(
                                errorCode = loginResponse.errorCode,
                                errorMessage = loginResponse.errorMessage,
                            ),
                        )
                    }

                    is ApiResponse.Failure -> {}
                }
            }
        }
}

네이버 로그인의 성공했다면 내부의 signUp 함수를 호출합니다.

private suspend fun userSignUp(loginResponse: LoginResponse) {
    userRepository.signUp(
        UserRequest(
            userName = loginResponse.userName ?: "",
            userContact = loginResponse.userContact ?: "",
            accessToken = loginResponse.accessToken ?: "",
            refreshToken = loginResponse.refreshToken ?: "",
        ),
    ).collectLatest { signUpResponse ->
        when (signUpResponse) {
            is ApiResponse.Success -> _event.emit(LoginEvent.Success)
            is ApiResponse.Failure -> {}
            is ApiResponse.Error -> _event.emit(
                LoginEvent.Error(
                    errorCode = signUpResponse.errorCode,
                    errorMessage = signUpResponse.errorMessage,
                ),
            )
        }
    }
}

디비에 정보를 성공적으로 저장했다면 LoginEvent를 Success로, 실패했다면 Error를 View에 전달합니다.