[Android / Kotlin] Firebase Cloud Messaging - PUSH ์•Œ๋ฆผ ๋ฐ›๊ธฐ

Subeenยท2023๋…„ 4์›” 3์ผ
0

Android

๋ชฉ๋ก ๋ณด๊ธฐ
17/73
post-thumbnail

๐Ÿ“ Firebase Console์„ ์‚ฌ์šฉํ•˜์—ฌ Firebase ์ถ”๊ฐ€

Firebase ํ”„๋กœ์ ํŠธ ๋งŒ๋“ค๊ธฐ ๋ฐ Firebase์— ์•ฑ ๋“ฑ๋ก ํ•˜๋Š” ๋ถ€๋ถ„์€ ์ฐธ๊ณ ํ•  ์ˆ˜ ์žˆ๋Š” ์˜ˆ์ œ๊ฐ€ ๋งŽ๊ธฐ์— ๋ณธ ํฌ์ŠคํŒ…์—์„œ๋Š” ์ƒ๋žตํ•˜๋ ค๊ณ  ํ•œ๋‹ค.
Firebase ๋ฌธ์„œ์˜ 'Firebase ํ”„๋กœ์ ํŠธ์— Firebase ์ถ”๊ฐ€' ๋ถ€๋ถ„์„ ์ฐธ๊ณ ํ•ด๋„ ์ข‹๋‹ค.

๐Ÿ“ Firebase ์—ฐ๊ฒฐํ•˜๊ธฐ

  • Android Studio ๋‚ด์—์„œ 'Tools > Firebase'๋ฅผ ํด๋ฆญํ•˜๋ฉด ํ™”๋ฉด ์šฐ์ธก์— ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํ™”๋ฉด์ด ๋‚˜ํƒ€๋‚˜๋Š”๋ฐ, 'Cloud Messaging'์„ ํด๋ฆญํ•œ๋‹ค.

  • (1) Connect your app to Firebase ์™€ (2) Add FCM to your app์„ ํด๋ฆญํ•ด Firebase์™€ ์—ฐ๊ฒฐ์„ ์ง„ํ–‰ํ•œ๋‹ค.
    ์—ฐ๊ฒฐ์ด ์™„๋ฃŒ๋˜๋ฉด ์•„๋ž˜ ํ™”๋ฉด๊ณผ ๊ฐ™์ด ํ‘œ์‹œ ๋œ๋‹ค.

๐Ÿ“ Firebase ์ธ์ฆ ์ถ”๊ฐ€

build.gradle(์•ฑ ์ˆ˜์ค€)์—์„œ Firebase ์ธ์ฆ Android ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ์ข…์† ํ•ญ๋ชฉ์„ ์ถ”๊ฐ€ํ•œ๋‹ค.

dependencies {
    implementation platform('com.google.firebase:firebase-bom:31.3.0')
    implementation 'com.google.firebase:firebase-analytics-ktx'
    implementation 'com.google.firebase:firebase-messaging:23.0.3'
}

๐Ÿ“ ์•ฑ ๋งค๋‹ˆํŽ˜์ŠคํŠธ ์ˆ˜์ •

