FCM 등록 토큰 관리를 위한 권장사항

FCM API를 사용해 전송 요청을 프로그래매틱 방식으로 빌드하는 경우 시간이 지나면서 비활성 등록 토큰이 있는 비활성 기기에 메시지를 전송하여 리소스 낭비가 발생할 수 있습니다. 이러한 상황은 Firebase Console에 보고된 메시지 전송 데이터 또는 BigQuery로 내보낸 데이터에 영향을 줄 수 있으며 이로 인해 전달 속도가 급격하게 감소한 것으로 나타나지만 실제로는 그렇지 않습니다. 이 가이드에서는 효율적인 메시지 타겟팅과 유효한 전송 보고를 보장하기 위해 취할 수 있는 몇 가지 조치에 대해 설명합니다.

만료된 비활성 등록 토큰

비활성 등록 토큰은 FCM에 1개월 넘게 연결되지 않은 비활성 기기와 연결된 토큰입니다. 시간이 지날수록 기기가 FCM에 다시 연결될 가능성은 점점 줄어듭니다. 이러한 비활성 토큰의 메시지 전송 및 주제 팬아웃은 전송되지 않을 가능성이 높습니다.

토큰이 비활성화되는 이유는 여러 가지가 있습니다. 예를 들어 토큰이 손실되거나, 파손되거나, 스토리지로 넘어가거나, 잊혀진 경우입니다.

비활성 토큰이 270일 동안 활동이 없으면 FCM에서 만료된 토큰으로 간주합니다. 토큰이 만료되면 FCM는 토큰을 유효하지 않은 것으로 표시하고 토큰으로의 전송을 거부합니다. 하지만 FCM은 기기가 다시 연결되고 앱이 열리는 흔치 않은 경우에 앱 인스턴스의 새 토큰을 발급합니다.

기본 권장사항

FCM API를 사용해 프로그래매틱 방식으로 전송 요청을 빌드하는 모든 앱에서 따라야 하는 몇 가지 기본 사항이 있습니다. 주요 권장사항은 다음과 같습니다.

  • FCM에서 등록 토큰을 검색하여 서버에 저장합니다. 서버의 중요한 역할은 각 클라이언트의 토큰을 추적하고 활성 토큰의 업데이트된 목록을 유지하는 것입니다. 코드 및 서버에 토큰 타임스탬프를 구현하고 이 타임스탬프를 정기적으로 업데이트하는 것이 좋습니다.
  • 토큰을 최신 상태로 유지하고 비활성 토큰을 삭제합니다. FCM에서 더 이상 유효하지 않은 것으로 간주하는 토큰을 삭제하는 것 외에도 토큰이 비활성 상태임을 나타내는 다른 징후를 모니터링하고 사전에 삭제하는 것이 좋습니다. 이 가이드에서는 이를 위한 몇 가지 옵션을 설명합니다.

등록 토큰 검색 및 저장

FCM SDK는 앱을 처음 시작할 때 클라이언트 앱 인스턴스용 등록 토큰을 생성합니다. 이 토큰은 API에서 타겟팅된 전송 요청에 포함하거나 주제 타겟팅을 위해 주제 구독에 추가해야 합니다.

앱을 처음 시작할 때 이 토큰을 검색하여 타임스탬프와 함께 앱 서버에 저장하는 것이 좋습니다. 이 타임스탬프는 FCM SDK에서 제공하지 않으므로 코드와 서버에서 구현해야 합니다.

또한 토큰을 서버에 저장하고 다음과 같은 경우처럼 토큰이 변경될 때마다 타임스탬프를 업데이트하는 것이 중요합니다.

  • 새 기기에서 앱 복원
  • 사용자가 앱 제거 또는 재설치
  • 사용자가 앱 데이터 소거
  • FCM에서 기존 토큰이 만료된 후 앱이 다시 활성화

예: Cloud Firestore에 토큰과 타임스탬프 저장

예를 들어 Cloud Firestore를 사용하여 fcmTokens라는 컬렉션에 토큰을 저장할 수 있습니다. 컬렉션의 각 문서 ID는 사용자 ID에 해당하며 문서에서 현재 등록 토큰과 최종 업데이트된 타임스탬프를 저장합니다. 다음 Kotlin 예시와 같이 set 함수를 사용합니다.

    /**
     * Persist token to third-party servers.
     *
     * Modify this method to associate the user's FCM registration token with any server-side account
     * maintained by your application.
     *
     * @param token The new token.
     */
    private fun sendTokenToServer(token: String?) {
        // If you're running your own server, call API to send token and today's date for the user

        // Example shown below with Firestore
        // Add token and timestamp to Firestore for this user
        val deviceToken = hashMapOf(
            "token" to token,
            "timestamp" to FieldValue.serverTimestamp(),
        )
        // Get user ID from Firebase Auth or your own server
        Firebase.firestore.collection("fcmTokens").document("myuserid")
            .set(deviceToken)
    }

