안드로이드 알림을 사용자가 못 지우게 막는 법: setOngoing과 포그라운드 서비스 완벽 정리

Prompt Architect 편집팀 · 2026-06-18 · 7분

TL;DR — 사용자가 스와이프로 알림을 지우지 못하게 막는 setOngoing(true) 사용법과, 실무에서 진짜로 필요한 포그라운드 서비스 연동까지 한 번에 정리했습니다.

백그라운드에서 음악을 재생하거나 위치를 추적하거나 파일을 업로드하는 앱을 만들다 보면, "이 작업이 진행 중이라는 알림을 사용자가 실수로 스와이프해서 지워버리면 안 되는데"라는 상황을 만나게 됩니다. 안드로이드에서는 이걸 막는 표준 방법이 마련되어 있습니다. 바로 NotificationCompat.BuildersetOngoing(true)입니다.

이 글에서는 단순히 메서드 하나를 소개하는 데 그치지 않고, 실무에서 이 옵션이 어떻게 동작하는지, 왜 포그라운드 서비스와 함께 써야 하는지, 그리고 흔히 빠지는 함정까지 단계별로 정리합니다.

1. 어떤 문제를 풀려는 것인가

기본 알림은 사용자가 옆으로 밀면(swipe) 즉시 사라집니다. 일회성 메시지라면 당연히 그래야 합니다. 하지만 다음과 같은 작업은 "진행 중"이라는 상태를 계속 보여줘야 합니다.

  • 음악/팟캐스트 재생 컨트롤
  • 내비게이션 경로 안내
  • 대용량 파일 다운로드·업로드 진행률
  • 운동 기록·위치 추적 같은 장시간 백그라운드 작업

이런 알림이 임의로 사라지면 사용자는 작업이 멈췄다고 오해하거나, 컨트롤(일시정지 버튼 등)에 접근할 수 없게 됩니다. 그래서 "지울 수 없는 알림"이 필요합니다.

2. setOngoing의 동작 원리

setOngoing(true)를 호출하면 해당 알림은 진행 중(ongoing) 알림으로 표시됩니다. 이 플래그가 켜지면 다음과 같이 동작합니다.

  • 사용자가 스와이프해도 알림이 삭제되지 않습니다.
  • "모두 지우기(Clear all)" 버튼을 눌러도 남아 있습니다.
  • 알림 목록에서 일반 알림보다 위쪽, 진행 중 섹션에 묶여 표시됩니다.

내부적으로는 알림의 flagsFLAG_ONGOING_EVENT가 추가되는 것과 같은 효과입니다. 다만 한 가지 분명히 알아둘 점이 있습니다. setOngoing(true)만으로는 앱이 죽으면 알림도 함께 사라질 수 있다는 점입니다. 시스템이 메모리 확보를 위해 프로세스를 정리하면, 단순 알림은 운명을 같이합니다. 진짜로 작업이 끝날 때까지 살아 있어야 한다면 5번 섹션의 포그라운드 서비스가 필요합니다.

3. 알림 채널부터 만들기 (Android 8.0+ 필수)

Android 8.0(API 26, Oreo) 이상에서는 채널 없이 알림을 띄우면 아예 표시되지 않습니다. 그래서 빌더를 만들기 전에 채널을 먼저 등록해야 합니다.

// Kotlin — 알림 채널 등록 (앱 시작 시 1회만 호출하면 됨)
private fun ensureNotificationChannel(context: Context) {
    // Oreo 미만 기기는 채널 개념이 없으므로 건너뜀
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return

    val channel = NotificationChannel(
        "task_progress_channel",          // 채널 ID (빌더에서 그대로 사용)
        "작업 진행 상태",                  // 설정 화면에 노출될 이름
        NotificationManager.IMPORTANCE_LOW // 소리 없이 조용히 표시
    ).apply {
        description = "진행 중인 백그라운드 작업을 알려줍니다."
    }

    val manager = context.getSystemService(NotificationManager::class.java)
    manager.createNotificationChannel(channel)
}

진행 중 알림은 자주 갱신되므로 소리가 매번 나면 매우 거슬립니다. 그래서 채널 중요도를 IMPORTANCE_LOW로 두어 조용히 표시되도록 하는 것이 일반적입니다.

4. 단계별 구현 코드

이제 본체인 빌더를 작성합니다. 원본 개념을 그대로 살리되, 변수명과 구성을 실무에 맞게 다시 짰습니다.

// Kotlin — 지울 수 없는 진행 중 알림 만들기
fun showOngoingNotification(context: Context, appName: String) {
    ensureNotificationChannel(context) // 3번에서 만든 채널 보장

    val notification = NotificationCompat.Builder(context, "task_progress_channel")
        .setContentTitle(appName)                       // 알림 제목
        .setContentText("$appName 작업을 처리하고 있습니다.") // 본문 설명
        .setSmallIcon(R.mipmap.ic_launcher)             // 상태바 아이콘 (필수)
        .setPriority(NotificationCompat.PRIORITY_LOW)    // Oreo 미만 기기용 우선순위
        .setOngoing(true)                               // ★ 사용자가 지우지 못하게 막는 핵심
        .build()

    val manager = NotificationManagerCompat.from(context)
    manager.notify(1001, notification) // 1001은 이 알림의 고유 ID
}

자바를 쓰는 프로젝트라면 동일한 로직을 다음처럼 작성합니다.

