카카오 API를 이용하여 로그인 구현하기

김태하·2023년 8월 7일
2
post-thumbnail

📌계기

이번에 대학교에서 진행하는 공학경진대회에서 앱을 개발하던 도중 간편하게 로그인을 할 수 있는 방법과 현재 많은 웹에서 사용하는 방법인 API를 이용한 로그인을 구현하게 되어 후에 또 사용하게 될 경우를 대비하여 이렇게 작성하게 되었다.

📌안드로이드 초기 설정

setting.gradle

Android SDK를 적용할 프로젝트의 settings.gradle(Project) 파일에 다음과 같이 Android SDK 레파지토리를 설정한다.

dependencyResolutionManagement {    
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)    
        repositories {        
            google()        
            mavenCentral()        
            maven { url 'https://devrepo.kakao.com/nexus/content/groups/public/' }    
    }
}

모듈 설정 - build.gradle(Module)

build.gradle(Module) 파일에 필요한 모듈을 설정한다. 전체 모듈을 설치하거나 필요한 모듈만 선택해 설치하면 된다.

dependencies {
  implementation "com.kakao.sdk:v2-all:2.15.0" // 전체 모듈 설치, 2.11.0 버전부터 지원
  implementation "com.kakao.sdk:v2-user:2.15.0" // 카카오 로그인
  implementation "com.kakao.sdk:v2-talk:2.15.0" // 친구, 메시지(카카오톡)
  implementation "com.kakao.sdk:v2-story:2.15.0" // 카카오스토리
  implementation "com.kakao.sdk:v2-share:2.15.0" // 메시지(카카오톡 공유)
  implementation "com.kakao.sdk:v2-navi:2.15.0" // 카카오내비
  implementation "com.kakao.sdk:v2-friend:2.15.0" // 카카오톡 소셜 피커, 리소스 번들 파일 포함
}

인터넷 사용 권한 설정 - AndroidManifest.xml

카카오 API를 통해 카카오 서버와 통신하기 위해 앱에 인터넷 사용 권한을 설정해야 한다. 다음과 같이 인터넷 사용 권한 설정을 추가해주면 된다.

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.sample">
 
    <!-- 인터넷 사용 권한 설정-->
    <uses-permission android:name="android.permission.INTERNET" />

    <application
    android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
    ...

Java 8 사용 설정 - build.gradle(Module)

Java 8 사용을 위한 설정이 필요하다. 다음 예제를 참고하여 build.gradle(Module) 파일에 다음과 같이 자바 버전을 설정한다.

// Java 8 사용을 위한 build.gradle 설정
android {

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    kotlinOptions {
        jvmTarget = "1.8"
    }
}

초기화

Android SDK를 사용하기 위해서는 이티브 앱 키로 초기화 해야 한다. 그렇기 때문에 Application()을 상속한 클래스를 하나 생성해준다.
나는 MyApplication이라는 이름으로 생성하였다.

import android.app.Application
import android.util.Log

import com.kakao.sdk.common.KakaoSdk
import com.kakao.sdk.common.util.Utility
import com.kakao.sdk.user.Constants.TAG

class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        // 다른 초기화 코드들

        // Kakao SDK 초기화
        KakaoSdk.init(this, "{NATIVE_APP_KEY}")

    }
}

NATIVE_APP_KEY는 이후 설명할 앱을 추가하는 과정인 키 해시 등록 파트를 보면 네이티브 앱 키를 확인 할 수 있다.

이후 Manifest.xml에서의 application에도 Kakao SDK 초기화를 수행한 클래스의 이름을 설정해야 한다.

<application
    <!-- android:name 설정 -->
    android:name=".MyApplication"
    ...
>

키 해시 등록

키 해시 등록 이전에 중요한 작업이 있다.

1. kakao developers 접속

https://developers.kakao.com/

2. 내 에플리케이션에 들어가 로그인하면 애플리케이션 추가 버튼이 나온다.

3. 추가하기 버튼을 눌러 앱을 생성 후 들어가보면 다음과 같은 화면이 나온다.

나중에 네이티브 앱 키를 사용할 것이다.

이후 플랫폼에 들어가 Android 플랫폼을 등록한다.

패키지명은 반드시 Manifest.xml에 있는 package명과 동일하게 작성한다.

나는 Manifest.xml 파일을 확인했을 때 package명이 적혀있지 않아 직접 프로젝트를 생성했을 때의 package명과 동일하게 작성해주었다.
ex) com.example.sample

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.sample">

이제 제일 중요한 키 해시 등록이다.

공식 문서를 보면 확인하는 방법은 여러가지이지만 나는 제대로 동작하지 않아 코드를 통해 키 해시를 얻었다.

