목차
FCM
Firebase Cloud Messaging (FCM)은 구글의 푸시 알림 서비스로, 모바일 애플리케이션, 웹 애플리케이션 및 서버 간에 메시지를 교환하는 데 사용됩니다. FCM은 이전에 Google Cloud Messaging (GCM)으로 알려져 있었으며, 이후 Firebase 서비스 스위트에 통합되면서 Firebase Cloud Messaging으로 명칭이 변경되었습니다.
FCM은 애플리케이션 개발자가 서버에서 클라이언트로 또는 클라이언트에서 서버로 다양한 유형의 메시지를 전송할 수 있도록 지원합니다. 이러한 메시지 중 일반적인 것은 푸시 알림, 데이터 메시지, 백그라운드 알림 등이 있습니다.
사진을 통해 본 흐름
1. 토큰 요청 및 획득
먼저 사용자가 앱을 설치하고 최초 실행시 토큰을 얻기위해 클라우드 서버에 요청을 보내고 토큰을 획득합니다
2. 서버에 토큰 저장
획득한 토큰을 서버로 전송하여 서버 db에 저장합니다
이 토큰은 서버가 클라우드에 메시지 전송을 요청할 때 어디로 보내는지 구분하기 위한 용도로 사용됩니다
3. 토큰을 이용해 메시지 전송 요청
서버에서 클라우드로 메시지 데이터와 함께 토큰을 보내 전송을 요청한다
4. 메시지 전송
클라우드는 요청받은 메시지를 토큰에 해당하는 단말기에 전송한다
5. 리스너를 통해 메시지 수신
앱이 실행중이 아니더라도 리스너를 통해 메시지를 수신할 수 있다
이제 실제 코드로 한 번 해보겠습니다~
우선 FCM을 사용하기 위해서 Firebase에 가서 새로운 프로젝트를 생성합니다
생성된 프로젝트에서 앱을 추가하여 시작하기에서 Android를 선택하여
빈칸을 완성하고 다음단계로 넘어갑니다.
그 다음 AndroidStudio 에서 build.gradle(project) 에
buildscript {
dependencies {
classpath 'com.google.gms:google-services:4.3.10'
}
}
위 코드를 추가해줍니다.
또한 build.gradle(app)에 플러그인 부분에 id 'com.google.gms.google-services'
를 추가해줍니다.
이제 본격적으로 기능을 만들어 보겠습니다..
build.gradle(app)의 dependencies에 implementation 'com.google.firebase:firebase-messaging-ktx'
를 추가해줍니다. (현재 작성자의 언어는 kotlin이기 때문에 java를 사용할 경우
implementation 'com.google.firebase:firebase-messaging:20.0.0'
자신의 안드로이드 스튜디오 버전에 맞게 사용하시길 바랍니다.)
잊지말고 퍼미션도 추가해줍시다. -> <uses-permission android:name="android.permission.INTERNET" />
그리고 FirebaseMessagingService를 상속받는 서비스를 하나 만들겠습니다.
class MyFirebaseMessagingService : FirebaseMessagingService() {
companion object {
private const val TAG = "FirebaseMessagingService"
}
private var FCMToken = ""
override fun onNewToken(token: String) {
super.onNewToken(token)
Log.d(TAG, "new Token ${token}")
val pref = this.getSharedPreferences("firebaseToken", Context.MODE_PRIVATE)
val editor = pref.edit()
editor.putString("firebaseToken", token).apply()
editor.apply()
Log.i(TAG, "토큰 저장 완료")
sendRegistrationToServer(token)
//여기서 서버에 토큰 저장시키기Todo
}
override fun onMessageReceived(message: RemoteMessage) {
super.onMessageReceived(message)
Log.d(TAG, "From : ${message.from}")
//받은 remoteMessage의 값 출력해보기. 데이터메세지 / 알림메세지
Log.d(TAG, "Message data : ${message.data}")
Log.d(TAG, "Message notification : ${message.notification?.body!!}")
/*if (message.notification?.body!!.isNotEmpty()) {
setNotification(message)
if (true) {
}
} else {
Log.e(TAG, "data가 비어있습니다. 메시지를 수신하지 못했습니다.")
//메세지에 전송된 알림 데이터가 포함되어있는지 확인하기위함
setNotification(
message
)
}*/
if (message.data.isNotEmpty()) {
scheduleJob()
} else {
handleNow()
}
//알람 내용이 비어 있지 않은 경우
if (message.notification!= null) {
setNotification(message)
}
}
// 메시지에 데이터 페이로드가 포함 되어 있을 때 실행되는 메서드
// 장시간 실행 (10초 이상) 작업의 경우 WorkManager를 사용하여 비동기 작업을 예약한다.
private fun scheduleJob() {
val work = OneTimeWorkRequest.Builder(MyWorker::class.java)
.build()
WorkManager.getInstance(this)
.beginWith(work)
.enqueue()
}
// 메시지에 데이터 페이로드가 포함 되어 있을 때 실행되는 메서드
// 10초 이내로 걸릴 때 메시지를 처리한다.
private fun handleNow() {
Log.d(TAG, "Short lived task is done.")
}
// 타사 서버에 토큰을 유지해주는 메서드이다.
private fun sendRegistrationToServer(token: String?) {
Log.d(TAG, "sendRegistrationTokenToServer($token)")
}
private fun setNotification(message : RemoteMessage) {
val channelId = "fcm_set_notification_channel" // 알림 채널 이름
val channelName = "fcm_set_notification"
val channelDescription = "fcm_send_notify"
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
// 오레오 버전 이후에는 채널이 필요
if (Build.VERSION.SDK_INT>= Build.VERSION_CODES.O) {
val importance = NotificationManager.IMPORTANCE_HIGH// 중요도 (HIGH: 상단바 표시 가능)
val channel = NotificationChannel(channelId, channelName, importance).apply{
description= channelDescription
}
notificationManager.createNotificationChannel(channel)
}
// RequestCode, Id를 고유값으로 지정하여 알림이 개별 표시
val uniId: Int = (System.currentTimeMillis() / 7).toInt()
// 일회용 PendingIntent : Intent 의 실행 권한을 외부의 어플리케이션에게 위임
val intent = Intent(this, MainActivity::class.java)
/*//각 key, value 추가
for(key in message.data.keys){
intent.putExtra(key, message.data.getValue(key))
}*/
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) // Activity Stack 을 경로만 남김(A-B-C-D-B => A-B)
//PendingIntent.FLAG_MUTABLE은 PendingIntent의 내용을 변경할 수 있도록 허용, PendingIntent.FLAG_IMMUTABLE은 PendingIntent의 내용을 변경할 수 없음
val pendingIntent = if (Build.VERSION.SDK_INT>= Build.VERSION_CODES.S) {
PendingIntent.getActivity(this, uniId, intent, PendingIntent.FLAG_ONE_SHOTor PendingIntent.FLAG_MUTABLE)
} else {
PendingIntent.getActivity(this, uniId, intent, PendingIntent.FLAG_IMMUTABLE)
}
val title: String? = message.notification?.title
val body: String? = message.notification?.body
// 알림에 대한 UI 정보, 작업
val notificationBuilder = NotificationCompat.Builder(this, channelId)
.setPriority(NotificationCompat.PRIORITY_HIGH) // 중요도 (HIGH: 상단바 표시 가능)
.setSmallIcon(R.mipmap.ic_launcher) // 아이콘 설정
.setContentTitle(title) // 제목
.setContentText(body) // 메시지 내용
.setAutoCancel(true) // 알람클릭시 삭제여부
.setContentIntent(pendingIntent) // 알림 실행 시 Intent
notificationManager.notify(uniId, notificationBuilder.build())
}
//필요한 곳 토큰 가져오기
fun getFirebaseToken() : String {
//비동기 방식
FirebaseMessaging.getInstance().token.addOnSuccessListener{
Log.d(TAG, "token=${it}")
FCMToken =it
}
return FCMToken
}
internal class MyWorker(appContext: Context, workerParams: WorkerParameters) :
Worker(appContext, workerParams) {
override fun doWork(): Result {
return Result.success()
}
}
}
작성자는 위와 같이 서비스를 완성했습니다.
onNewToken - 클라우드 서버에 등록되었을 때 호출되고, 파라미터로 전달된 token이 앱을 구분하기 위한 고유한 키가 됩니다.
onMessageReceived - 클라우드 서버에서 메시지를 전송하면 자동으로 호출되고, 해당 메서드안에서 메시지를 처리하여 사용자에게 알림을 보내거나 할 수 있습니다.
완성한 후
콘솔 > 프로젝트 선택 > 왼쪽 바에서 참여 > Cloud Messaging으로 이동하여
그러면 화면 상단에 send your first message를 클릭해 메세지를 보내 테스트를 하면
성공한 것을 확인할 수 있었습니다.
Kakao Login
카카오 Developers에 가서
- https://developers.kakao.com 회원가입 또는 로그인
- 내 애플리케이션 클릭
- 애플리케이션 추가하기
- 앱의 해시키 등록
4번은 아무 액티비티에 가서 아래 코드를 통해 카카오에 필요한 해시키를 얻으실 수 있습니다.
val hash = Utility.getKeyHash(this)
Log.d("MainActivity", "${hash.toString()}")
위 4가지 해야할 일을 완료하여 기본 세팅이 완료되면
카카오 Developers에 있는 내 애플리케이션의 네이티브 앱 키를 가져오셔서 안드로이드 스튜디오에서 사용하시면 됩니다.
로그인
1. settings.gradle
maven { url 'https://devrepo.kakao.com/nexus/content/groups/public/' } 코드 추가
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven { url '<https://devrepo.kakao.com/nexus/content/groups/public/>' }
}
}
2. build.gradle(app)
//카카오톡 로그인
implementation "com.kakao.sdk:v2-user:2.11.0"
3. 초기화
class GlobalApplication : Application() {
private val nativeKey = BuildConfig.kakao_app_key
override fun onCreate() {
super.onCreate()
// KaKao SDK 초기화
KakaoSdk.init(this, nativeKey)
Timber.plant(Timber.DebugTree())
}
}
3-1. 코드 생성 후 manifest에 application 태그의 name 부분에 등록(앱 시작시 실행)
3-2. meta-data 등록
3-3. 카카오 로그인을 구현하기 위해서 카카오 인증 서버에서 전달해주는 인가 코드(Authorization Code)를 발급해 서비스 앱에 등록된 Redirect URI을 전달받아야 합니다 따라서 전달 받기 위한 카카오의 액티비티를 생성해줍니다.
<application
android:name=".GlobalApplication"
/>
...
<meta-data
android:name="com.kakao.sdk.AppKey"
android:value="your_nativekey"/>
...
<!-- 카카오 로그인, 인가코드를 받기 위한 액티비티 -->
<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" />
<data android:host="oauth"
android:scheme="kakao네이티브앱키" />
</intent-filter>
</activity>
scheme에 꼭 네이티브앱키 앞에 kakao를 붙여줘야합니다.
기능 완성
// 카카오계정으로 로그인 공통 callback 구성
// 카카오톡으로 로그인 할 수 없어 카카오계정으로 로그인할 경우 사용됨
private val callback: (OAuthToken?, Throwable?) -> Unit ={token, error->
if (error != null) {
if (error is AuthError && error.response.error == "misconfigured") {
Log.e("LoginActivity", "Invalid android_key_hash or ios_bundle_id or web_site_url. Check your configuration", error)
}
Toast.makeText(this, "카카오계정으로 로그인 실패", Toast.LENGTH_SHORT).show()
Log.e("LoginActivity", "카카오계정으로 로그인 실패", error)
} else if (token != null) {
Toast.makeText(this, "카카오계정으로 로그인 성공", Toast.LENGTH_SHORT).show()
Log.i("LoginActivity", "카카오계정으로 로그인 성공: ${token.accessToken}")
UserApiClient.instance.accessTokenInfo{tokenInfo, error->
UserApiClient.instance.me{user, error->
name = user?.kakaoAccount?.profile?.nickname
Log.i("1 login", "${name}")
Log.i("2 login", "${user?.kakaoAccount?.profile?.nickname}")
val sharedPref = getSharedPreferences("profile_nickname", Context.MODE_PRIVATE)
with(sharedPref.edit()){
putString("profileNickname", user?.kakaoAccount?.profile?.nickname)
apply() // 비동기적으로 데이터를 저장
}
}
}
sendAccessTokenToServer(token.accessToken) //백엔드에 전달
}
}
...//MainActivity
binding.loginBtn.setOnClickListener {
// 카카오톡이 설치되어 있으면 카카오톡으로 로그인, 아니면 카카오계정으로 로그인
if (UserApiClient.instance.isKakaoTalkLoginAvailable(this)) {
UserApiClient.instance.loginWithKakaoTalk(this) { token, error ->
if (error != null) {
Log.e(TAG, "카카오톡으로 로그인 실패", error)
// 사용자가 카카오톡 설치 후 디바이스 권한 요청 화면에서 로그인을 취소한 경우,
// 의도적인 로그인 취소로 보고 카카오계정으로 로그인 시도 없이 로그인 취소로 처리 (예: 뒤로 가기)
if (error is ClientError && error.reason == ClientErrorCause.Cancelled) {
return@loginWithKakaoTalk
}
// 카카오톡에 연결된 카카오계정이 없는 경우, 카카오계정으로 로그인 시도
UserApiClient.instance.loginWithKakaoAccount(this, callback = callback)
} else if (token != null) {
Toast.makeText(this, "카카오톡으로 로그인 성공", Toast.LENGTH_SHORT).show()
Log.i(TAG, "accessToken: ${token}")
Log.i("5 login", "${name}")
sendAccessTokenToServer(token.accessToken)
}
}
} else {
UserApiClient.instance.loginWithKakaoAccount(this, callback = callback)
}
}
을 통해 로그인 구현을 완성했습니다.
- 참고 - FCM
- 참고 - Kakao Login
'Project' 카테고리의 다른 글
Google Solution Challenge 회고 [ Don't Jaywalk Scarlett) (1) | 2024.05.02 |
---|---|
[Project] 이겨야한다 팀 중간정리(웹러닝) - ML (0) | 2024.03.28 |
GDSC Solution Challenge 2024 (0) | 2024.03.24 |
GDSC Solution Challenge 2024 (0) | 2024.03.13 |
[Project-MOA(Design)] 그리드와 리스트 (0) | 2024.01.15 |