반응형

손쉽게 구글 계정의 정보를 받아올 수 있도록 안드로이드 스튜디오의 프로젝트와 구글 로그인을 연동해보도록 하겠습니다.

구글 플레이 공식문서를 참고하였습니다.

문서에는 자바 및 deprecated된 startActivityForResult을 사용하는 등 과거의 버전으로 설명이 되어있지만 

현재 시점으로 최신 버전인 Chipmunk 2021.2.1 Patch 1버전을 기준으로 작성하였습니다.

 

기본 요건

안드로이드용 Google 로그인에는 다음과 같은 요구사항들이 있습니다.

  • Android 4.4 이상을 실행하고 Android 4.2.2 이상을 기반으로 Google API 플랫폼을 실행하며 Google Play 서비스 버전 15.0.0 이상을 실행하는 AVD가 있는 에뮬레이터가 포함된 호환 Android기기
  • SDK도구 구성 요소를 포함한 Android SDK의 최신 버전 SDK는 Android 스튜디오의 Android SDK Manager에서 사용
  • Android 4.4(KitKat) 이상에서 컴파일하도록 구성된 프로젝트

Google Play 서비스 추가

Gradle Sciprts - settings.gradle 파일에 Google의 Maven 저장소가 포함되어 있는지 확인합니다. 

새 프로젝트를 만들면 기본적으로 세팅이 되어있습니다.

앱 수준 build.gradle에 아래와 같은 Google Play 서비스를 선언해줍니다.

implementation 'com.google.android.gms:play-services-auth:20.2.0'
dependencies {

    implementation 'com.google.android.gms:play-services-auth:20.2.0'
    // ...
}

Google API 콘솔 프로젝트 구성하기

이 링크로 접속 하셔서 '프로젝트 구성' 버튼을 눌러줍니다. 

요런 창이 나올텐데 " + Create a new project "를 눌러 새로운 프로젝트를 생성해줍니다.

프로젝트 이름은 임의로 지정해준 후 NEXT를 눌러줍니다. 

여기에는 사용자 동의여부 화면이 표시될 때 나타날 앱의 이름을 적어주시면 됩니다. 저는 테스트용으로 만들어서 그냥 위와 동일하게 지었습니다.

이제 OAuth 클라이언트를 구성해줍니다. 

총 3가지 항목을 입력해야하는데

첫 번째 항목은 어디에서 사용할 것인지를 선택하면 됩니다. Android와 연동할 것이기 때문에 Android로 지정해 주었습니다.

두 번째 항목은 앱의 패키지 이름을 넣어주면 됩니다. ex ) com.soopeach.googlesignupexample

마지막 세 번째 항목은 SHA-1 키를 입력해주어야 합니다. 

터미널 혹은 명령프롬포트를 열어 아래와 같은 명령어를 입력해줍니다.

  • 맥 OS / 리눅스
    • keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
  • 윈도우
    • "C:\Program Files\Android\Android Studio\jre\bin\keytools" -list -v -keystore "%USERPROFILE%\.android\debug.keystore" -alias androiddebugkey -storepass android -keypass android

빨간색 표시된 SHA1 코드를 세 번째 항목에 입력해주면 됩니다.

파란색 버튼을 눌러 json파일을 다운로드해줍니다.

credentials.json 파일이 다운로드 되는데

해당 파일을 이처럼 앱의 Project - app 안에 넣어주시면 됩니다.

전체 메인 엑티비티의 소스코드

package com.soopeach.googlesignupexample

import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
import com.google.android.gms.auth.api.signin.GoogleSignInClient
import com.google.android.gms.auth.api.signin.GoogleSignInOptions
import com.google.android.gms.common.api.ApiException
import com.google.android.gms.tasks.Task
import com.soopeach.googlesignupexample.databinding.ActivityMainBinding


class MainActivity : AppCompatActivity() {

    lateinit var mGoogleSignInClient: GoogleSignInClient
    lateinit var resultLauncher: ActivityResultLauncher<Intent>