토큰이 검색될 때마다 sendTokenToServer를 호출하여 Cloud Firestore에 저장됩니다.

    /**
     * Called if the FCM registration token is updated. This may occur if the security of
     * the previous token had been compromised. Note that this is called when the
     * FCM registration token is initially generated so this is where you would retrieve the token.
     */
    override fun onNewToken(token: String) {
        Log.d(TAG, "Refreshed token: $token")

        // If you want to send messages to this application instance or
        // manage this apps subscriptions on the server side, send the
        // FCM registration token to your app server.
        sendTokenToServer(token)
    }
        var token = Firebase.messaging.token.await()

        // Check whether the retrieved token matches the one on your server for this user's device
        val preferences = this.getPreferences(Context.MODE_PRIVATE)
        val tokenStored = preferences.getString("deviceToken", "")
        lifecycleScope.launch {
            if (tokenStored == "" || tokenStored != token)
            {
                // If you have your own server, call API to send the above token and Date() for this user's device

                // Example shown below with Firestore
                // Add token and timestamp to Firestore for this user
                val deviceToken = hashMapOf(
                    "token" to token,
                    "timestamp" to FieldValue.serverTimestamp(),
                )

                // Get user ID from Firebase Auth or your own server
                Firebase.firestore.collection("fcmTokens").document("myuserid")
                    .set(deviceToken).await()
            }
        }

토큰 최신 상태 유지 및 비활성 토큰 삭제

토큰이 최신 상태인지 비활성 상태인지를 판단하는 것이 항상 간단하지만은 않습니다. 모든 사례를 포괄하도록 토큰을 비활성 상태로 간주할 기준을 정해야 합니다. 기본적으로 FCM은 앱 인스턴스가 한 달 동안 연결되지 않은 경우 토큰을 비활성 상태로 간주합니다. 1개월이 지난 토큰은 비활성 기기일 가능성이 높으며 그 외의 활성 기기는 토큰을 갱신했을 것입니다.

사용 사례에 따라 한 달이 너무 짧거나 너무 길 수 있으므로 자신에게 적합한 기준을 결정할 수 있습니다.

FCM 백엔드에서 잘못된 토큰 응답 감지

FCM에서 잘못된 토큰 응답을 감지하고 유효하지 않거나 만료된 것으로 알려진 등록 토큰을 시스템에서 삭제하여 이에 대응해야 합니다. HTTP v1 API를 사용하면 다음과 같은 오류 메시지가 전송 요청이 잘못된 토큰이나 만료된 토큰을 타겟팅했음을 나타낼 수 있습니다.

  • UNREGISTERED(HTTP 404)
  • INVALID_ARGUMENT(HTTP 400)

메시지 페이로드가 유효하다고 확신하고 타겟팅된 토큰에 대해 이러한 응답 중 하나를 수신하는 경우 이 토큰은 다시 유효한 상태가 되지 않으므로 해당 레코드를 삭제하는 것이 안전합니다. 예를 들어 Cloud Firestore에서 유효하지 않은 토큰을 삭제하려면 다음과 같은 함수를 배포하고 실행할 수 있습니다.

    // Registration token comes from the client FCM SDKs
    const registrationToken = 'YOUR_REGISTRATION_TOKEN';

    const message = {
    data: {
        // Information you want to send inside of notification
    },
    token: registrationToken
    };

    // Send message to device with provided registration token
    getMessaging().send(message)
    .then((response) => {
        // Response is a message ID string.
    })
    .catch((error) => {
        // Delete token for user if error code is UNREGISTERED or INVALID_ARGUMENT.
        if (errorCode == "messaging/registration-token-not-registered") {
            // If you're running your own server, call API to delete the
            token for the user

            // Example shown below with Firestore
            // Get user ID from Firebase Auth or your own server
            Firebase.firestore.collection("fcmTokens").document(user.uid).delete()
        }
    });

FCM은 토큰이 270일 후에 만료되거나 클라이언트가 명시적으로 등록 취소한 경우에만 잘못된 토큰 응답을 반환합니다. 자체 정의에 따라 비활성을 더욱 정확하게 추적해야 하는 경우 사전에 비활성 등록 토큰을 삭제할 수 있습니다.

