[React Native] android foreground notification 안드로이드 포그라운드 노티피케이션 만들기

김방토·2024년 3월 27일
1

React Native

목록 보기
1/7

스포일러

이 글에서는 리액트 네이티브로 작성된 어플에서 실행 시작과 동시에 나타나 스와이프로 삭제 전까지 살아있는 노티피케이션이 완성된다.

결론부터 말씀드리자면

안타까운 소식이지만 리액트 네이티브 only로는 불가능한 기능이다.
커뮤니티 라이브러리를 사용해도 좋지만, 이 글에서는 라이브러리 없이 네이티브로 구현한다.
(네이티브 모듈을 만들어서 리액트 네이티브 프로젝트에 끼워넣는다)

들어가기 전에

저는 네이티브에 대해 정확한 지식 없이 리액트 네이티브로 무작정 시작했습니다.
따라하면 어찌저찌 완성은 되겠지만 정확한 설명이 아닐 수 있습니다.
(네이티브 안 배워도 크로스플랫폼이 다 해주는 줄 알았습니다...)

개발환경

안드로이드 스튜디오
React Native CLI
Android API 34
Java(새로 추가한 부분)
Kotlin(기존 RN CLI로 자동생성된 부분)

코틀린을 몰라서, 새로 작성해야 하는 부분은 자바로 작성했다.
자바-코틀린 호환 잘 되니 걱정하지 마시라.

구현해보기

기본 알림 만들기 - 안드로이드 공식문서를 따라갑니다.

Foreground(포그라운드)란?

포그라운드 서비스는 사용자가 인지할 수 있는 작업을 실행합니다.
포그라운드 서비스는 상태 표시줄 알림을 표시하여 앱이 포그라운드에서 작업을 실행하고 시스템 리소스를 소비하고 있음을 사용자에게 알립니다.

포그라운드 서비스 - 안드로이드 공식문서

백그라운드 vs 포그라운드

백그라운드는 사용자가 알지 못하는 서비스. 눈에 보이지 않는 상태에서도 계속 할 일을 한다.
포그라운드는 사용자가 알도록 눈에 보이는 서비스. 눈에 보이는 상태에서 할 일을 한다.

이 정도로 생각하면 된다. 예를 들면 어플이 화면에서 내려가도 계속 음악을 실행하고(상단바/알림센터에 눈에 보이는 재생 바가 있고) 있는 음악 어플같은 경우가 포그라운드 서비스의 대표적인 예라고 할 수 있겠다.

꼭 눈에 보이게 해야하나요?

백그라운드에서 야금야금 실행되고 있다면, 사용자는 이 어플이 내 핸드폰을 얼마나 채찍질 하고 있는지 알 수 없다. 그래서 안드로이드는 일정 버전 이상부터 리소스를 많이 잡아먹는 서비스들을 꼭 사용자가 알 수 있도록 하고 있는데,(사용자가 모르면 중단시킨다...!) 이것이 바로 포그라운드 서비스의 노티피케이션이다.
쉽게 말해서 사용자에게 나 뫄뫄 어플인데, 이거 하고있는 중! 하고 알려주는 기능을 겸한다. 그리고 사용자는 엥 그거 필요 없는데? 하면 알림 센터에서 옆으로 쓱 밀어 해당 서비스를 중지시킬 수 있다. 여러분이 어플 광고 밀어서 지우는 것 처럼.

ForegroundService.java

나처럼 타입스크립트/자바스크립트에 오랜시간 익숙해진 사람이라면(나는 이전에 공부했던 백엔드 서비스도 Nest JS로 처리했기 때문에 더) 난데없는 자바에 알러지가 올라올 수 있다. 어쩔 수 없다. 그냥 알러지 벅벅 긁으면서 진행하면 된다(...). 다 나의 소중한 지식이 될 것이다...

안드로이드 패키지의 MainActivity, MainApplication이 있는 위치를 찾아 그 폴더에 작성했다.
부분부분 떼어 설명하고, 전체 코드는 마지막에 쭉 써놓도록 한다.

public class ForegroundService extends Service {
    @Override
    public void onCreate() {
        super.onCreate();
    }
    
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
    