    override fun onStart() {
        super.onStart()
        val account = GoogleSignIn.getLastSignedInAccount(this)
        account?.let {
            Toast.makeText(this, "Logged In", Toast.LENGTH_SHORT).show()
        } ?: Toast.makeText(this, "Not Yet", Toast.LENGTH_SHORT).show()
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        val binding = ActivityMainBinding.inflate(layoutInflater)
        super.onCreate(savedInstanceState)

        // ActivityResultLauncher
        setResultSignUp()

        val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
            .requestEmail()
            .requestProfile()
            .build()

        mGoogleSignInClient = GoogleSignIn.getClient(this, gso);

        with(binding) {
            btnSignIn.setOnClickListener {
                signIn()
            }

            btnSignOut.setOnClickListener {
                signOut()
            }
            btnGetProfile.setOnClickListener {
                GetCurrentUserProfile()
            }
        }

        setContentView(binding.root)
    }

    private fun setResultSignUp() {
        resultLauncher =
            registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
                // 정상적으로 결과가 받아와진다면 조건문 실행
                if (result.resultCode == Activity.RESULT_OK) {
                    val task: Task<GoogleSignInAccount> =
                        GoogleSignIn.getSignedInAccountFromIntent(result.data)
                    handleSignInResult(task)

                }
            }
    }

    private fun handleSignInResult(completedTask: Task<GoogleSignInAccount>) {
        try {
            val account = completedTask.getResult(ApiException::class.java)
            val email = account?.email.toString()
            val familyName = account?.familyName.toString()
            val givenName = account?.givenName.toString()
            val displayName = account?.displayName.toString()
            val photoUrl = account?.photoUrl.toString()

            Log.d("로그인한 유저의 이메일", email)
            Log.d("로그인한 유저의 성", familyName)
            Log.d("로그인한 유저의 이름", givenName)
            Log.d("로그인한 유저의 전체이름", displayName)
            Log.d("로그인한 유저의 프로필 사진의 주소", photoUrl)
        } catch (e: ApiException) {
            // The ApiException status code indicates the detailed failure reason.
            // Please refer to the GoogleSignInStatusCodes class reference for more information.
            Log.w("failed", "signInResult:failed code=" + e.statusCode)
        }
    }

    private fun signIn() {
        val signInIntent: Intent = mGoogleSignInClient.getSignInIntent()
        resultLauncher.launch(signInIntent)
    }

    private fun signOut() {
        mGoogleSignInClient.signOut()
            .addOnCompleteListener(this) {
                // ...
            }
    }

    private fun revokeAccess() {
        mGoogleSignInClient.revokeAccess()
            .addOnCompleteListener(this) {
                // ...
            }
    }

    private fun GetCurrentUserProfile() {
        val curUser = GoogleSignIn.getLastSignedInAccount(this)
        curUser?.let {
            val email = curUser.email.toString()
            val familyName = curUser.familyName.toString()
            val givenName = curUser.givenName.toString()
            val displayName = curUser.displayName.toString()
            val photoUrl = curUser.photoUrl.toString()

            Log.d("현재 로그인 되어있는 유저의 이메일", email)
            Log.d("현재 로그인 되어있는 유저의 성", familyName)
            Log.d("현재 로그인 되어있는 유저의 이름", givenName)
            Log.d("현재 로그인 되어있는 유저의 전체이름", displayName)
            Log.d("현재 로그인 되어있는 유저의 프로필 사진의 주소", photoUrl)
        }
    }
}

Google 로그인 및 GoogleSignInClient 객체 구성

메인 엑티비티의 onCreate() 내부에서 GoogleSignInOptions 객체를 만들어줍니다.

val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
            .requestEmail()
            .requestProfile()
            .build()

사용자의 이메일, 프로필(이름 및 프로필 사진의 링크)를 요청하기 위하여 requestEmail(), requestProfile()을 사용하였습니다.

