Skip to main content

Headless JS

Headless JS는 앱이 백그라운드에 있을 때도 자바스크립트 작업을 실행할 수 있는 방법이다. 최신 데이터를 동기화하거나, 푸시 알림을 처리하거나, 음악을 재생하는 등의 용도로 활용할 수 있다.

JS API

태스크는 AppRegistry에 등록하는 비동기 함수다. React 애플리케이션을 등록하는 방식과 유사하다:

tsx
import {AppRegistry} from 'react-native';
AppRegistry.registerHeadlessTask('SomeTaskName', () =>
require('SomeTaskName'),
);

그리고 SomeTaskName.js 파일에서 다음과 같이 작성한다:

tsx
module.exports = async taskData => {
// 작업 수행
};

태스크 내에서는 UI를 건드리지 않는 한 네트워크 요청이나 타이머 등 무엇이든 할 수 있다. 태스크가 완료되면(Promise가 resolve되면), React Native는 "일시 정지" 모드로 전환된다(다른 태스크가 실행 중이거나 포그라운드 앱이 있는 경우는 제외).

플랫폼 API

여전히 약간의 네이티브 코드가 필요하지만, 매우 간결하다. HeadlessJsTaskService를 상속받고 getTaskConfig를 오버라이드하면 된다. 예를 들면 다음과 같다:

java
package com.your_application_name;

import android.content.Intent;
import android.os.Bundle;
import com.facebook.react.HeadlessJsTaskService;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.jstasks.HeadlessJsTaskConfig;
import javax.annotation.Nullable;

public class MyTaskService extends HeadlessJsTaskService {

@Override
protected @Nullable HeadlessJsTaskConfig getTaskConfig(Intent intent) {
Bundle extras = intent.getExtras();
if (extras != null) {
return new HeadlessJsTaskConfig(
"SomeTaskName",
Arguments.fromBundle(extras),
5000, // 태스크 타임아웃 (밀리초)
false // 선택사항: 포그라운드에서 태스크 허용 여부. 기본값은 false
);
}
return null;
}
}

그런 다음 AndroidManifest.xml 파일의 application 태그 안에 서비스를 추가한다:

xml
<service android:name="com.example.MyTaskService" />

이제 서비스를 시작할 때마다, 예를 들어 주기적 작업으로 또는 시스템 이벤트/브로드캐스트에 대한 응답으로, JS가 실행되고 태스크를 수행한 후 종료된다.

예제:

java
Intent service = new Intent(getApplicationContext(), MyTaskService.class);
Bundle bundle = new Bundle();

bundle.putString("foo", "bar");
service.putExtras(bundle);

getApplicationContext().startForegroundService(service);

재시도

기본적으로 헤드리스 JS 태스크는 재시도를 수행하지 않는다. 재시도를 구현하려면 HeadlessJsRetryPolicy를 생성하고 특정 Error를 던져야 한다.

LinearCountingRetryPolicyHeadlessJsRetryPolicy의 구현체로, 최대 재시도 횟수와 각 시도 간의 고정된 지연 시간을 설정할 수 있다. 이 정책이 필요에 맞지 않다면 직접 HeadlessJsRetryPolicy를 구현할 수 있다. 이러한 정책은 HeadlessJsTaskConfig 생성자에 추가 인자로 전달할 수 있다. 예를 들면 다음과 같다.

java
HeadlessJsRetryPolicy retryPolicy = new LinearCountingRetryPolicy(
3, // 최대 재시도 횟수
1000 // 각 재시도 간 지연 시간(밀리초)
);

return new HeadlessJsTaskConfig(
'SomeTaskName',
Arguments.fromBundle(extras),
5000,
false,
retryPolicy
);

재시도는 특정 Error가 발생했을 때만 시도된다. 헤드리스 JS 태스크 내부에서 해당 오류를 임포트하고, 재시도가 필요할 때 오류를 던질 수 있다.

예제:

tsx
import {HeadlessJsTaskError} from 'HeadlessJsTask';