	// 코드 들어갈 부분
}

새 클래스를 하나 만들어준다. 안드로이드의 모든 서비스는 Service(android.app.Service)를 상속받는다.

private static final int NOTIFICATION_ID = 1;
private static final String NOTIFICATION_NAME = "Foreground Service Channel Name";
private static final String CHANNEL_ID = "foreground_service_channel_id";

노티피케이션 아이디와 채널 아이디, 채널 이름을 임의로 지정해준다. 이 글에서는 이렇게 각 하나씩만 가지고 진행한다.

private void createNotificationChannel() {
        if(Build.VERSION_CODES.O <= Build.VERSION.SDK_INT) {
        	// 여기에 채널 생성 코드 작성
        }
    }

채널을 생성한다.
안드로이드 버전 오레오(8.0)부터는 채널을 생성하지 않으면 노티피케이션을 띄울 수 없다.

NotificationChannel notiChannel = new NotificationChannel(
	CHANNEL_ID,
	NOTIFICATION_NAME,
	NotificationManager.IMPORTANCE_DEFAULT
);

NotificationManager notificationManager = getSystemService(NotificationManager.class);

notificationManager.createNotificationChannel(notiChannel);

노티피케이션 채널을 설정해준다.
NotificationChannel 생성자는 채널 아이디, 노티피케이션 이름, 중요도를 인자로 받는다.
중요도는 중요도 수준 - 안드로이드 공식 문서를 참고하시라.

@Override
public int onStartCommand(Intent intent, int flag, int startId) {

	// 여기에 코드 작성

	return START_STICKY;
}

onStartCommand를 오버라이딩한다.

반환 값으로 START_STICKY, START_NOT_STICKY, START_REDELIVER_INTENT가 있는데, 간단하게 정리하자면 이렇다.

  • START_STICKY
    서비스가 강제로 종료되었을 때 서비스 다시 시작 - 계속 실행되어야 할때 사용
  • START_NOT_STICKY
    서비스가 강제로 종료되었을 때 다시 시작하지 않음 - 일회성 작업에 사용
  • START_REDELIVER_INTENT
    서비스가 강제로 종료되면 시스템이 서비스를 다시 시작시키고 이전에 전달된 Intent를 다시 전달 - 작업을 완료하기 전에 중단될 수 있는 서비스에 사용
createNotificationChannel();

Notification noti = new NotificationCompat.Builder(this, CHANNEL_ID)
                	.setContentTitle("히히 노티피케이션 띄웠다")
                	.setContentText("너무 힘들어")
                	.setSmallIcon(R.mipmap.ic_launcher)
                	.build();

if(Build.VERSION_CODES.O <= Build.VERSION.SDK_INT) {
	startForeground(NOTIFICATION_ID, noti);
}

노티피케이션 세부 내용을 설정한다. (title, text, small icon 등)
설정한 노티피케이션으로 포그라운드 서비스를 실행한다.

전체코드

package com.프로젝트명;

import android.annotation.SuppressLint;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Intent;
import android.content.pm.ServiceInfo;
import android.os.Build;
import android.os.IBinder;

import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;

public class ForegroundService extends Service {

    private static final int NOTIFICATION_ID = 1;
    private static final String NOTIFICATION_NAME = "Foreground Service Channel Name";
    private static final String CHANNEL_ID = "foreground_service_channel_id";

    @Override
    public void onCreate() {
        super.onCreate();
    }

    @SuppressLint("ObsoleteSdkInt")
    private void createNotificationChannel() {
        if(Build.VERSION_CODES.O <= Build.VERSION.SDK_INT) {
            NotificationChannel notiChannel = new NotificationChannel(
                    CHANNEL_ID,
                    NOTIFICATION_NAME,
                    NotificationManager.IMPORTANCE_DEFAULT
            );

            NotificationManager notificationManager = getSystemService(NotificationManager.class);
            notificationManager.createNotificationChannel(notiChannel);
        }
    }