더 많은 요청 속성은 여기에서 확인할 수 잇습니다.

 

onCreate() 밖에서 아래와 같이 지연초기화를 명시해주고

lateinit var mGoogleSignInClient : GoogleSignInClient

onCreate() 내부에서 GoogleSignInClient 객체를 만들어줍니다.

mGoogleSignInClient = GoogleSignIn.getClient(this, gso);

로그인, 로그아웃 버튼 추가

메인 엑티비티의 xml은 아래와 같이 구성되어있습니다.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
    tools:context=".MainActivity">

    <com.google.android.gms.common.SignInButton
        android:id="@+id/btnSignIn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/btnSignOut"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <com.google.android.material.button.MaterialButton
        android:id="@+id/btnSignOut"
        android:text="로그아웃"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/btnSignIn"
        app:layout_constraintTop_toTopOf="parent" />

    <com.google.android.material.button.MaterialButton
        android:id="@+id/btnGetProfile"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="프로필정보"
        app:layout_constraintStart_toEndOf="@+id/btnSignIn"
        app:layout_constraintEnd_toStartOf="@id/btnSignOut"
        app:layout_constraintTop_toBottomOf="@+id/btnSignIn"
        app:layout_constraintBottom_toBottomOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

아래와 같은 화면을 나타냅니다.

메인 엑티비티 내부에서 아래와 같이 클릭 리스너를 달아주었습니다.

with(binding){
    btnSignIn.setOnClickListener {
        signIn()
    }

    btnSignOut.setOnClickListener {
        signOut()
    }
    btnGetProfile.setOnClickListener {
        GetCurrentUserProfile()
    }
}

로그인 처리하기

가장 먼저 signInIntent를 진행하고 값을 받아오기 위하여 deprecated된 startActivityForResult가 아닌 ActivityResultLauncher을 사용해야합니다. 

registerForActivityResult는 프레그먼트 혹은 엑티비티를 만들기 전에 호출하는 것이 안전하기 때문에

onCreate() 밖에 ActivityResultLauncher의 지연초기화를 명시해주고

lateinit var resultLauncher: ActivityResultLauncher<Intent>

registerForActivityResult를 이용한 콜백 메서드를 만들어줍니다.

private fun setResultSignUp(){
    resultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()){ result ->
        // 정상적으로 결과가 받아와진다면 조건문 실행
        if (result.resultCode == Activity.RESULT_OK){
            val task: Task<GoogleSignInAccount> = GoogleSignIn.getSignedInAccountFromIntent(result.data)
            
            // 아래에서 구현할 계정의 정보를 이용하여 Log에 출력할 함수.
            handleSignInResult(task)
            
        }
    }
}

이 메서드(setResultSignUp())는 onCreate()의 최상단에서 호출해줍니다. 엑티비티에서 값을 받아온다면 자동으로 if문 안의 내용이 실행됩니다.

 

로그인에 성공하면 task에 해당 account의 정보가 담기게 됩니다. 그것을 다룰 handleSignInResult() 메서드를 아래와 같이 구현합니다.

private fun handleSignInResult(completedTask: Task<GoogleSignInAccount>) {
    try {
        val account = completedTask.getResult(ApiException::class.java)
        val email = account?.email.toString()
        val familyName = account?.familyName.toString()
        val givenName = account?.givenName.toString()
        val displayName = account?.displayName.toString()
        val photoUrl = account?.photoUrl.toString()

        Log.d("로그인한 유저의 이메일", email)
        Log.d("로그인한 유저의 성", familyName)
        Log.d("로그인한 유저의 이름", givenName)
        Log.d("로그인한 유저의 전체이름", displayName)
        Log.d("로그인한 유저의 프로필 사진의 주소", photoUrl)
    } catch (e: ApiException) {
        // The ApiException status code indicates the detailed failure reason.
        // Please refer to the GoogleSignInStatusCodes class reference for more information.
        Log.w("failed", "signInResult:failed code=" + e.statusCode)
    }
}