๐Ÿ“Œ ์•ฑ์˜ ๋งค๋‹ˆํŽ˜์ŠคํŠธ์— FirebaseMessagingService๋ฅผ ํ™•์žฅํ•˜๋Š” ์„œ๋น„์Šค ์ถ”๊ฐ€
์ด ์„œ๋น„์Šค๋Š” ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์•ฑ์˜ ์•Œ๋ฆผ์„ ์ˆ˜์‹ ํ•˜๋Š” ๊ฒƒ ์™ธ์— ๋‹ค๋ฅธ ๋ฐฉ์‹์œผ๋กœ ๋ฉ”์‹œ์ง€๋ฅผ ์ฒ˜๋ฆฌํ•˜๋ ค๋Š” ๊ฒฝ์šฐ์— ํ•„์š”ํ•˜๋‹ค. ํฌ๊ทธ๋ผ์šด๋“œ ์•ฑ์˜ ์•Œ๋ฆผ ์ˆ˜์‹ , ๋ฐ์ดํ„ฐ ํŽ˜์ด๋กœ๋“œ ์ˆ˜์‹ , ์—…์ŠคํŠธ๋ฆผ ๋ฉ”์‹œ์ง€ ์ „์†ก ๋“ฑ์„ ์ˆ˜ํ–‰ํ•˜๋ ค๋ฉด ์ด ์„œ๋น„์Šค๋ฅผ ํ™•์žฅํ•ด์•ผ ํ•œ๋‹ค.

  • AndroidManifest.xml
        <service
            android:name=".fcm.MyFirebaseMessagingService"
            android:exported="false">
            <intent-filter>
                <action android:name="com.google.firebase.MESSAGING_EVENT" />
            </intent-filter>
        </service>

๐Ÿ“ ๊ถŒํ•œ ์š”์ฒญ (Android 13)

๐Ÿ’ก Android 13(API ์ˆ˜์ค€ 33)์—์„œ๋Š” ์•ฑ์—์„œ ์˜ˆ์™ธ ์—†๋Š” ์•Œ๋ฆผ์„ ๋ณด๋‚ด๊ธฐ ์œ„ํ•œ ์ƒˆ๋กœ์šด ๋Ÿฐํƒ€์ž„ ๊ถŒํ•œ POST_NOTIFICATIONS๋ฅผ ๋„์ž…ํ–ˆ๋‹ค.

  • AndroidManifest.xml
    ์•ฑ์˜ ๋งค๋‹ˆํŽ˜์ŠคํŠธ ํŒŒ์ผ์—์„œ POST_NOTIFICATIONS ๊ถŒํ•œ์„ ์„ ์–ธํ•œ๋‹ค.
<manifest ...>
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
    <application ...>
        ...
    </application>
</manifest>
  • Android API ์ˆ˜์ค€์ด 33 ์ด์ƒ์ผ ๊ฒฝ์šฐ์—๋งŒ ๊ถŒํ•œ์˜ ํ—ˆ์šฉ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•œ๋‹ค.
class FCMActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding
    private val PERMISSION_REQUEST_CODE = 5000

    private fun permissionCheck() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            val permissionCheck = ContextCompat.checkSelfPermission(
                this,
                android.Manifest.permission.POST_NOTIFICATIONS
            )
            if (permissionCheck != PackageManager.PERMISSION_GRANTED) {
                ActivityCompat.requestPermissions(
                    this,
                    arrayOf(android.Manifest.permission.POST_NOTIFICATIONS),
                    PERMISSION_REQUEST_CODE
                )
            }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_fcm)

        permissionCheck()
    }
    
    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        when (requestCode) {
            PERMISSION_REQUEST_CODE -> {
                if (grantResults.isEmpty() || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
                    Toast.makeText(applicationContext, "Permission is denied", Toast.LENGTH_SHORT)
                        .show()
                } else {
                    Toast.makeText(applicationContext, "Permission is granted", Toast.LENGTH_SHORT)
                        .show()
                }
            }
        }
    }
}

๐Ÿ‘ฉ๐Ÿปโ€๐Ÿ’ป FirebaseMessagingService

์œ„์—์„œ 'FirebaseMessagingService' ๋ฅผ ํ™•์žฅํ•˜๋Š” ์„œ๋น„์Šค๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ํ•ด๋‹น ์„œ๋น„์Šค๋ฅผ ๋“ฑ๋กํ•˜๋Š” ์ ˆ์ฐจ๋ฅผ ์ง„ํ–‰ํ•˜์˜€๋‹ค.
FirebaseMessagingService๋ฅผ ์ƒ์†ํ•˜๋Š” MyFirebaseMessagingService ํด๋ž˜์Šค๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.

  • MyFirebaseMessagingService.kt