module.exports = async taskData => {
const condition = ...;
if (!condition) {
throw new HeadlessJsTaskError();
}
};

모든 오류에 대해 재시도를 수행하려면, 오류를 캐치한 후 위의 오류를 던져야 한다.

주의사항

  • 기본적으로 앱이 포그라운드에서 실행 중일 때 태스크를 실행하려고 하면 앱이 강제 종료된다. 이는 개발자가 태스크에서 많은 작업을 수행해 UI를 느리게 만드는 실수를 방지하기 위한 조치다. 이 동작을 제어하려면 네 번째 boolean 인자를 전달할 수 있다.
  • BroadcastReceiver에서 서비스를 시작할 때는 onReceive()에서 반환하기 전에 반드시 HeadlessJsTaskService.acquireWakeLockNow()를 호출해야 한다.

예제 사용법

서비스는 Java API를 통해 시작할 수 있다. 먼저 서비스를 언제 시작할지 결정하고, 그에 맞는 솔루션을 구현해야 한다. 다음은 네트워크 연결 변경에 반응하는 예제이다.

아래 코드는 브로드캐스트 리시버를 등록하기 위한 Android 매니페스트 파일의 일부를 보여준다.

xml
<receiver android:name=".NetworkChangeReceiver" >
<intent-filter>
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
</intent-filter>
</receiver>

브로드캐스트 리시버는 onReceive 함수에서 브로드캐스트된 인텐트를 처리한다. 이곳은 앱이 포그라운드에 있는지 확인하기에 적합한 위치이다. 앱이 포그라운드에 있지 않다면, putExtra를 사용해 추가 정보를 포함하거나 포함하지 않고 인텐트를 준비할 수 있다. (bundle은 parcelable 값만 처리할 수 있음을 유의해야 한다.) 마지막으로 서비스를 시작하고 wakelock을 획득한다.

java
import android.app.ActivityManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
import android.os.Build;

import com.facebook.react.HeadlessJsTaskService;

public class NetworkChangeReceiver extends BroadcastReceiver {

@Override
public void onReceive(final Context context, final Intent intent) {
/**
이 부분은 네트워크 연결이 변경될 때마다 호출된다
예: 연결됨 -> 연결 안 됨
**/
if (!isAppOnForeground((context))) {
/**
서비스를 시작하고 네트워크 연결에 대한 추가 정보를 전송한다
**/
boolean hasInternet = isNetworkAvailable(context);
Intent serviceIntent = new Intent(context, MyTaskService.class);
serviceIntent.putExtra("hasInternet", hasInternet);
context.startForegroundService(serviceIntent);
HeadlessJsTaskService.acquireWakeLockNow(context);
}
}

private boolean isAppOnForeground(Context context) {
/**
앱이 포그라운드에 있는지 확인해야 한다. 그렇지 않으면 앱이 크래시될 수 있다.
https://stackoverflow.com/questions/8489993/check-android-application-is-in-foreground-or-not
**/
ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
List<ActivityManager.RunningAppProcessInfo> appProcesses =
activityManager.getRunningAppProcesses();
if (appProcesses == null) {
return false;
}
final String packageName = context.getPackageName();
for (ActivityManager.RunningAppProcessInfo appProcess : appProcesses) {
if (appProcess.importance ==
ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND &&
appProcess.processName.equals(packageName)) {
return true;
}
}
return false;
}

public static boolean isNetworkAvailable(Context context) {
ConnectivityManager cm = (ConnectivityManager)
context.getSystemService(Context.CONNECTIVITY_SERVICE);

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Network networkCapabilities = cm.getActiveNetwork();

if(networkCapabilities == null) {
return false;
}

NetworkCapabilities actNw = cm.getNetworkCapabilities(networkCapabilities);

if(actNw == null) {
return false;
}

if(actNw.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) || actNw.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) || actNw.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) {
return true;
}

return false;
}

// API 레벨 29에서 deprecated 됨
NetworkInfo netInfo = cm.getActiveNetworkInfo();
return (netInfo != null && netInfo.isConnected());
}
}