정기적으로 토큰 업데이트

서버에서 모든 등록 토큰을 주기적으로 검색 및 업데이트하는 것이 좋습니다. 이를 위해 다음을 수행해야 합니다.

  • 클라이언트 앱에 앱 로직을 추가하여 적절한 API 호출(예: Apple 플랫폼의 경우 token(completion):, Android의 경우 getToken())을 사용해 현재 토큰을 검색한 다음 앱 서버로 전송하여 저장합니다(타임스탬프 포함). 모든 클라이언트 및 토큰을 포괄하도록 월별 작업으로 구성할 수 있습니다.
  • 서버 로직을 추가하여 토큰 변경 여부와 관계없이 주기적으로 토큰의 타임스탬프를 업데이트합니다.

WorkManager를 사용하여 토큰을 업데이트하는 Android 로직의 예시는 Firebase 블로그의 클라우드 메시징 토큰 관리를 참조하세요.

어떤 타이밍 패턴을 따르든지 토큰은 주기적으로 업데이트해야 합니다. 업데이트 빈도를 한 달에 한 번으로 설정하면 배터리에 대한 영향과 비활성 등록 토큰의 감지 기능 간에 적절한 균형을 유지할 가능성이 높습니다. 이렇게 업데이트를 수행하면 비활성화된 기기가 다시 활성화될 때 등록이 업데이트되도록 할 수도 있습니다. 일주일 이내 간격으로 업데이트를 수행해도 별다른 이점은 없습니다.

비활성 등록 토큰 삭제

기기에 메시지를 보내기 전에 기기의 등록 토큰 타임스탬프가 비활성 기간 내에 있는지 확인합니다. 예를 들어 Cloud Functions for Firebase를 구현하여 일일 검사를 실행하여 타임스탬프가 const EXPIRATION_TIME = 1000 * 60 * 60 * 24 * 30;와 같이 정의된 비활성 기간 내에 있는지 확인한 후 비활성 토큰을 삭제할 수 있습니다.

exports.pruneTokens = functions.pubsub.schedule('every 24 hours').onRun(async (context) => {
  // Get all documents where the timestamp exceeds is not within the past month
  const staleTokensResult = await admin.firestore().collection('fcmTokens')
      .where("timestamp", "<", Date.now() - EXPIRATION_TIME)
      .get();
  // Delete devices with stale tokens
  staleTokensResult.forEach(function(doc) { doc.ref.delete(); });
});

주제에서 비활성 토큰 구독 취소

주제를 사용하는 경우 구독 중인 주제에서 비활성 토큰을 등록 취소하는 것이 좋습니다. 이 과정은 두 단계로 이루어집니다.

  1. 앱에서는 한 달에 한 번 또는 등록 토큰이 변경될 때마다 주제를 다시 구독해야 합니다. 이에 따라 앱이 다시 활성화되면 구독이 자동으로 다시 표시되는 자가 복구 솔루션 형태를 띄게 됩니다.
  2. 앱 인스턴스가 1개월(또는 자체 비활성 기간) 동안 유휴 상태면 Firebase Admin SDK를 사용해 주제를 구독 취소하고 FCM 백엔드에서 토큰 및 주제 매핑을 삭제해야 합니다.

이러한 두 단계의 장점은 팬아웃할 비활성 토큰이 적기 때문에 팬아웃이 더 빨리 일어나고, 비활성 앱 인스턴스는 다시 활성 상태가 되었을 때 자동으로 재구독된다는 것입니다.

전송 성공 측정

메시지 전송을 가장 정확하게 파악하려면 활발히 사용되는 앱 인스턴스에 메시지를 보내는 것이 가장 좋습니다. 구독자가 많은 주제에 정기적으로 메시지를 보내는 경우 특히 그렇습니다. 이러한 구독자 중 일부가 실제로 비활성 상태인 경우 시간이 지날수록 전송 통계에 큰 영향을 줄 수 있습니다.

메시지를 토큰으로 타겟팅하기 전에 다음을 고려하세요.

  • Google 애널리틱스, BigQuery에서 캡처된 데이터 또는 기타 추적 신호에 토큰이 활성 상태임이 나타나나요?
  • 일정 기간 동안 이전 전송 시도가 지속적으로 실패했나요?
  • 지난 1개월 동안 서버에서 등록 토큰이 업데이트되었나요?
  • Android 기기에서 FCM Data APIdroppedDeviceInactive로 인한 높은 메시지 전송 실패율을 보고하나요?

전송에 대한 자세한 내용은 메시지 전송 이해를 참조하세요.