class MyFirebaseMessagingService : FirebaseMessagingService() {

    // ๋ฉ”์‹œ์ง€๋ฅผ ์ˆ˜์‹ ํ•  ๋•Œ ํ˜ธ์ถœ ๋œ๋‹ค.
    // ์ˆ˜์‹ ๋œ RemoteMessage ๊ฐ์ฒด๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๊ณ  ๋ฉ”์‹œ์ง€ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋‹ค.
    override fun onMessageReceived(remoteMessage: RemoteMessage) {
        super.onMessageReceived(remoteMessage)

        // ๋ฉ”์‹œ์ง€์— ๋ฐ์ดํ„ฐ ํŽ˜์ด๋กœ๋“œ๊ฐ€ ํฌํ•จ ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•œ๋‹ค.
        // ํŽ˜์ด๋กœ๋“œ๋ž€ ์ „์†ก๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์˜๋ฏธํ•œ๋‹ค.
        if (remoteMessage.data.isNotEmpty()) {
            Log.d(TAG, "Message data payload: ${remoteMessage.data}")
            sendNotification(
                remoteMessage.data["title"].toString(),
                remoteMessage.data["body"].toString()
            )
        } else {
            // ๋ฉ”์‹œ์ง€์— ์•Œ๋ฆผ ํŽ˜์ด๋กœ๋“œ๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•œ๋‹ค.
            remoteMessage.notification?.let {
                sendNotification(
                    remoteMessage.notification!!.title.toString(),
                    remoteMessage.notification!!.body.toString()
                )
            }
        }
    }

    // ์ƒˆ ํ† ํฐ์ด ์ƒ์„ฑ๋  ๋•Œ๋งˆ๋‹ค onNewToken ์ฝœ๋ฐฑ์ด ํ˜ธ์ถœ๋œ๋‹ค.
    // ๋“ฑ๋ก ํ† ํฐ์ด ์ฒ˜์Œ ์ƒ์„ฑ๋˜๋ฏ€๋กœ ์—ฌ๊ธฐ์„œ ํ† ํฐ์„ ๊ฒ€์ƒ‰ํ•  ์ˆ˜ ์žˆ๋‹ค.
    override fun onNewToken(token: String) {
        super.onNewToken(token)
        sendRegistrationToServer(token)
    }

    // ๋ฉ”์‹œ์ง€์— ๋ฐ์ดํ„ฐ ํŽ˜์ด๋กœ๋“œ๊ฐ€ ํฌํ•จ ๋˜์–ด ์žˆ์„ ๋•Œ ์‹คํ–‰๋˜๋Š” ๋ฉ”์„œ๋“œ
    // ์žฅ์‹œ๊ฐ„ ์‹คํ–‰ (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)")
    }

    // ์ˆ˜์‹  ๋œ FCM ๋ฉ”์‹œ์ง€๋ฅผ ํฌํ•จํ•˜๋Š” ๊ฐ„๋‹จํ•œ ์•Œ๋ฆผ์„ ๋งŒ๋“ค๊ณ  ํ‘œ์‹œํ•œ๋‹ค.
    private fun sendNotification(title: String, body: String) {
        val intent = Intent(this, FCMActivity::class.java)
        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
        val pendingIntent = PendingIntent.getActivity(
            this, 0, intent,
            PendingIntent.FLAG_IMMUTABLE
        )

        val channelId = "fcm_default_channel"
        val defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
        val notificationBuilder = NotificationCompat.Builder(this, channelId)
            .setSmallIcon(R.drawable.notification_icon)
            .setContentTitle(title)
            .setContentText(body)
            .setAutoCancel(true)
            .setSound(defaultSoundUri)
            .setContentIntent(pendingIntent)

        val notificationManager =
            getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

        // ์˜ค๋ ˆ์˜ค ์ด์ƒ์—์„œ ์•Œ๋ฆผ์„ ์ œ๊ณตํ•˜๋ ค๋ฉด ์•ฑ์˜ ์•Œ๋ฆผ ์ฑ„๋„์„ ์‹œ์Šคํ…œ์— ๋“ฑ๋กํ•ด์•ผ ํ•œ๋‹ค.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(
                channelId,
                "Channel human readable title",
                NotificationManager.IMPORTANCE_DEFAULT
            )
            notificationManager.createNotificationChannel(channel)
        }

        notificationManager.notify(0, notificationBuilder.build())
    }

    companion object {
        private const val TAG = "MyFirebaseMsgService"
    }

    internal class MyWorker(appContext: Context, workerParams: WorkerParameters) :
        Worker(appContext, workerParams) {
        override fun doWork(): Result {
            return Result.success()
        }
    }
}

