RNWW Plugin Background
React Native WebView 백그라운드 실행 플러그인 (Expo Native Module)
Headless WebView를 통해 앱 라이프사이클 이벤트 발생 시 JavaScript 콜백을 실행합니다.
설치
npm install rnww-plugin-background
빠른 시작
import { registerBackgroundHandlers } from 'rnww-plugin-background';
registerBackgroundHandlers({
bridge: yourBridgeImplementation,
platform: { OS: Platform.OS },
});
핵심 개념
이 플러그인은 Headless WebView에 JavaScript 콜백을 주입하여 앱 라이프사이클 이벤트 발생 시 실행합니다.
bridge.call('registerTask', {
taskId: 'my-task',
triggers: ['app_foreground', 'app_background'],
callback: (trigger, data) => {
if (trigger === 'app_background') {
saveCurrentState();
} else if (trigger === 'app_foreground') {
syncData();
}
},
notification: {
title: '백그라운드 실행 중',
body: '서비스가 실행 중입니다'
}
});
Bridge Handlers
registerTask
백그라운드 작업을 등록합니다.
const result = await bridge.call('registerTask', {
taskId: string;
triggers?: TriggerType[];
callback?: BackgroundCallback;
onEvent?: TaskCallback;
onTerminate?: TaskCallback;
callbackId?: string;
notification?: NotificationConfig;
});
unregisterTask
등록된 작업을 해제합니다.
const result = await bridge.call('unregisterTask', { taskId: 'my-task' });
startTask
작업을 시작합니다. Android에서는 Foreground Service가 시작됩니다.
const result = await bridge.call('startTask', { taskId: 'my-task' });
stopTask
작업을 중지합니다.
const result = await bridge.call('stopTask', { taskId: 'my-task' });
stopAllTasks
모든 작업을 중지합니다.
const result = await bridge.call('stopAllTasks');
updateNotification
실행 중인 알림 내용을 업데이트합니다. (Android only)
const result = await bridge.call('updateNotification', {
taskId: 'my-task',
title: '동기화 중',
body: '작업 진행 중...',
silent: true
});
getTaskStatus
특정 작업의 상태를 조회합니다.
const result = await bridge.call('getTaskStatus', { taskId: 'my-task' });
getAllTasksStatus
모든 작업의 상태를 조회합니다.
const result = await bridge.call('getAllTasksStatus');
checkBackgroundPermission
백그라운드 권한 상태를 확인합니다.
const result = await bridge.call('checkBackgroundPermission');
requestBackgroundPermission
백그라운드 권한을 요청합니다. (배터리 최적화 예외)
const result = await bridge.call('requestBackgroundPermission');
checkNotificationPermission
알림 권한을 확인합니다. (Android 13+ 필수)
const result = await bridge.call('checkNotificationPermission');
requestNotificationPermission
알림 권한을 요청합니다.
const result = await bridge.call('requestNotificationPermission');
setTaskData
작업별 데이터를 저장합니다. (SharedPreferences/UserDefaults)
const result = await bridge.call('setTaskData', {
taskId: 'my-task',
data: { lastSync: Date.now(), count: 5 }
});
getTaskData
작업별 데이터를 조회합니다.
const result = await bridge.call('getTaskData', { taskId: 'my-task' });
removeTaskData
작업별 데이터를 삭제합니다.
const result = await bridge.call('removeTaskData', { taskId: 'my-task' });
disposeBackgroundHandlers
브릿지 핸들러와 리소스를 정리합니다.
await bridge.call('disposeBackgroundHandlers');
트리거 시스템
지원 트리거
app_foreground | 앱이 포그라운드로 전환 |
app_background | 앱이 백그라운드로 전환 |
사용 예시
bridge.call('registerTask', {
taskId: 'lifecycle-task',
triggers: ['app_foreground', 'app_background'],
callback: (trigger, data) => {
console.log(`트리거: ${trigger}, 시간: ${data.timestamp}`);
if (trigger === 'app_background') {
saveCurrentState();
} else if (trigger === 'app_foreground') {
syncData();
}
},
notification: { title: 'Service', body: 'Running...' }
});
콜백 시스템
callback (WebView 콜백)
callback은 Headless WebView 내에서 실행됩니다.
callback: (trigger: TriggerType, data: TriggerData) => void
중요 제한사항: 클로저 사용 불가
const API_URL = 'https://api.example.com';
callback: (trigger, data) => {
fetch(API_URL);
}
callback: (trigger, data) => {
fetch('https://api.example.com');
}
onEvent (네이티브 이벤트)
onEvent는 브릿지 레벨에서 네이티브 이벤트를 수신합니다.
bridge.call('registerTask', {
taskId: 'my-task',
onEvent: (event) => {
switch (event.type) {
case 'started':
console.log('작업 시작됨');
break;
case 'stopped':
console.log('작업 중지됨');
break;
case 'trigger':
console.log(`트리거 발생: ${event.trigger}`);
break;
case 'terminated':
console.log('비정상 종료 감지');
break;
}
},
});
onTerminate (종료 콜백)
서비스 종료 직전에 실행됩니다.
bridge.call('registerTask', {
taskId: 'my-task',
onTerminate: async (event) => {
await bridge.call('setTaskData', {
taskId: event.taskId,
data: { terminatedAt: Date.now() }
});
},
});
주의: 프로세스가 즉시 kill되면 호출되지 않을 수 있습니다. 비정상 종료는 다음 앱 실행 시 terminated 이벤트로 감지됩니다.
callbackId
여러 작업의 이벤트를 구분하기 위한 식별자입니다.
bridge.call('registerTask', {
taskId: 'task-1',
callbackId: 'sync-callback',
});
bridge.on('onTaskEvent', (event) => {
if (event.callbackId === 'sync-callback') {
}
});
알림 설정 (Android)
NotificationConfig
interface NotificationConfig {
taskId?: string;
title: string;
body: string;
icon?: string;
color?: string;
priority?: 'min' | 'low' | 'default' | 'high' | 'max';
ongoing?: boolean;
silent?: boolean;
channelId?: string;
channelName?: string;
channelDescription?: string;
}
예시
notification: {
title: '백그라운드 서비스',
body: '실행 중입니다',
icon: 'ic_notification',
color: '#4CAF50',
priority: 'low',
ongoing: true,
silent: false,
channelId: 'my_channel',
channelName: 'Background Service'
}
이벤트
TaskEvent 구조
interface TaskEvent {
taskId: string;
type: 'started' | 'stopped' | 'trigger' | 'terminating' | 'terminated';
trigger?: TriggerType;
reason?: 'unexpected';
lastStartedAt?: number;
timestamp: number;
callbackId?: string;
}
이벤트 타입
started | 작업 시작됨 |
stopped | 작업 중지됨 |
trigger | 트리거 발생 (WebView callback 호출됨) |
terminating | 서비스 종료 직전 (best-effort) |
terminated | 비정상 종료 감지 (앱 재시작 시) |
에러 타입
type BackgroundError =
| 'TASK_NOT_FOUND'
| 'TASK_ALREADY_EXISTS'
| 'TASK_ALREADY_RUNNING'
| 'NOTIFICATION_PERMISSION_DENIED'
| 'INVALID_INPUT'
| 'UNKNOWN';
전체 사용 예시
async function setupBackgroundService() {
const notifPerm = await bridge.call('checkNotificationPermission');
if (!notifPerm.granted) {
const result = await bridge.call('requestNotificationPermission');
if (!result.granted) {
alert('알림 권한이 필요합니다');
return;
}
}
const registerResult = await bridge.call('registerTask', {
taskId: 'lifecycle-task',
triggers: ['app_foreground', 'app_background'],
callback: (trigger, data) => {
console.log('[WebView] Trigger:', trigger);
if (trigger === 'app_background') {
fetch('https://api.example.com/sync', {
method: 'POST',
body: JSON.stringify({ event: 'background', timestamp: data.timestamp })
});
}
},
onEvent: (event) => {
console.log('[Native] Event:', event.type);
},
onTerminate: (event) => {
console.log('Task terminating:', event.taskId);
},
notification: {
title: '백그라운드 서비스',
body: '앱이 백그라운드에서 실행 중입니다',
icon: 'ic_notification',
color: '#4CAF50',
priority: 'low',
ongoing: true,
silent: true
}
});
if (!registerResult.success) {
console.error('등록 실패:', registerResult.error);
return;
}
const startResult = await bridge.call('startTask', { taskId: 'lifecycle-task' });
if (!startResult.success) {
console.error('시작 실패:', startResult.error);
}
}
async function stopService() {
await bridge.call('stopTask', { taskId: 'lifecycle-task' });
}
플랫폼별 설정
Android
AndroidManifest.xml에 자동 추가:
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
Android 13+ (API 33):
POST_NOTIFICATIONS 런타임 권한 필수
- 권한 없이
startTask 호출 시 NOTIFICATION_PERMISSION_DENIED 에러
iOS
Info.plist에 추가:
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>processing</string>
</array>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>expo.modules.custombackground.refresh</string>
</array>
실행 플로우
Task 시작 플로우
1. registerTask() - 태스크 등록 (TaskManager에 저장)
2. startTask() - 태스크 시작
- Android: Foreground Service 시작
- HeadlessWebView 초기화
- callback 함수 WebView에 주입
3. "started" 이벤트 발생
라이프사이클 트리거 플로우
1. 앱 포그라운드/백그라운드 전환 감지
- Android: OnActivityEntersForeground/OnActivityEntersBackground
- iOS: willEnterForegroundNotification/didEnterBackgroundNotification
2. 실행 중인 태스크 중 해당 트리거를 가진 태스크 탐색
3. HeadlessWebView에서 callback 실행
4. "trigger" 이벤트 발생
종료 플로우
1. stopTask() 또는 시스템에 의한 종료
2. "terminating" 이벤트 발생 (best-effort)
3. 리소스 정리
4. "stopped" 이벤트 발생
비정상 종료 감지
1. 앱이 강제 종료됨 (isRunning=true 상태로 저장됨)
2. 앱 재시작 시 TaskManager가 이전 상태 복원
3. isRunning=true인 태스크 감지
4. "terminated" 이벤트 발생 (reason: 'unexpected')
제한사항
1. 클로저 변수 사용 불가
callback 함수는 toString()으로 변환되어 WebView에 주입됩니다.
외부 스코프 변수는 WebView에서 접근할 수 없습니다.
const config = { url: 'https://api.com' };
callback: (trigger, data) => {
fetch(config.url);
}
callback: (trigger, data) => {
fetch('https://api.com');
}
2. TypeScript 지원
TypeScript 타입 어노테이션은 컴파일 시 제거되므로 정상 작동합니다.
callback: (trigger: TriggerType, data: TriggerData) => {
}
3. 비동기 콜백
async/await 사용 가능합니다.
callback: async (trigger, data) => {
const response = await fetch('https://api.com');
}
4. 네트워크 요청
Headless WebView에서 fetch, XMLHttpRequest, WebSocket 사용 가능합니다.
라이선스
MIT