첫번째 생각한 방법은 FCM (Firebase cloud Message)를 이용한 방법. 클라이언트도 파이어베이스를 추가로 설치해줘야 해서 귀찮아 보인다.
그래서 추가로 알아본 것이 GCM (Google cloud message). FCM 이 사실 이 GCM의 새로운 버전. 주요 구조가 비슷하다.
GCM 이 Android 와 iOS만 지원하는 것을 FCM 은 Mobile Web 등 여러 플랫폼을 추가로 지원한다.
국내 많은 모바일 앱의 Push 가 GCM 을 이용하는 상황.
GCM을 이용할 때 클라이언트에서 구현해야 하는 등록 및 구독 로직을 FCM 라이브러리 안에 포함시켜 개발자가 별도로 개발하지 않아도 된다.
또한 Firebase Analytics insight 기능으로 특정 타겟층에만 push 전송이 가능. 앞으로 구글에서 새로운 기능을 모두 FCM 쪽에만 추가할 예정이라 하니, FCM 을 사용해야겠다.
FCM 에서 메시지를 수신하도록 기기를 등록
클라이언트에서 앱 인스턴스를 고유하게 식별하는 등록 토큰을 받는다.
다운스트림 메시지 전송 및 수신
앱 서버에서 클라이언트 앱에 메시지를 전송한다
→ 메시지 요청은 FCM 백인드로 전송
→ FCM 백앤드는 메시지 요청을 수신하고 플랫폼별 전송 레이어로 보낸다.
→ 기기가 온라인 상태이면 플랫폼별 전송 레이어를 통해 메시지를 기기로 전송
→ 기기에서 클라이언트 앱이 메시지 또는 알림을 수신
알림메시지와 데이터 메시지, 두 가지 형태
알림 메시지는 FCM SDK 에서 자동으로 처리되는 형태의 메시지이며, 데이터 메시지는 클라이언트 앱에서 처리해야하는 유형.
알림 메시지에는 사전에 정의된 키 모음이나 커스텀 키-값 쌍의 데이터 페이로드(선택사항)가 포함된다.
앱이 백그라운드 상태이면 알림 메시지가 알림 목록으로 전송됩니다. 포그라운드 상태의 앱인 경우 콜백 함수가 메시지를 처리합니다.
Firebase console에서 프로젝트 생성하기
프로젝트 추가를 하여 프로젝트를 만든다.
하라는대로 Firebase 를 추가한다.
Firebase 구성파일과 클라우드 라이브러리 Gradle 파일을 모두 추가해주면 기본 설정은 끝.
FirebaseMessaging.getInstance().token.addOnCompleteListener(OnCompleteListener { task ->
if (!task.isSuccessful) {
Log.w(TAG, "Fetching FCM registration token failed", task.exception)
return@OnCompleteListener
}
// Get new FCM registration token
val token = task.result
// Log and toast
val msg =token.toString();
Log.d(TAG, msg)
Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
})
test 를 진행하기 위해 일단 생성된 token을 Log 로 저장해준 후,
Firebase console에 접근하면 보이는 In-App-messaging → 알림작성으로 기기 테스트를 진행하였다.
알림이 도착한다! 신난다
토큰을 서버에 post 로 전달해줬다.
<service
android:name=".MyFirebaseMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/ic_smile" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_color"
android:resource="@color/colorAccent" />
package com.gyoung.movierecord
import android.R
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.media.RingtoneManager
import android.os.Build
import android.util.Log
import androidx.core.app.NotificationCompat
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
class MyFirebaseMessagingService : FirebaseMessagingService() {
val TAG = "FIREBASE_MESSAGING"
override fun onMessageReceived(remoteMessage: RemoteMessage) {
Log.d(TAG, "From: " + remoteMessage.from)
// Check if message contains a data payload.
if (remoteMessage.data.size > 0) {
Log.d(TAG, "Message data payload: " + remoteMessage.data)
if ( /* Check if data needs to be processed by long running job */true) {
} else {
// Handle message within 10 seconds
handleNow()
}
}
// Check if message contains a notification payload.
if (remoteMessage.notification != null) {
Log.d(TAG, "Message Notification Body: " + remoteMessage.notification!!.body)
sendNotification(remoteMessage.notification!!.body)
}
}
private fun handleNow() {
Log.d(TAG, "Short lived task is done.")
}
/**
* Create and show a simple notification containing the received FCM message.
*
* @param messageBody FCM message body received.
*/
private fun sendNotification(messageBody: String?) {
val intent = Intent(this, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
val pendingIntent =
PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_ONE_SHOT)
val channelId = "channelid"
val defaultSoundUri =
RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
val notificationBuilder =
NotificationCompat.Builder(this, channelId)
.setSmallIcon(R.mipmap.sym_def_app_icon)
.setContentTitle("새로운 메세지입니다")
.setContentText(messageBody)
.setAutoCancel(true)
.setSound(defaultSoundUri)
.setContentIntent(pendingIntent)
val notificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channelName = "name"
val channel =
NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_HIGH)
notificationManager.createNotificationChannel(channel)
}
notificationManager.notify(0, notificationBuilder.build())
}
}
일단 notification 을 처리하는 것만 처리해주고 data는 log 로만 잘 전달되는지 확인하였다.
서버에서 FCM 백앤드에 메시지 요청을 보내면, FCM 백앤드가 사용자 기기의 클라이언트 앱에 메시지를 보내는 형태이다.
...
const admin = require('firebase-admin');
let serAccount = require('../firebase-admin.json');
admin.initializeApp({
credential: admin.credential.cert(serAccount),
});
앱 서버에서 FCM 으로 보내는 요청은 승인을 받아야 한다. HTTP v1 API 승인을 이용하게 되는데, HTTP v1 API 승인은 수명이 짧다. Firebase Admin SDK 를 사용하여 메세지를 보내면 라이브러리에서 토큰이 자동으로 처리된다.
npm install firebase-admin --save
const admin = require('firebase-admin');
let serAccount = require('./record_movie.json');
admin.initializeApp({
credential: admin.credential.cert(serAccount),
})
serAccount 에는 Firebase Admin SDK 의 비공개 키를 다운받아 경로를 넣어주면 된다.(프로젝트 설정 → 서비스 계정)
// push PAGE
router.get('/push_send', function (req, res, next) {
let target_token =`{token}`
//target_token은 푸시 메시지를 받을 디바이스의 토큰값입니다
let message = {
notification: {
title: '테스트 데이터 발송',
body: '데이터가 잘 가나요?',
},
token: target_token,
}
admin
.messaging()
.send(message)
.then(function (response) {
console.log('Successfully sent message: : ', response)
return res.status(200).json({success : true})
})
.catch(function (err) {
console.log('Error Sending message!!! : ', err)
return res.status(400).json({success : false})
});
})
포스트맨으로 알림 메시지를 넣어줬다.
잘간다.
앱 꺼져있을 때 이미지는 manifest에서 넣어준 이미지 소스.
onMessageReceived
함수에서 앱 켜져있을 때에도 아마 이미지를 넣어줄 수 있을텐데 귀찮아서 안했음.
remoteMessage
에 data
를 넣어줬을 때에도 data 잘 간다.
Log : D/FIREBASE_MESSAGING: Message data payload: {body=데이터가 잘 가나요?, hello=hh, title=테스트 데이터 발송}
let message = {
data: {
title: '테스트 데이터 발송',
body: '데이터가 잘 가나요?',
hello : 'hh'
},
token: target_token,
}