๐Ÿ‘ฉ๐Ÿปโ€๐Ÿ’ป Activity

์•กํ‹ฐ๋น„ํ‹ฐ์— ํ˜„์žฌ ๋“ฑ๋ก ๋œ ํ† ํฐ์„ ๊ฐ€์ ธ์˜ค๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค.
๐Ÿ’ก ํ˜„์žฌ ํ† ํฐ์„ ๊ฐ€์ ธ์˜ค๋ ค๋ฉด FirebaseMessaging.getInstance().getToken()์„ ํ˜ธ์ถœํ•œ๋‹ค.

๐Ÿ“Œ ๊ธฐ๊ธฐ ๋“ฑ๋ก ํ† ํฐ ์•ก์„ธ์Šค
FCM SDK๋Š” ์•ฑ์„ ์ฒ˜์Œ ์‹œ์ž‘ํ•  ๋•Œ ํด๋ผ์ด์–ธํŠธ ์•ฑ ์ธ์Šคํ„ด์Šค์šฉ ๋“ฑ๋ก ํ† ํฐ์„ ์ƒ์„ฑํ•œ๋‹ค.
๋‹จ์ผ ๊ธฐ๊ธฐ๋ฅผ ํƒ€๊ฒŸํŒ…ํ•˜๊ฑฐ๋‚˜ ๊ธฐ๊ธฐ ๊ทธ๋ฃน์„ ๋งŒ๋“ค๋ ค๋ฉด FirebaseMessagingService๋ฅผ ํ™•์žฅํ•˜๊ณ  onNewToken์„ ์žฌ์ •์˜ํ•˜์—ฌ ์ด ํ† ํฐ์— ์•ก์„ธ์Šคํ•ด์•ผ ํ•œ๋‹ค.

  • ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ฒฝ์šฐ์— ๋“ฑ๋ก ํ† ํฐ์ด ๋ณ€๊ฒฝ๋  ์ˆ˜ ์žˆ๋‹ค.
    • ์ƒˆ ๊ธฐ๊ธฐ์—์„œ ์•ฑ ๋ณต์›
    • ์‚ฌ์šฉ์ž๊ฐ€ ์•ฑ ์ œ๊ฑฐ/์žฌ์„ค์น˜
    • ์‚ฌ์šฉ์ž๊ฐ€ ์•ฑ ๋ฐ์ดํ„ฐ ์†Œ๊ฑฐ
  • FCMActivity.kt
class FCMActivity : AppCompatActivity() {
    companion object {
        private val PERMISSION_REQUEST_CODE = 5000
        private val TAG = "FCMActivity"
    }
    private lateinit var binding: ActivityFcmBinding