// Java — 동일 기능
NotificationCompat.Builder builder =
        new NotificationCompat.Builder(context, "task_progress_channel");
builder.setContentTitle(appName);                          // 제목
builder.setContentText(appName + " 작업을 처리하고 있습니다."); // 본문
builder.setSmallIcon(R.mipmap.ic_launcher);               // 아이콘
builder.setPriority(NotificationCompat.PRIORITY_LOW);      // 우선순위
builder.setOngoing(true);                                  // 삭제 방지 플래그

NotificationManagerCompat.from(context).notify(1001, builder.build());

핵심은 setOngoing(true) 한 줄이지만, setSmallIcon이 없으면 알림 자체가 표시되지 않으니 빠뜨리지 않도록 주의합니다.

5. 포그라운드 서비스와 함께 쓰기 (실무의 정석)

앞서 말했듯 setOngoing(true)만으로는 프로세스가 살아 있는 동안만 알림이 유지됩니다. 음악 재생이나 위치 추적처럼 화면을 벗어나도 작업이 계속돼야 한다면 **포그라운드 서비스(Foreground Service)**로 띄워야 합니다. 포그라운드 서비스는 알림을 강제로 동반하며, 그 알림은 본질적으로 ongoing 상태가 됩니다.

// Kotlin — 서비스 안에서 포그라운드로 승격
class UploadService : Service() {
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        ensureNotificationChannel(this)

        val notification = NotificationCompat.Builder(this, "task_progress_channel")
            .setContentTitle("파일 업로드 중")
            .setContentText("잠시만 기다려 주세요.")
            .setSmallIcon(R.mipmap.ic_launcher)
            .setOngoing(true)
            .build()

        // 서비스를 포그라운드로 올리면 알림이 시스템 차원에서 유지됨
        startForeground(1001, notification)

        return START_STICKY // 시스템이 종료해도 가능하면 재시작
    }

    override fun onBind(intent: Intent?): IBinder? = null
}

Android 13(API 33)부터는 POST_NOTIFICATIONS 런타임 권한을, Android 14(API 34)부터는 매니페스트에 포그라운드 서비스 타입(foregroundServiceType)을 명시해야 합니다. 이 부분을 놓치면 최신 기기에서 서비스가 곧바로 크래시 납니다.

6. 흔한 실수와 엣지케이스

  • 채널 누락: Oreo 이상에서 채널 없이 호출하면 알림이 조용히 무시됩니다. 로그에도 명확히 안 찍혀 디버깅이 까다롭습니다.
  • smallIcon 누락: 작은 아이콘이 없으면 알림이 표시되지 않습니다. 가장 흔한 "안 떠요" 원인입니다.
  • 작업 종료 후 방치: 작업이 끝났는데 ongoing 알림을 안 지우면 사용자는 영영 못 지우는 좀비 알림에 갇힙니다. 완료 시 반드시 stopForeground(STOP_FOREGROUND_REMOVE) 또는 cancel(1001)로 정리하세요.
  • 권한 미요청: Android 13+에서 POST_NOTIFICATIONS를 안 받으면 알림이 아예 안 보입니다.
  • 남용 금지: ongoing 알림은 사용자 통제권을 빼앗는 강한 수단입니다. 실제로 진행 중인 작업이 없는데 광고성으로 쓰면 사용자가 앱을 지웁니다. 정당한 사용 사례에만 쓰세요.

7. 요약

  • setOngoing(true)는 사용자가 스와이프나 "모두 지우기"로 알림을 삭제하지 못하게 막는 플래그입니다.
  • Android 8.0 이상에서는 반드시 알림 채널을 먼저 등록해야 합니다.
  • 화면을 벗어나도 유지돼야 한다면 단순 알림이 아니라 포그라운드 서비스로 띄워야 합니다.
  • 작업이 끝나면 알림을 직접 제거해 좀비 알림을 남기지 마세요.

AI에게 물어볼 때 (프롬프트 팁)

이 주제를 ChatGPT나 Claude에게 물어볼 때, 막연히 "안드로이드 알림 안 지워지게 해줘"라고 하면 버전별 함정을 놓친 답을 받기 쉽습니다. 다음처럼 맥락을 구체화하면 훨씬 정확한 코드를 얻습니다.

  • "Android 14, Kotlin, minSdk 26 환경이다. 업로드가 끝날 때까지 사용자가 못 지우는 진행 중 알림을 포그라운드 서비스로 띄우고 싶다. 필요한 매니페스트 권한과 foregroundServiceType, POST_NOTIFICATIONS 런타임 권한 처리까지 포함해서 전체 예제를 보여줘."
  • "아래 NotificationCompat.Builder 코드에서 알림이 표시되지 않는 원인을 체크리스트로 진단해줘. (코드 첨부) 채널, smallIcon, 권한, 타깃 SDK 관점에서 빠진 게 없는지 짚어줘."
  • "setOngoing(true)와 startForeground()의 차이를 표로 정리하고, 각각 알림이 사라지는 조건이 어떻게 다른지 설명해줘."

이렇게 OS 버전, 언어, SDK 레벨, 사용 시나리오를 명시하는 것이 좋은 프롬프트의 핵심입니다. 더 정교한 프롬프트 설계가 궁금하다면 Prompt Architect의 프롬프트 분석기로 여러분의 질문을 점검해 보세요.