    @SuppressLint("ObsoleteSdkInt")
    @Override
    public int onStartCommand(Intent intent, int flag, int startId) {
        createNotificationChannel();

        Notification noti = new NotificationCompat.Builder(this, CHANNEL_ID)
                .setContentTitle("히히 노티피케이션 띄웠다")
                .setContentText("너무 힘들어")
                .setSmallIcon(R.mipmap.ic_launcher)
                .build();

        if(Build.VERSION_CODES.O <= Build.VERSION.SDK_INT) {
            startForeground(NOTIFICATION_ID, noti);
        }

        return START_STICKY;
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
}

채널 아이디 vs 노티피케이션 아이디 vs 노티피케이션 이름

  • 노티피케이션 채널 아이디 (Channel ID):

-노티피케이션 채널은 Android 8.0 (API 레벨 26)부터 도입된 개념으로, 사용자에게 표시되는 각각의 노티피케이션 그룹을 정의
-채널 아이디는 노티피케이션 채널을 식별하는 데 사용
-사용자는 채널마다 독립적으로 설정을 변경할 수 있음 (특정 채널의 알림을 끌거나 중요도를 변경할 수 있음)
-앱에서 새로운 노티피케이션을 생성할 때마다 해당 노티피케이션에 사용할 채널 아이디를 지정

  • 노티피케이션 아이디 (Notification ID):

-노티피케이션 아이디는 각각의 노티피케이션 인스턴스를 구별
-동일한 채널에 여러 개의 노티피케이션을 보낼 때, 각각의 노티피케이션은 고유한 아이디를 가져야 함
-노티피케이션 아이디를 사용하여 노티피케이션을 업데이트하거나 삭제할 수 있음

  • 노티피케이션 이름 (Notification Name):

-노티피케이션에 표시되는 제목
-사용자에게 알림의 내용을 간략히 설명하는데 사용
-노티피케이션의 컨텐츠를 나타내는 제목이나 요약

AndroidManifest.xml

매니페스트는 앱의 뼈대를 담당하는 부분이다. 어플이 요구하는 권한이나 어떤 기능(서비스)이 들어가는지 등이 들어있는 중요한 부분이다.

우리가 알려줘야 할 것은 1. 알림 권한이 필요하다는 것 2. 우리가 만든 서비스를 쓸거라는 것이다.
아래와 같이 추가(user-permission은 꼭 manaifest 안에, service는 꼭 application 안에 넣어주기)한다.

<manifest xmlns:android="http://schemas.android.com/apk/res/android">

  	// 권한 추가 시작
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
	// 권한 추가 끝
  
    <application
      android:name=".MainApplication"
      android:label="@string/app_name"
      android:icon="@mipmap/ic_launcher"
      android:roundIcon="@mipmap/ic_launcher_round"
      android:allowBackup="false"
      android:theme="@style/AppTheme">
      <activity
        android:name=".MainActivity"
        android:label="@string/app_name"
        android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
        android:launchMode="singleTask"
        android:windowSoftInputMode="adjustResize"
        android:exported="true">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
      </activity>
      
      // 서비스 추가 시작
      <service
          android:name=".ForegroundService"
          android:foregroundServiceType="specialUse" />
      // 서비스 추가 끝
      
    </application>
</manifest>

MainApplication.kt

프로젝트가 시작되는 메인 부분이다. 자바의 Main과 같은 부분이라고 생각하면 된다.
어허! 버릇없게. Main부터 말해야지!

우리가 만들어준 포그라운드 서비스를 실행시켜주면 된다.


class MainApplication : Application(), ReactApplication {

  ...

  @RequiresApi(Build.VERSION_CODES.O)
  override fun onCreate() {
    ...

	//  포그라운드 서비스 실행 시작
    val serviceIntent = Intent(this, ForegroundService::class.java)
    startForegroundService(serviceIntent)
    // 포그라운드 서비스 실행 끝
  }
}

왜인지 너무나 험난한 과정이었다.
당연함... 네이티브 공부를 해야했기 때문이다...
혹시 나처럼 RN 프로젝트에 네이티브 기능을 추가하고싶은 누군가가 있다면 꼬옥 봐주길 바라며.

profile
🍅 준비된 안드로이드 개발자 📱

0개의 댓글