나는 초기화를 수행하였던 MyApplication에 키 해시를 얻는 코드를 추가하였다.

import android.app.Application
import android.util.Log

import com.kakao.sdk.common.KakaoSdk
import com.kakao.sdk.common.util.Utility
import com.kakao.sdk.user.Constants.TAG

class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        // 다른 초기화 코드들

        // Kakao SDK 초기화
        KakaoSdk.init(this, "{NATIVE_APP_KEY}")

		// 키 해시를 얻는 코드
        var keyHash = Utility.getKeyHash(this)
        Log.e(TAG, "해시 키 값 : ${keyHash}")
    }
}

애뮬레이터를 실행 후 logcat에서 해시를 검색한 결과 다음과 같이 나온다.

이 키 해시를 등록해준다.

이 과정이 끝나면 설정은 끝났다.

본격적으로 로그인 기능을 구현해보자.

📌 카카오 로그인하기

Redirect URI 설정 - manifest.xml

카카오 로그인 기능을 구현하기 위해서는 리다이렉션(Redirection)을 통해 인가 코드를 받아야 한다. 이를 위해 AndroidManifest.xml에 액티비티(Activity) 설정이 필요하다.

기존에 존재하는 .MainActivity의 <activity> 코드 밑에 추가하면 된다.

<activity 
    android:name="com.kakao.sdk.auth.AuthCodeHandlerActivity"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        
        <!-- Redirect URI: "kakao${NATIVE_APP_KEY}://oauth" -->
        <data android:host="oauth"
                android:scheme="kakao${NATIVE_APP_KEY}" />
    </intent-filter>
</activity>

중요한 부분은 android:scheme="kakao${NATIVE_APP_KEY}" NATIVE_APP_KEY 부분에 우리가 생성한 앱의 네이티브 앱 키를 입력해주면 된다.

기억해야할 점은 네이티브 앱 키 앞에 kakao를 무조건 붙어야한다.

코드 작성 전 모듈 설정 - build.gradle

implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.1"
implementation 'androidx.activity:activity-ktx:1.7.2'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1'

혹시 코드 일부가 import 되지 않는다면 관련된 모듈을 찾아 작성하거나 댓글을 남긴다면 확인해보고 알려드리겠습니다.

코드 작성

로그인과 로그아웃, 로그인 상태를 확인 할 코드 예제이다.
사용자의 이름, 이메일을 가져오는 코드도 추가하였다.

1. layout에서 activity_main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center_horizontal"
    android:padding="16dp"
    tools:context=".MainActivity">

    <Space
        android:layout_width="match_parent"
        android:layout_height="10dp" />

    <Button
        android:id="@+id/btn_kakao_login"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="카카오 로그인하기" />

    <Button
        android:id="@+id/btn_kakao_logout"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="카카오 로그아웃하기" />

    <TextView
        android:id="@+id/tv_login_status"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="20sp" />

    <Button
        android:id="@+id/buttonInfo"
        android:layout_width="205dp"
        android:layout_height="wrap_content"
        android:text="Button" />
</LinearLayout>

2. MainActivity

import android.annotation.SuppressLint
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.widget.Button
import android.widget.TextView
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.asLiveData
import com.example.kakaotest2.KakaoOauthViewModel.Companion.TAG
import com.kakao.sdk.user.UserApiClient


class MainActivity : AppCompatActivity() {

    private lateinit var kakaoOauthViewModel: KakaoOauthViewModel

    @SuppressLint("MissingInflatedId")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // ViewModelProvider를 통해 ViewModel 인스턴스 생성
        kakaoOauthViewModel = ViewModelProvider(this, KakaoOauthViewModelFactory(application)).get(KakaoOauthViewModel::class.java)

        val btnKakaoLogin = findViewById<Button>(R.id.btn_kakao_login)
        val btnKakaoLogout = findViewById<Button>(R.id.btn_kakao_logout)
        val tvLoginStatus = findViewById<TextView>(R.id.tv_login_status)

        btnKakaoLogin.setOnClickListener {
            kakaoOauthViewModel.kakaoLogin()
        }

        btnKakaoLogout.setOnClickListener {
            kakaoOauthViewModel.kakaoLogout()
        }

        kakaoOauthViewModel.isLoggedIn.asLiveData().observe(this) { isLoggedIn ->
            val loginStatusInfoTitle = if (isLoggedIn) "로그인 상태" else "로그아웃 상태"
            tvLoginStatus.text = loginStatusInfoTitle
        }