    private fun permissionCheck() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            val permissionCheck = ContextCompat.checkSelfPermission(
                this,
                android.Manifest.permission.POST_NOTIFICATIONS
            )
            if (permissionCheck != PackageManager.PERMISSION_GRANTED) {
                ActivityCompat.requestPermissions(
                    this,
                    arrayOf(android.Manifest.permission.POST_NOTIFICATIONS),
                    PERMISSION_REQUEST_CODE
                )
            }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_fcm)

        permissionCheck()

        // ํ˜„์žฌ ํ† ํฐ์„ ๊ฐ€์ ธ์˜ค๋ ค๋ฉด
        // FirebaseMessaging.getInstace().getToken()์„ ํ˜ธ์ถœํ•œ๋‹ค.
        FirebaseMessaging.getInstance().token.addOnCompleteListener(OnCompleteListener { task ->
            if (!task.isSuccessful) {
                Log.w(TAG, "Fetching FCM registration token failed", task.exception)
                return@OnCompleteListener
            }

            // FCM ๋“ฑ๋ก ํ† ํฐ ๊ฐ€์ ธ์˜ค๊ธฐ
            val token = task.result

            val msg = "FCM Registration token: " + token;
            Log.d(TAG, msg)
            Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
        })
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        when (requestCode) {
            PERMISSION_REQUEST_CODE -> {
                if (grantResults.isEmpty() || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
                    Toast.makeText(applicationContext, "Permission is denied", Toast.LENGTH_SHORT)
                        .show()
                } else {
                    Toast.makeText(applicationContext, "Permission is granted", Toast.LENGTH_SHORT)
                        .show()
                }
            }
        }
    }
}

๐Ÿ“ Firebase Console์—์„œ ๋ฉ”์‹œ์ง€ ๋ณด๋‚ด๊ธฐ

  • Firebase Console ํ”„๋กœ์ ํŠธ ๋‚ด์—์„œ ์ขŒ์ธก ํƒญ์˜ '์ฐธ์—ฌ > Messaging' ์„ ํด๋ฆญํ•œ๋‹ค.

  • Messaging ํ™”๋ฉด์—์„œ '์บ ํŽ˜์ธ > ์ƒˆ ์บ ํŽ˜์ธ' ์„ ํด๋ฆญํ•˜๋ฉด ์•Œ๋ฆผ ์ž‘์„ฑ ํ™”๋ฉด์œผ๋กœ ์ด๋™ํ•œ๋‹ค.
    (1) ์•Œ๋ฆผ ํƒญ์—์„œ ์•Œ๋ฆผ ์ œ๋ชฉ๊ณผ ์•Œ๋ฆผ ํ…์ŠคํŠธ๋ฅผ ์ž…๋ ฅํ•œ๋‹ค.
    (2) ํƒ€๊ฒŸ ํƒญ์—์„œ ์•ฑ์„ ์„ ํƒํ•œ๋‹ค.
    (3) ์˜ˆ์•ฝ ํƒญ์—์„œ ์•Œ๋ฆผ์„ ๋ณด๋‚ด๊ณ ์ž ํ•˜๋Š” ๋‚ ์งœ ๋ฐ ์‹œ๊ฐ„์„ ์„ ํƒํ•œ๋‹ค.
    (4), (5) ์„ ํƒ์‚ฌํ•ญ์ด๋ฏ€๋กœ ์„ ํƒ ์ž‘์„ฑ ํ›„ ๊ฒ€ํ†  ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅธ๋‹ค.

๐Ÿ‘€ ๊ฒฐ๊ณผ ํ™”๋ฉด

Firebase Console์˜ ์•Œ๋ฆผ ์ž‘์„ฑ ํ™”๋ฉด์—์„œ ์ œ๋ชฉ๊ณผ ์•Œ๋ฆผ ํ…์ŠคํŠธ๋งŒ ์ž…๋ ฅํ•˜์—ฌ ์ „์†กํ–ˆ์œผ๋ฉฐ, ๋‹ค์Œ ํ™”๋ฉด๊ณผ ๊ฐ™์ด ์•Œ๋ฆผ ๋ฉ”์‹œ์ง€๊ฐ€ ์ „์†ก๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

profile
๊ฐœ๋ฐœ ๊ณต๋ถ€ ๊ธฐ๋ก ๐ŸŒฑ

0๊ฐœ์˜ ๋Œ“๊ธ€