[Android] 네이버 로그인 프로필 가져오기
프로젝트 관련 글
저번 포스팅에서는 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에 전달합니다.