rnww-plugin-background
v1.7.3
Published
React Native WebView Background Execution Plugin with Expo support
Downloads
107
Maintainers
Readme
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에서
AppBridgeAPI를 사용할 수 있습니다 - 기존 Bridge 핸들러와 연동됩니다
설치
npm install rnww-plugin-background빠른 시작
// 태스크 등록 - callback은 .toString()으로 전달해야 함!
await AppBridge.call('backgr:registerTask', {
taskId: 'my-task',
triggers: ['app_foreground', 'app_background'],
callback: function(trigger, data) {
// ⚠️ Headless WebView에서 실행됨 (Foreground WebView와 별개 context)
console.log('Trigger:', trigger);
if (trigger === 'init') {
// 초기화 - TaskData에서 이전 상태 복원
AppBridge.call('backgr:getTaskData', { taskId: data.taskId })
.then(function(result) {
window.__counter = result?.data?.counter || 0;
});
}
}.toString(), // ⚠️ 반드시 .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 특징 비교
| 항목 | Foreground WebView | Headless WebView | |------|-------------------|------------------| | 용도 | 실제 웹페이지 렌더링 | 백그라운드 스크립트 실행 | | 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()// Headless에서 데이터 저장
AppBridge.call('backgr:setTaskData', {
taskId: 'my-task',
data: { counter: 10, lastUpdate: Date.now() }
});
// Foreground에서 데이터 읽기 (앱 재시작 시)
const result = await AppBridge.call('backgr:getTaskData', { taskId: 'my-task' });
// result.data = { counter: 10, lastUpdate: ... }트리거 시스템
트리거 종류
| 트리거 | 호출 시점 | 자동 호출 | 설명 |
|--------|----------|----------|------|
| 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(), // ✅ .toString() 필수!
notification: { title: 'Service', body: 'Running' }
});
// ❌ 잘못된 방법 - JSON 직렬화 불가
await AppBridge.call('backgr:registerTask', {
taskId: 'my-task',
callback: myCallback, // ❌ 함수는 JSON으로 전달 불가!
notification: { title: 'Service', body: 'Running' }
});완전한 callback 예시
const BACKGROUND_CALLBACK = function(trigger, data) {
// ⚠️ 이 코드는 Headless WebView에서 실행됨!
// window, AppBridge, fetch 등 Web API 사용 가능
if (trigger === 'init') {
// TaskData에서 이전 상태 복원
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++;
// TaskData에 상태 저장
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에서 백그라운드 상태 동기화
앱을 다시 열었을 때 백그라운드 상태와 동기화하는 패턴:
// Foreground WebView에서 (앱 재시작 시)
async function syncWithBackground() {
const TASK_ID = 'my-task';
// 1. 실행 중인 태스크 확인
const statusResult = await AppBridge.call('backgr:getTaskStatus', { taskId: TASK_ID });
if (statusResult?.status?.isRunning) {
console.log('🔄 실행 중인 백그라운드 태스크 발견!');
// 2. TaskData에서 현재 상태 읽기
const dataResult = await AppBridge.call('backgr:getTaskData', { taskId: TASK_ID });
if (dataResult?.data) {
// 3. UI 업데이트
setCounter(dataResult.data.counter);
setLastUpdate(dataResult.data.updatedAt);
console.log('UI 복원 완료:', dataResult.data);
}
}
}
// 앱 로드 시 호출
useEffect(() => {
syncWithBackground();
}, []);알림 설정 (Android)
notification: {
title: string; // 필수
body: string; // 필수
icon?: string; // drawable 리소스명
color?: string; // '#4CAF50'
priority?: 'min' | 'low' | 'default' | 'high' | 'max';
ongoing?: boolean; // 기본값: true (스와이프 불가)
silent?: boolean; // 기본값: false
channelId?: string; // Android 8.0+
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