		// 사용자 정보 가져오기
        val test = findViewById<Button>(R.id.buttonInfo)
        test.setOnClickListener {
            getInfo()
        }


    }

    fun getInfo(){
        // 사용자 정보 요청 (기본)
        UserApiClient.instance.me { user, error ->
            if (error != null) {
                Log.e(TAG, "사용자 정보 요청 실패", error)
            }
            else if (user != null) {
                Log.i(TAG, "사용자 정보 요청 성공" +
                        "\n회원번호: ${user.id}" +
                        "\n이메일: ${user.kakaoAccount?.email}" +
                        "\n닉네임: ${user.kakaoAccount?.profile?.nickname}" )

            }
        }
    }

3. KakaoOauthViewModel

import android.app.Application
import android.util.Log
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import com.kakao.sdk.auth.model.OAuthToken
import com.kakao.sdk.common.model.ClientError
import com.kakao.sdk.common.model.ClientErrorCause
import com.kakao.sdk.user.UserApiClient
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine

class KakaoOauthViewModel(application : Application) : ViewModel() {

    // 비어있는 생성자 추가
    constructor() : this(Application())

    companion object {
        const val TAG = "KakaoOauthViewModel"
    }

    private val context = application.applicationContext

    // 로그인 여부 초기값 false, 바인딩
    val isLoggedIn = MutableStateFlow<Boolean>(false)

    // 코루틴
    fun kakaoLogin(){
        viewModelScope.launch {
            // handleKakaoLogin은 true false 로그인 성공 여부 리턴
            // emit으로 로그인 여부를 이벤트로 보냄
            isLoggedIn.emit(handleKakaoLogin())

        }
    }

    // 코루틴
    fun kakaoLogout(){
        viewModelScope.launch {
            // handleKakaoLogin은 true false 로그인 성공 여부 리턴
            // emit으로 로그인 여부를 이벤트로 보냄
            if(handleKakaoLogout()){
                isLoggedIn.emit(false)
            }


        }
    }

    private suspend fun handleKakaoLogout() : Boolean =
        suspendCoroutine { continuation ->
            // 로그아웃
            UserApiClient.instance.logout { error ->
                if (error != null) {
                    Log.e(TAG, "로그아웃 실패. SDK에서 토큰 삭제됨", error)
                    continuation.resume(false)
                }
                else {
                    Log.i(TAG, "로그아웃 성공. SDK에서 토큰 삭제됨")
                    continuation.resume(true)
                }
            }
        }

    private suspend fun handleKakaoLogin() : Boolean =
        suspendCoroutine<Boolean> { continuation ->
            // 로그인 조합 예제

            // 카카오계정으로 로그인 공통 callback 구성
            // 카카오톡으로 로그인 할 수 없어 카카오계정으로 로그인할 경우 사용됨
            val callback: (OAuthToken?, Throwable?) -> Unit = { token, error ->
                if (error != null) {
                    Log.e(TAG, "카카오계정으로 로그인 실패", error)
                    continuation.resume(false)
                } else if (token != null) {
                    Log.i(TAG, "카카오계정으로 로그인 성공 ${token.accessToken}")

                    continuation.resume(true)

                }
            }

            // 카카오톡이 설치되어 있으면 카카오톡으로 로그인, 아니면 카카오계정으로 로그인
            if (UserApiClient.instance.isKakaoTalkLoginAvailable(context)) {
                UserApiClient.instance.loginWithKakaoTalk(context) { token, error ->
                    if (error != null) {
                        Log.e(TAG, "카카오톡으로 로그인 실패", error)

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

                        // 카카오톡에 연결된 카카오계정이 없는 경우, 카카오계정으로 로그인 시도
                        UserApiClient.instance.loginWithKakaoAccount(context, callback = callback)
                    } else if (token != null) {
                        Log.i(TAG, "카카오톡으로 로그인 성공 ${token.accessToken}")
                    }
                }
            } else {
                UserApiClient.instance.loginWithKakaoAccount(context, callback = callback)
            }
        }


}

4. KakaoOauthViewModelFactory

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import android.app.Application

class KakaoOauthViewModelFactory(private val application: Application) : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(KakaoOauthViewModel::class.java)) {
            return KakaoOauthViewModel(application) as T
        }
        throw IllegalArgumentException("Unknown ViewModel class")
    }
}

📌 작동 화면

  • 로그인 하기 클릭하였을 때

  • 로그인 후

밑의 버튼을 누르면 사용자 정보를 가져오고 logcat에서 확인 가능하다.

이 밖에도 앱과 spring boot와 통신하는 기능도 추가하였는데 추후에 작성할 예정이다.

츨처 : Kakao Developers

2개의 댓글

comment-user-thumbnail
2023년 8월 7일

즐겁게 읽었습니다. 유용한 정보 감사합니다.

1개의 답글