Android 13 이상 푸시 알림 권한(POST_NOTIFICATIONS) 대응 및 타이밍 설계 정리

앱 타겟 버전을 Android 13(API 33) 이상으로 올리면서 알림 기능 운영에 큰 변화가 생겼습니다. 예전에는 알림 채널만 만들어서 푸시를 보내면 사용자가 알아서 설정에서 끄는 방식이었는데, 이제는 POST_NOTIFICATIONS라는 런타임 권한을 직접 받아야 합니다.

새로 설치한 앱은 알림이 기본적으로 꺼져 있기 때문에, 예전 생각만 하고 개발하면 푸시가 아예 안 가는 상황이 발생합니다.

운영을 하다 보니 "코드를 어디에 넣을까"보다 "어느 타이밍에 권한을 요청할까"가 훨씬 중요하더군요. 앱 시작하자마자 아무 설명 없이 팝업부터 띄우면 사용자는 대부분 거부합니다. 게다가 한 번 거부당하면 다시 권한을 유도하기가 꽤 까다롭습니다. 나중에 제가 다시 보려고 실무 기준으로 핵심만 짧게 정리해 둡니다.

1. 핵심 개념: 헷갈리기 쉬운 3가지 요소 분리하기

개발하다 보면 FCM 토큰 발급, 알림 채널 생성, 런타임 권한 요청을 다 같은 하나로 묶어서 생각하기 쉬운데, 이거 확실히 분리해서 처리해야 운영 데이터가 안 꼬입니다.

항목필요한 버전역할 및 특징
POST_NOTIFICATIONSAndroid 13 이상앱이 기기에 일반 알림을 표시할 수 있는 최종 허가권입니다.
NotificationChannelAndroid 8.0 이상알림의 종류(배송, 채팅, 공지 등)와 중요도를 나누는 기준입니다.
FCM Token모든 버전Firebase 사용 시 기기를 식별하기 위한 고유 키값입니다.

주의: FCM 토큰이 정상적으로 발급되었고 서버로 전송 성공했어도, 기기에서 POST_NOTIFICATIONS 권한이 거부되어 있으면 사용자는 푸시 알림을 구경도 못 합니다. 두 상태를 매칭해서 서버에서 관리해야 비용과 리소스를 아낍니다.

2. Manifest 설정 및 권한 요청 예시 코드

먼저 AndroidManifest.xml에 권한을 선언해 줍니다.

XML
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

    <application>
        </application>
</manifest>

저의 경우 권한 요청 로직이 꼬이지 않도록 아래처럼 별도 헬퍼 클래스를 만들어서 관리합니다. targetSDK 33 이상에서 shouldShowRequestPermissionRationale를 활용하는 구조입니다.

Kotlin
class NotificationPermissionRequester(
    private val activity: ComponentActivity,
    private val onGranted: () -> Unit,
    private val onDenied: () -> Unit,
) {
    private val launcher = activity.registerForActivityResult(
        ActivityResultContracts.RequestPermission(),
    ) { granted ->
        if (granted) onGranted() else onDenied()
    }

    fun requestIfNeeded() {
        // Android 13 미만은 런타임 권한이 없으므로 바로 통과
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
            onGranted()
            return
        }

        val permission = Manifest.permission.POST_NOTIFICATIONS
        when {
            // 이미 권한이 있는 경우
            ContextCompat.checkSelfPermission(activity, permission) == 
                    PackageManager.PERMISSION_GRANTED -> {
                onGranted()
            }

            // 사용자가 이전에 거부한 적이 있는 경우 (설명 UI 필요)
            activity.shouldShowRequestPermissionRationale(permission) -> {
                showRationaleDialog(permission)
            }

            // 처음 요청하는 경우
            else -> {
                launcher.launch(permission)
            }
        }
    }

    private fun showRationaleDialog(permission: String) {
        AlertDialog.Builder(activity)
            .setTitle("알림 권한 안내")
            .setMessage("주문 배송 상태 및 중요 공지 알림을 받으시려면 알림 권한 허용이 필요합니다.")
            .setPositiveButton("계속") { _, _ -> launcher.launch(permission) }
            .setNegativeButton("나중에") { _, _ -> onDenied() }
            .show()
    }
}

