RNWW Plugin Background
React Native WebView 백그라운드 실행 플러그인 (Expo Native Module)
Headless WebView를 사용하여 앱 종료 후에도 JavaScript를 독립적으로 실행합니다.
⚠️ 필수 설정
본 프로젝트(RN-Expo-WebApp-Wrapper-Template)에서 npm run config를 실행하여 플러그인 설정에서 **"Enable Headless Bridge"**를 체크해야 정상적으로 동작합니다.
이 옵션을 활성화하면:
- Headless WebView에 Bridge 스크립트가 자동으로 주입됩니다
- Headless WebView에서
AppBridge API를 사용할 수 있습니다
- 기존 Bridge 핸들러와 연동됩니다
설치
npm install rnww-plugin-background
빠른 시작
await AppBridge.call('backgr:registerTask', {
taskId: 'my-task',
triggers: ['app_foreground', 'app_background'],
callback: function(trigger, data) {
console.log('Trigger:', trigger);
if (trigger === 'init') {
AppBridge.call('backgr:getTaskData', { taskId: data.taskId })
.then(function(result) {
window.__counter = result?.data?.counter || 0;
});
}
}.toString(),
notification: {
title: '백그라운드 실행 중',
body: '서비스가 실행 중입니다'
}
});
await AppBridge.call('backgr:startTask', { taskId: 'my-task' });
핵심 개념
두 개의 분리된 WebView Context
이 플러그인의 핵심은 Foreground WebView와 Headless WebView가 완전히 별개의 context라는 점입니다.
┌─────────────────────────────────────────────────────────────────┐
│ Native (React Native) │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Bridge Handlers │ │
│ │ backgr:setTaskData, backgr:getTaskData, api:*, ... │ │
│ └───────────────────────────────────────────────────────────┘ │
│ ▲ ▲ │
│ │ │ │
│ postMessage│ postMessage│ │
│ │ │ │
│ ┌─────────────────┴──┐ ┌─────────┴────────────────────────┐ │
│ │ Foreground WebView │ │ Headless WebView │ │
│ │ │ │ │ │
│ │ • 실제 웹 페이지 │ │ • 빈 페이지 (스크립트만 실행) │ │
│ │ • 사용자 인터랙션 │ │ • 백그라운드 콜백 실행 │ │
│ │ • window.AppBridge │ │ • window.AppBridge │ │
│ │ (인스턴스 A) │ │ (인스턴스 B) │ │
│ │ │ │ │ │
│ │ 동일한 코드 주입 │ │ 동일한 코드 주입 │ │
│ └────────────────────┘ └──────────────────────────────────┘ │
│ 앱과 함께 종료 Foreground Service로 유지 │
└─────────────────────────────────────────────────────────────────┘
Context 특징 비교
| 용도 | 실제 웹페이지 렌더링 | 백그라운드 스크립트 실행 |
| DOM | 전체 HTML/CSS 렌더링 | 빈 페이지 |
| window 객체 | 독립 인스턴스 | 독립 인스턴스 |
| AppBridge | 동일한 API (인스턴스 A) | 동일한 API (인스턴스 B) |
| 변수 공유 | ❌ 불가 | ❌ 불가 |
| localStorage | 앱 종료 시 유지 | 서비스 종료 시 초기화 |
| 생명주기 | 앱과 함께 | 서비스와 함께 (앱 종료 후에도 유지) |
AppBridge - 같은 코드, 다른 인스턴스
두 WebView에 동일한 Bridge 클라이언트 코드가 주입되므로:
- 같은 API 사용 가능:
AppBridge.send(), AppBridge.call(), AppBridge.on() 등
- 같은 Native 핸들러 호출 가능:
backgr:setTaskData, api:* 등
- 하지만 변수, 리스너, 상태는 공유되지 않음
TaskData를 통한 Context 동기화
두 WebView 간 데이터 공유는 TaskData API를 통해서만 가능합니다:
Foreground WebView ←→ Native TaskData Storage ←→ Headless WebView
setTaskData() → [JSON 데이터 저장] ← setTaskData()
getTaskData() ← → getTaskData()
AppBridge.call('backgr:setTaskData', {
taskId: 'my-task',
data: { counter: 10, lastUpdate: Date.now() }
});
const result = await AppBridge.call('backgr:getTaskData', { taskId: 'my-task' });
트리거 시스템
트리거 종류
init | 태스크 시작 직후 | ✅ 항상 | 초기화 로직 실행 |
terminate | 태스크 종료 직전 | ✅ 항상 | 상태 저장, 리소스 정리 |
app_foreground | 앱 포그라운드 전환 | ❌ triggers 배열 필요 | 데이터 동기화, UI 갱신 |
app_background | 앱 백그라운드 전환 | ❌ triggers 배열 필요 | 상태 저장, 네트워크 요청 |
callback 함수 작성 주의사항
⚠️ callback은 반드시 .toString()으로 전달해야 합니다!
const myCallback = function(trigger, data) {
console.log(trigger);
};
await AppBridge.call('backgr:registerTask', {
taskId: 'my-task',
callback: myCallback.toString(),
notification: { title: 'Service', body: 'Running' }
});
await AppBridge.call('backgr:registerTask', {
taskId: 'my-task',
callback: myCallback,
notification: { title: 'Service', body: 'Running' }
});
완전한 callback 예시
const BACKGROUND_CALLBACK = function(trigger, data) {
if (trigger === 'init') {
AppBridge.call('backgr:getTaskData', { taskId: data.taskId })
.then(function(result) {
if (result && result.data && typeof result.data.counter === 'number') {
window.__counter = result.data.counter;
} else {
window.__counter = 0;
}
window.__interval = setInterval(function() {
window.__counter++;
AppBridge.call('backgr:setTaskData', {
taskId: data.taskId,
data: { counter: window.__counter, updatedAt: Date.now() }
});
AppBridge.call('backgr:updateNotification', {
taskId: data.taskId,
title: 'Counter: ' + window.__counter,
body: 'Running...'
});
}, 1000);
});
}
if (trigger === 'terminate') {
if (window.__interval) {
clearInterval(window.__interval);
window.__interval = null;
}
}
};
await AppBridge.call('backgr:registerTask', {
taskId: 'counter-task',
triggers: ['app_foreground', 'app_background'],
callback: BACKGROUND_CALLBACK.toString(),
notification: { title: 'Counter', body: 'Starting...' }
});
Bridge Handlers
모든 핸들러는 backgr: 네임스페이스를 사용합니다.
태스크 생명주기
backgr:registerTask | 태스크 등록 |
backgr:unregisterTask | 태스크 해제 |
backgr:startTask | 태스크 시작 |
backgr:stopTask | 태스크 중지 |
backgr:stopAllTasks | 모든 태스크 중지 |
상태 조회
backgr:getTaskStatus | 특정 태스크 상태 조회 |
backgr:getAllTasksStatus | 전체 태스크 상태 조회 |
알림 (Android)
backgr:updateNotification | 알림 내용 업데이트 |
권한
backgr:checkBackgroundPermission | 백그라운드 권한 확인 |
backgr:requestBackgroundPermission | 배터리 최적화 예외 요청 |
backgr:checkNotificationPermission | 알림 권한 확인 (Android 13+) |
backgr:requestNotificationPermission | 알림 권한 요청 (Android 13+) |
TaskData (Context 동기화)
backgr:setTaskData | 데이터 저장 | ✅ 동작 |
backgr:getTaskData | 데이터 조회 | ✅ 동작 |
backgr:removeTaskData | 데이터 삭제 | ✅ 동작 |
WebView 제어 (레거시)
backgr:attachWebView | WebView 수동 연결 |
backgr:detachWebView | WebView 연결 해제 |
backgr:evaluateInMainWebView | Main WebView에서 JS 실행 |
앱 종료 후 동작
앱이 완전히 종료되어도 Foreground Service 내의 Headless WebView는 계속 동작합니다.
앱 종료 후에도 동작하는 것
- ✅ callback 함수 (Headless WebView에서 실행)
- ✅
backgr:* 핸들러 (Native에서 직접 처리)
- ✅ Web API (fetch, setTimeout, setInterval 등)
- ✅ TaskData 저장/조회
앱 종료 후 동작하지 않는 것
- ❌ React Native 핸들러 (
api:*, auth:* 등)
- ❌ Foreground WebView와의 직접 통신
Foreground에서 백그라운드 상태 동기화
앱을 다시 열었을 때 백그라운드 상태와 동기화하는 패턴:
async function syncWithBackground() {
const TASK_ID = 'my-task';
const statusResult = await AppBridge.call('backgr:getTaskStatus', { taskId: TASK_ID });
if (statusResult?.status?.isRunning) {
console.log('🔄 실행 중인 백그라운드 태스크 발견!');
const dataResult = await AppBridge.call('backgr:getTaskData', { taskId: TASK_ID });
if (dataResult?.data) {
setCounter(dataResult.data.counter);
setLastUpdate(dataResult.data.updatedAt);
console.log('UI 복원 완료:', dataResult.data);
}
}
}
useEffect(() => {
syncWithBackground();
}, []);
알림 설정 (Android)
notification: {
title: string;
body: string;
icon?: string;
color?: string;
priority?: 'min' | 'low' | 'default' | 'high' | 'max';
ongoing?: boolean;
silent?: boolean;
channelId?: string;
channelName?: string;
channelDescription?: string;
}
에러 타입
TASK_NOT_FOUND | 미등록 태스크 | registerTask 먼저 호출 |
TASK_ALREADY_EXISTS | 중복 등록 | unregisterTask 후 재등록 |
TASK_ALREADY_RUNNING | 이미 실행 중 | getTaskStatus로 확인 |
NOTIFICATION_PERMISSION_DENIED | 알림 권한 없음 | requestNotificationPermission 호출 |
WEBVIEW_INIT_TIMEOUT | Headless 초기화 실패 | 앱 재시작 또는 스크립트 확인 |
INVALID_INPUT | 잘못된 입력 | 유효한 taskId 확인 |
플랫폼별 설정
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" />
Android 13+ (API 33): POST_NOTIFICATIONS 런타임 권한 필수
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>
라이선스
MIT