이 글에서는 리액트 네이티브로 작성된 어플에서 실행 시작과 동시에 나타나 스와이프로 삭제 전까지 살아있는 노티피케이션이 완성된다.
안타까운 소식이지만 리액트 네이티브 only로는 불가능한 기능이다.
커뮤니티 라이브러리를 사용해도 좋지만, 이 글에서는 라이브러리 없이 네이티브로 구현한다.
(네이티브 모듈을 만들어서 리액트 네이티브 프로젝트에 끼워넣는다)
저는 네이티브에 대해 정확한 지식 없이 리액트 네이티브로 무작정 시작했습니다.
따라하면 어찌저찌 완성은 되겠지만 정확한 설명이 아닐 수 있습니다.
(네이티브 안 배워도 크로스플랫폼이 다 해주는 줄 알았습니다...)
안드로이드 스튜디오
React Native CLI
Android API 34
Java(새로 추가한 부분)
Kotlin(기존 RN CLI로 자동생성된 부분)
코틀린을 몰라서, 새로 작성해야 하는 부분은 자바로 작성했다.
자바-코틀린 호환 잘 되니 걱정하지 마시라.
기본 알림 만들기 - 안드로이드 공식문서를 따라갑니다.
포그라운드 서비스는 사용자가 인지할 수 있는 작업을 실행합니다.
포그라운드 서비스는 상태 표시줄 알림을 표시하여 앱이 포그라운드에서 작업을 실행하고 시스템 리소스를 소비하고 있음을 사용자에게 알립니다.
백그라운드는 사용자가 알지 못하는 서비스. 눈에 보이지 않는 상태에서도 계속 할 일을 한다.
포그라운드는 사용자가 알도록 눈에 보이는 서비스. 눈에 보이는 상태에서 할 일을 한다.
이 정도로 생각하면 된다. 예를 들면 어플이 화면에서 내려가도 계속 음악을 실행하고(상단바/알림센터에 눈에 보이는 재생 바가 있고) 있는 음악 어플같은 경우가 포그라운드 서비스의 대표적인 예라고 할 수 있겠다.
백그라운드에서 야금야금 실행되고 있다면, 사용자는 이 어플이 내 핸드폰을 얼마나 채찍질 하고 있는지 알 수 없다. 그래서 안드로이드는 일정 버전 이상부터 리소스를 많이 잡아먹는 서비스들을 꼭 사용자가 알 수 있도록 하고 있는데,(사용자가 모르면 중단시킨다...!) 이것이 바로 포그라운드 서비스의 노티피케이션이다.
쉽게 말해서 사용자에게 나 뫄뫄 어플인데, 이거 하고있는 중! 하고 알려주는 기능을 겸한다. 그리고 사용자는 엥 그거 필요 없는데? 하면 알림 센터에서 옆으로 쓱 밀어 해당 서비스를 중지시킬 수 있다. 여러분이 어플 광고 밀어서 지우는 것 처럼.
나처럼 타입스크립트/자바스크립트에 오랜시간 익숙해진 사람이라면(나는 이전에 공부했던 백엔드 서비스도 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가 있는데, 간단하게 정리하자면 이렇다.
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;
}
}
-노티피케이션 채널은 Android 8.0 (API 레벨 26)부터 도입된 개념으로, 사용자에게 표시되는 각각의 노티피케이션 그룹을 정의
-채널 아이디는 노티피케이션 채널을 식별하는 데 사용
-사용자는 채널마다 독립적으로 설정을 변경할 수 있음 (특정 채널의 알림을 끌거나 중요도를 변경할 수 있음)
-앱에서 새로운 노티피케이션을 생성할 때마다 해당 노티피케이션에 사용할 채널 아이디를 지정
-노티피케이션 아이디는 각각의 노티피케이션 인스턴스를 구별
-동일한 채널에 여러 개의 노티피케이션을 보낼 때, 각각의 노티피케이션은 고유한 아이디를 가져야 함
-노티피케이션 아이디를 사용하여 노티피케이션을 업데이트하거나 삭제할 수 있음
-노티피케이션에 표시되는 제목
-사용자에게 알림의 내용을 간략히 설명하는데 사용
-노티피케이션의 컨텐츠를 나타내는 제목이나 요약
매니페스트는 앱의 뼈대를 담당하는 부분이다. 어플이 요구하는 권한이나 어떤 기능(서비스)이 들어가는지 등이 들어있는 중요한 부분이다.
우리가 알려줘야 할 것은 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>
프로젝트가 시작되는 메인 부분이다. 자바의 Main과 같은 부분이라고 생각하면 된다.
어허! 버릇없게. Main부터 말해야지!
우리가 만들어준 포그라운드 서비스를 실행시켜주면 된다.
class MainApplication : Application(), ReactApplication {
...
@RequiresApi(Build.VERSION_CODES.O)
override fun onCreate() {
...
// 포그라운드 서비스 실행 시작
val serviceIntent = Intent(this, ForegroundService::class.java)
startForegroundService(serviceIntent)
// 포그라운드 서비스 실행 끝
}
}
왜인지 너무나 험난한 과정이었다.
당연함... 네이티브 공부를 해야했기 때문이다...
혹시 나처럼 RN 프로젝트에 네이티브 기능을 추가하고싶은 누군가가 있다면 꼬옥 봐주길 바라며.