handleSignInResult 함수는 구글로그인한 유저의 account정보를 받아와 로그창에 정보들을 출력하도록 구현하였습니다. GoogleSignInOptions에서 requireEmail()과 같은 식으로 요청한 정보들만 사용이 가능합니다. 위에서 requireEmail()로 email을 requireProfile()로 familyName, givenName, displayName, photoUrl을 요청하였습니다.

 

마지막으로 구글 로그인 버튼이 눌렸을 때 인텐트를 시작할 signIn() 메서드를 아래와 같이 구현합니다.

private fun signIn() {
    val signInIntent: Intent = mGoogleSignInClient.getSignInIntent()
    resultLauncher.launch(signInIntent)
}

 

이제 앱을 실행하여 구글 로그인 버튼을 누르면

 

정상적으로 로그인되어 handleSignInResult 함수가 동작한 것을 확인할 수 있습니다.

 

현재 로그인한 사용자의 프로필 정보를 요청하기

GoogleSignIn.getLastSignedInAccount() 메서드를 사용하여 현재 로그인하고 있는 사용자의 프로필 정보를 확인할 수 있습니다.

현재 로그인되어있는 사용자의 프로필 정보를 로그창에 출력해주는 GetCurrentUserProfile() 메서드를 아래와 같이 구현하였습니다.

private fun GetCurrentUserProfile(){
    val curUser = GoogleSignIn.getLastSignedInAccount(this)
    curUser?.let {
        val email = curUser.email.toString()
        val familyName = curUser.familyName.toString()
        val givenName = curUser.givenName.toString()
        val displayName = curUser.displayName.toString()
        val photoUrl = curUser.photoUrl.toString()

        Log.d("현재 로그인 되어있는 유저의 이메일", email)
        Log.d("현재 로그인 되어있는 유저의 성", familyName)
        Log.d("현재 로그인 되어있는 유저의 이름", givenName)
        Log.d("현재 로그인 되어있는 유저의 전체이름", displayName)
        Log.d("현재 로그인 되어있는 유저의 프로필 사진의 주소", photoUrl)
    }
}

 

btnGetProfile.setOnClickListener {
    GetCurrentUserProfile()
}

정상적으로 동작하는 것을 확인할 수 있습니다.

로그아웃 처리하기

로그인을 하고 앱을 종료하여도 로그인 상태가 유지됩니다.

 

아래에 구현된 signOut() 메서드를 통하여 로그아웃을 진행할 수 있습니다.

private fun signOut() {
    mGoogleSignInClient.signOut()
        .addOnCompleteListener(this) {
            // ...
        }
}

 

아래에 구현된 revokeAccess() 메서드는 사용자에게 앱에서 Google 계정 연결을 해제하도록 하는 메서드입니다.  이 메서드가 실행되면 앱이 GoogleAPI에서 얻은 정보를 삭제합니다.

private fun revokeAccess() {
    mGoogleSignInClient.revokeAccess()
        .addOnCompleteListener(this) {
            // ...
        }
}

로그인 여부 판단

GoogleSignIn.getLastSignedInAccount() 를 사용하여 현재 로그인이 되어있는지 안되어있는지 확인할 수 있습니다.

이 메서드가 null을 반환하면 로그인이 되어있지 않은 상태이고 GoogleSignInAccount 객체를 반환하면 로그인이 되어있는 상태입니다.

 

onStart()에서 위의 로직을 진행하여 앱이 실행될 때 로그인이 되어있는 상태라면 Logged In을 그렇지 않다면 Not Yet을 출력하도록 만들었습니다.

override fun onStart() {
    super.onStart()
    val account = GoogleSignIn.getLastSignedInAccount(this)
    account?.let {
        Toast.makeText(this, "Logged In", Toast.LENGTH_SHORT).show()
    } ?: Toast.makeText(this, "Not Yet", Toast.LENGTH_SHORT).show()
}

로그인 상태

로그아웃 상태

반응형
  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 카카오스토리 공유하기