
Security News
Happy Birthday, Shai-Hulud
It has been one year since Shai-Hulud made its first appearance on npm.
rnww-plugin-background
Advanced tools
React Native WebView 백그라운드 실행 플러그인 (Expo Native Module)
Headless WebView를 사용하여 앱 종료 후에도 JavaScript를 독립적으로 실행합니다.
본 프로젝트(RN-Expo-WebApp-Wrapper-Template)에서 npm run config를 실행하여 플러그인 설정에서 **"Enable Headless Bridge"**를 체크해야 정상적으로 동작합니다.
이 옵션을 활성화하면:
AppBridge API를 사용할 수 있습니다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' });
이 플러그인의 핵심은 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로 유지 │
└─────────────────────────────────────────────────────────────────┘
| 항목 | Foreground WebView | Headless WebView |
|---|---|---|
| 용도 | 실제 웹페이지 렌더링 | 백그라운드 스크립트 실행 |
| DOM | 전체 HTML/CSS 렌더링 | 빈 페이지 |
| window 객체 | 독립 인스턴스 | 독립 인스턴스 |
| AppBridge | 동일한 API (인스턴스 A) | 동일한 API (인스턴스 B) |
| 변수 공유 | ❌ 불가 | ❌ 불가 |
| localStorage | 앱 종료 시 유지 | 서비스 종료 시 초기화 |
| 생명주기 | 앱과 함께 | 서비스와 함께 (앱 종료 후에도 유지) |
두 WebView에 동일한 Bridge 클라이언트 코드가 주입되므로:
AppBridge.send(), AppBridge.call(), AppBridge.on() 등backgr:setTaskData, api:* 등두 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은 반드시 .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' }
});
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...' }
});
모든 핸들러는 backgr: 네임스페이스를 사용합니다.
| 핸들러 | 설명 |
|---|---|
backgr:registerTask | 태스크 등록 |
backgr:unregisterTask | 태스크 해제 |
backgr:startTask | 태스크 시작 |
backgr:stopTask | 태스크 중지 |
backgr:stopAllTasks | 모든 태스크 중지 |
| 핸들러 | 설명 |
|---|---|
backgr:getTaskStatus | 특정 태스크 상태 조회 |
backgr:getAllTasksStatus | 전체 태스크 상태 조회 |
| 핸들러 | 설명 |
|---|---|
backgr:updateNotification | 알림 내용 업데이트 |
| 핸들러 | 설명 |
|---|---|
backgr:checkBackgroundPermission | 백그라운드 권한 확인 |
backgr:requestBackgroundPermission | 배터리 최적화 예외 요청 |
backgr:checkNotificationPermission | 알림 권한 확인 (Android 13+) |
backgr:requestNotificationPermission | 알림 권한 요청 (Android 13+) |
| 핸들러 | 설명 | 앱 종료 후 |
|---|---|---|
backgr:setTaskData | 데이터 저장 | ✅ 동작 |
backgr:getTaskData | 데이터 조회 | ✅ 동작 |
backgr:removeTaskData | 데이터 삭제 | ✅ 동작 |
| 핸들러 | 설명 |
|---|---|
backgr:attachWebView | WebView 수동 연결 |
backgr:detachWebView | WebView 연결 해제 |
backgr:evaluateInMainWebView | Main WebView에서 JS 실행 |
앱이 완전히 종료되어도 Foreground Service 내의 Headless WebView는 계속 동작합니다.
backgr:* 핸들러 (Native에서 직접 처리)api:*, auth:* 등)앱을 다시 열었을 때 백그라운드 상태와 동기화하는 패턴:
// 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();
}, []);
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 확인 |
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 런타임 권한 필수
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
FAQs
React Native WebView Background Execution Plugin with Expo support
We found that rnww-plugin-background demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Security News
It has been one year since Shai-Hulud made its first appearance on npm.

Research
/Security News
Operators behind PolinRider used a compromised GitHub account to plant malware in four development versions of a Packagist package with 700,000+ downloads.

Security News
GitHub Actions now supports cache-mode, a least-privilege control on the Actions cache aimed at the cache poisoning technique behind recent compromises.