3. 권한 요청 타이밍과 거부 시 Fallback 설계

가장 중요한 것은 사용자가 "이 알림이 나한테 필요하겠구나"라고 느끼는 맥락에서 팝업을 띄우는 것입니다.

추천하는 권한 요청 타이밍

  • 주문/예약 앱: 사용자가 결제를 끝내거나 예약을 완료한 직후 ("배송/예약 상태 알림을 보내드릴까요?")

  • 채팅/댓글 앱: 사용자가 특정 글에 댓글을 달거나 채팅방에 참여할 때

  • 설정 화면: 사용자가 직접 '마케팅 정보 알림 수신' 토글을 켤 때

사용자가 거부했을 때 (Fallback) 대책

권한을 거부당했다고 해서 서비스 이용을 막으면 이탈률만 높아집니다. 알림 외에 앱 안에서 확인할 수 있는 대체 UX를 반드시 열어줘야 합니다.

알림 종류권한 거부 시 대체 방안 (Fallback)
주문/예약 상태앱 메인 화면이나 마이페이지에 '진행 현황' 배너/인앱 인디케이터 노출
채팅 및 댓글앱 실행 시 하단 탭에 빨간 배지(badge)를 띄우거나 목록 새로고침 유도
마케팅/이벤트콘텐츠 이용은 그대로 유지하되, 나중에 설정 메뉴에서 켤 수 있도록 유도

4. 실무 테스트 시 체크리스트

실제 기기나 에뮬레이터에서 검증할 때 아래 시나리오별로 동작을 다 확인하셔야 운영 환경에서 빵꾸가 안 납니다.

  • Android 13 이상 신규 설치: 앱을 처음 켰을 때 권한 허용 전까지는 푸시가 실제로 안 오는지 확인

  • 사용자가 스와이프(뒤로가기)로 팝업 닫음: 권한 상태가 거부로 변하지 않고, 다음 적절한 시점에 다시 팝업을 띄울 수 있는지 확인

  • 기존 앱 업그레이드: Android 12 이하에서 이미 알림 채널을 쓰던 사용자가 13으로 업데이트했을 때 pre-grant(자동 허용) 조건이 맞는지 확인

  • 서버 연동 안전성: 테스트 중 로그캣(Logcat)이나 응답 값에 실제 FCM 토큰이나 개인 식별 데이터가 통째로 찍히지 않는지 확인 (보안 주의)

5. 초보자가 자주 하는 실수 요약

  1. 앱 시작하자마자 요청하기: 사용자는 어떤 알림이 오는지도 모른 채 90% 확률로 거절합니다.

  2. FCM 토큰과 권한 동일시하기: 서버에 토큰 발급 완료되었다고 무조건 알림 성공 처리하면 안 됩니다. 권한 상태값도 서버에 같이 인덱싱해 두는 편이 좋습니다.

  3. 거부 후 무한 팝업 강제하기: 계속 권한을 달라고 떼쓰는 앱은 사용자가 지워버립니다. 거부 후에는 깔끔하게 포기하고 앱 내 알림 배지나 설정 이동 링크로 유도하는 것이 자연스럽습니다.

앱을 오래 운영하다 보면 결국 기술적인 구현 자체보다 사용자 경험(UX)을 해치지 않는 흐름을 잡는 게 더 까다롭습니다. 런타임 권한 처리가 필요하신 분들은 위 흐름 참고하셔서 적용해 보시기 바랍니다.

댓글

이 블로그의 인기 게시물

안드로이드 화면 꺼짐 방지 FLAG_KEEP_SCREEN_ON 및 WakeLock 적용 방법

안드로이드 백그라운드 타이머 구현 및 Foreground Service 알림 설계 정리

관광 앱 다국어 데이터 모델 및 검색 색인 설계 (자꾸 까먹어서 정리)