
Security News
Deno Goes the Way of the Dinosaur
Cloudflare is acquiring Deno’s team, which will provide another year of bug fixes and security updates before ending support for the runtime.
rnww-plugin-background
Advanced tools
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 콜백을 주입하여 앱 라이프사이클 이벤트 발생 시 실행합니다.
init, terminate): 항상 자동 호출됨app_foreground, app_background): triggers 배열에 포함 시에만 호출// 콜백 함수가 WebView에 주입되어 트리거 발생 시 실행됨
bridge.call('registerTask', {
taskId: 'my-task',
triggers: ['app_foreground', 'app_background'], // 라이프사이클 트리거 (선택)
callback: (trigger, data) => {
// 이 코드는 Headless WebView 내에서 실행됨
switch (trigger) {
case 'init':
// 태스크 시작 시 초기화 (항상 호출됨)
console.log('Task initialized');
break;
case 'terminate':
// 태스크 종료 직전 정리 (항상 호출됨)
console.log('Task terminating');
break;
case 'app_background':
// 앱이 백그라운드로 갈 때 상태 저장
saveCurrentState();
break;
case 'app_foreground':
// 앱이 포그라운드로 올 때 데이터 동기화
syncData();
break;
}
},
notification: {
title: '백그라운드 실행 중',
body: '서비스가 실행 중입니다'
}
});
백그라운드 작업을 등록합니다.
const result = await bridge.call('registerTask', {
taskId: string; // 필수: 작업 고유 ID
triggers?: TriggerType[]; // 라이프사이클 트리거 ['app_foreground', 'app_background']
// ※ init, terminate는 자동 호출되므로 지정 불필요
callback?: BackgroundCallback; // WebView에서 실행될 콜백 (init/terminate 포함)
onEvent?: TaskCallback; // 네이티브 이벤트 리스너
onTerminate?: TaskCallback; // 종료 직전 콜백 (브릿지 레벨)
callbackId?: string; // 이벤트 추적용 식별자
notification?: NotificationConfig; // Android 필수
});
// 결과
// { success: true, taskId: 'my-task' }
// { success: false, error: 'TASK_ALREADY_EXISTS' }
triggers vs 기본 트리거:
triggers 배열: app_foreground, app_background만 지정 가능init, terminate: triggers 배열과 무관하게 항상 자동 호출됨등록된 작업을 해제합니다.
const result = await bridge.call('unregisterTask', { taskId: 'my-task' });
// { success: true, taskId: 'my-task' }
작업을 시작합니다. Android에서는 Foreground Service가 시작됩니다.
const result = await bridge.call('startTask', { taskId: 'my-task' });
// { success: true, taskId: 'my-task' }
// { success: false, error: 'NOTIFICATION_PERMISSION_DENIED' }
작업을 중지합니다.
const result = await bridge.call('stopTask', { taskId: 'my-task' });
// { success: true, taskId: 'my-task' }
모든 작업을 중지합니다.
const result = await bridge.call('stopAllTasks');
// { success: true }
실행 중인 알림 내용을 업데이트합니다. (Android only)
const result = await bridge.call('updateNotification', {
taskId: 'my-task',
title: '동기화 중',
body: '작업 진행 중...',
silent: true // 업데이트 시 소리/진동 없음 (기본값: true)
});
// { success: true }
특정 작업의 상태를 조회합니다.
const result = await bridge.call('getTaskStatus', { taskId: 'my-task' });
// {
// success: true,
// status: {
// taskId: 'my-task',
// isRunning: true,
// mode: 'persistent',
// startedAt: 1234567890000
// }
// }
모든 작업의 상태를 조회합니다.
const result = await bridge.call('getAllTasksStatus');
// {
// success: true,
// tasks: [...],
// isAnyRunning: true
// }
백그라운드 권한 상태를 확인합니다.
const result = await bridge.call('checkBackgroundPermission');
// {
// success: true,
// canRunBackground: true,
// batteryOptimizationExempt: true, // Android
// notificationPermission: true, // Android 13+
// requiredPermissions: [],
// deniedPermissions: []
// }
백그라운드 권한을 요청합니다. (배터리 최적화 예외)
const result = await bridge.call('requestBackgroundPermission');
알림 권한을 확인합니다. (Android 13+ 필수)
const result = await bridge.call('checkNotificationPermission');
// { success: true, granted: true, canAskAgain: false }
알림 권한을 요청합니다.
const result = await bridge.call('requestNotificationPermission');
// { success: true, granted: true, canAskAgain: false }
작업별 데이터를 저장합니다. (SharedPreferences/UserDefaults)
const result = await bridge.call('setTaskData', {
taskId: 'my-task',
data: { lastSync: Date.now(), count: 5 }
});
// { success: true }
작업별 데이터를 조회합니다.
const result = await bridge.call('getTaskData', { taskId: 'my-task' });
// { success: true, data: { lastSync: 1234567890, count: 5 } }
작업별 데이터를 삭제합니다.
const result = await bridge.call('removeTaskData', { taskId: 'my-task' });
// { success: true }
브릿지 핸들러와 리소스를 정리합니다.
await bridge.call('disposeBackgroundHandlers');
// { success: true }
트리거는 기본 트리거와 라이프사이클 트리거로 구분됩니다.
triggers 배열 설정과 무관하게 항상 자동으로 호출됩니다.
| 트리거 | 호출 시점 | 설명 |
|---|---|---|
init | 태스크 시작 직후 | 콜백 함수가 WebView에 주입 완료된 후 자동 호출. 초기화 로직 실행에 사용 |
terminate | 태스크 종료 직전 | 서비스가 종료되기 전 자동 호출. 상태 저장, 리소스 정리 등에 사용 |
triggers 배열에 명시적으로 포함해야 호출됩니다.
| 트리거 | 호출 시점 | 설명 |
|---|---|---|
app_foreground | 앱이 포그라운드로 전환 | 사용자가 앱으로 돌아올 때 호출. 데이터 동기화, UI 갱신 등에 사용 |
app_background | 앱이 백그라운드로 전환 | 사용자가 앱을 떠날 때 호출. 상태 저장, 네트워크 요청 등에 사용 |
startTask() 호출
│
▼
┌─────────────────────┐
│ 콜백 WebView 주입 │
└─────────────────────┘
│
▼
┌─────────────────────┐
│ init 트리거 발생 │ ◄── 항상 자동 호출
└─────────────────────┘
│
▼
┌─────────────────────┐
│ 태스크 실행 중 │
│ │
│ app_foreground ◄───┼── triggers 배열에 포함 시
│ app_background ◄───┼── triggers 배열에 포함 시
│ │
└─────────────────────┘
│
▼ (stopTask 또는 서비스 종료)
┌─────────────────────┐
│ terminate 트리거 발생│ ◄── 항상 자동 호출
└─────────────────────┘
│
▼
종료
bridge.call('registerTask', {
taskId: 'lifecycle-task',
triggers: ['app_foreground', 'app_background'], // 라이프사이클 트리거만 지정
callback: (trigger, data) => {
console.log(`트리거: ${trigger}, 시간: ${data.timestamp}`);
switch (trigger) {
// ═══════════════════════════════════════════════════════════
// 기본 트리거 (triggers 배열 설정 없이도 항상 호출됨)
// ═══════════════════════════════════════════════════════════
case 'init':
// 태스크 시작 시 초기화
// - API 연결 설정
// - 초기 데이터 로드
// - 타이머/인터벌 설정
console.log('Task initialized');
initializeConnections();
loadInitialData();
break;
case 'terminate':
// 태스크 종료 직전 정리
// - 상태 저장
// - 연결 종료
// - 리소스 해제
console.log('Task terminating');
saveCurrentState();
closeConnections();
break;
// ═══════════════════════════════════════════════════════════
// 라이프사이클 트리거 (triggers 배열에 포함 시에만 호출됨)
// ═══════════════════════════════════════════════════════════
case 'app_background':
// 앱이 백그라운드로 전환될 때
// - 현재 상태 저장
// - 서버에 상태 동기화
console.log('App went to background');
saveCurrentState();
syncToServer();
break;
case 'app_foreground':
// 앱이 포그라운드로 돌아올 때
// - 서버에서 최신 데이터 가져오기
// - 캐시 갱신
console.log('App came to foreground');
fetchLatestData();
refreshCache();
break;
}
},
notification: { title: 'Service', body: 'Running...' }
});
모든 트리거 호출 시 전달되는 데이터입니다.
interface TriggerData {
timestamp: number; // 트리거 발생 시간 (Unix timestamp, ms)
taskId: string; // 작업 ID
}
// 사용 예
callback: (trigger, data) => {
console.log(`Task: ${data.taskId}`);
console.log(`Time: ${new Date(data.timestamp).toISOString()}`);
}
callback은 Headless WebView 내에서 실행됩니다.
callback: (trigger: TriggerType, data: TriggerData) => void
// TriggerType: 'init' | 'terminate' | 'app_foreground' | 'app_background'
| 트리거 | 호출 조건 | 용도 |
|---|---|---|
init | 항상 자동 호출 | 초기화, 연결 설정, 전역 변수 설정 |
terminate | 항상 자동 호출 | 정리, 최종 저장, 연결 종료 |
app_foreground | triggers 배열에 포함 시 | 데이터 새로고침, UI 동기화 |
app_background | triggers 배열에 포함 시 | 상태 저장, 서버 동기화 |
callback: (trigger, data) => {
switch (trigger) {
case 'init':
// 초기화 로직 (항상 호출됨)
break;
case 'terminate':
// 정리 로직 (항상 호출됨)
break;
case 'app_foreground':
// 포그라운드 전환 시 (triggers에 포함 필요)
break;
case 'app_background':
// 백그라운드 전환 시 (triggers에 포함 필요)
break;
}
}
중요 제한사항: 클로저 사용 불가
// ❌ 작동 안 함 - 외부 변수 참조
const API_URL = 'https://api.example.com';
callback: (trigger, data) => {
fetch(API_URL); // API_URL은 WebView에서 undefined!
}
// ✅ 올바른 방법 - 값을 직접 인라인
callback: (trigger, data) => {
fetch('https://api.example.com');
}
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;
}
},
// ...
});
서비스 종료 직전에 실행됩니다.
bridge.call('registerTask', {
taskId: 'my-task',
onTerminate: async (event) => {
// 종료 직전 상태 저장 (약 5초 제한)
await bridge.call('setTaskData', {
taskId: event.taskId,
data: { terminatedAt: Date.now() }
});
},
// ...
});
주의: 프로세스가 즉시 kill되면 호출되지 않을 수 있습니다. 비정상 종료는 다음 앱 실행 시 terminated 이벤트로 감지됩니다.
여러 작업의 이벤트를 구분하기 위한 식별자입니다.
bridge.call('registerTask', {
taskId: 'task-1',
callbackId: 'sync-callback',
// ...
});
// 이벤트에서 callbackId로 구분
bridge.on('onTaskEvent', (event) => {
if (event.callbackId === 'sync-callback') {
// task-1의 이벤트
}
});
interface NotificationConfig {
taskId?: string; // updateNotification 시 대상 지정
title: string; // 알림 제목
body: string; // 알림 본문
icon?: string; // Android drawable 리소스명
color?: string; // 아이콘 색상 (hex: '#4CAF50')
priority?: 'min' | 'low' | 'default' | 'high' | 'max';
ongoing?: boolean; // 지속 알림 (스와이프 불가, 기본값: true)
silent?: boolean; // 무음 알림 (기본값: false, updateNotification 시 true)
channelId?: string; // Android 8.0+
channelName?: string;
channelDescription?: string;
}
notification: {
title: '백그라운드 서비스',
body: '실행 중입니다',
icon: 'ic_notification',
color: '#4CAF50',
priority: 'low',
ongoing: true,
silent: false,
channelId: 'my_channel',
channelName: 'Background Service'
}
interface TaskEvent {
taskId: string;
type: 'started' | 'stopped' | 'trigger' | 'terminating' | 'terminated';
trigger?: TriggerType; // type이 'trigger'일 때 (init, terminate, app_foreground, app_background)
reason?: 'unexpected'; // type이 'terminated'일 때
lastStartedAt?: number; // type이 'terminated'일 때
timestamp: number;
callbackId?: string;
}
| 타입 | 설명 |
|---|---|
started | 작업 시작됨 |
stopped | 작업 중지됨 |
trigger | 트리거 발생 (WebView callback 호출됨). event.trigger에 트리거 종류 포함 |
terminating | 서비스 종료 직전 (best-effort) |
terminated | 비정상 종료 감지 (앱 재시작 시) |
startTask() 호출
│
▼
┌─────────────────────────────────────────┐
│ type: 'started' │
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ type: 'trigger', trigger: 'init' │ ◄── 자동
└─────────────────────────────────────────┘
│
▼
실행 중...
│
▼ (라이프사이클 이벤트 발생 시)
┌─────────────────────────────────────────┐
│ type: 'trigger', trigger: 'app_*' │ ◄── triggers 배열에 포함 시
└─────────────────────────────────────────┘
│
▼ (stopTask 또는 서비스 종료)
┌─────────────────────────────────────────┐
│ type: 'trigger', trigger: 'terminate' │ ◄── 자동
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ type: 'terminating' │
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ type: 'stopped' │
└─────────────────────────────────────────┘
type BackgroundError =
| 'TASK_NOT_FOUND' // 작업을 찾을 수 없음
| 'TASK_ALREADY_EXISTS' // 이미 등록된 작업
| 'TASK_ALREADY_RUNNING' // 이미 실행 중
| 'NOTIFICATION_PERMISSION_DENIED' // 알림 권한 없음 (Android 13+)
| 'INVALID_INPUT' // 잘못된 입력
| 'UNKNOWN'; // 알 수 없는 에러
async function setupBackgroundService() {
// 1. 권한 확인 (Android 13+)
const notifPerm = await bridge.call('checkNotificationPermission');
if (!notifPerm.granted) {
const result = await bridge.call('requestNotificationPermission');
if (!result.granted) {
alert('알림 권한이 필요합니다');
return;
}
}
// 2. 작업 등록
const registerResult = await bridge.call('registerTask', {
taskId: 'lifecycle-task',
triggers: ['app_foreground', 'app_background'], // 라이프사이클 트리거
// WebView에서 실행될 콜백
// init, terminate는 triggers 배열과 무관하게 항상 호출됨
callback: (trigger, data) => {
console.log('[WebView] Trigger:', trigger, 'at', data.timestamp);
switch (trigger) {
// ═══════════════════════════════════════════════════════════
// 기본 트리거 (항상 자동 호출)
// ═══════════════════════════════════════════════════════════
case 'init':
// 태스크가 시작되고 콜백이 WebView에 주입된 직후
console.log('[WebView] Task initialized');
// 초기화 작업 수행
// - 전역 변수 설정
// - API 클라이언트 초기화
// - WebSocket 연결 등
window.__taskState = {
startedAt: data.timestamp,
syncCount: 0
};
break;
case 'terminate':
// 서비스가 종료되기 직전
console.log('[WebView] Task terminating');
// 정리 작업 수행
// - 최종 상태 저장
// - 연결 종료
// - 리소스 해제
if (window.__taskState) {
fetch('https://api.example.com/final-sync', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
taskId: data.taskId,
duration: data.timestamp - window.__taskState.startedAt,
totalSyncs: window.__taskState.syncCount
})
});
}
break;
// ═══════════════════════════════════════════════════════════
// 라이프사이클 트리거 (triggers 배열에 포함 시에만 호출)
// ═══════════════════════════════════════════════════════════
case 'app_background':
// 앱이 백그라운드로 전환될 때
console.log('[WebView] App went to background');
// 백그라운드 진입 시 처리
// - 현재 상태 서버에 동기화
// - 알림 업데이트
window.__taskState.syncCount++;
fetch('https://api.example.com/sync', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
event: 'background',
timestamp: data.timestamp
})
});
break;
case 'app_foreground':
// 앱이 포그라운드로 돌아올 때
console.log('[WebView] App came to foreground');
// 포그라운드 복귀 시 처리
// - 최신 데이터 가져오기
// - 캐시 갱신
fetch('https://api.example.com/status')
.then(res => res.json())
.then(status => {
console.log('[WebView] Server status:', status);
});
break;
}
},
// 네이티브 이벤트 리스너 (브릿지 레벨)
onEvent: (event) => {
console.log('[Native] Event:', event.type, event.trigger || '');
// 네이티브 이벤트 처리
// - UI 업데이트
// - 상태 관리
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: async (event) => {
console.log('[Native] Task terminating:', event.taskId);
// 종료 직전 네이티브 레벨 처리
// - 로컬 데이터 저장
// - 로그 기록
await bridge.call('setTaskData', {
taskId: event.taskId,
data: { terminatedAt: Date.now() }
});
},
notification: {
title: '백그라운드 서비스',
body: '앱이 백그라운드에서 실행 중입니다',
icon: 'ic_notification',
color: '#4CAF50',
priority: 'low',
ongoing: true,
silent: true,
channelId: 'background_service',
channelName: 'Background Service',
channelDescription: '백그라운드 작업 실행 알림'
}
});
if (!registerResult.success) {
console.error('등록 실패:', registerResult.error);
return;
}
// 3. 작업 시작
const startResult = await bridge.call('startTask', { taskId: 'lifecycle-task' });
if (!startResult.success) {
console.error('시작 실패:', startResult.error);
}
// 시작 후 트리거 순서:
// 1. "started" 이벤트 발생 → onEvent 호출
// 2. "init" 트리거 발생 → callback 호출
}
// 작업 중지
async function stopService() {
// 중지 시 트리거 순서:
// 1. "terminate" 트리거 발생 → callback 호출
// 2. "terminating" 이벤트 발생 → onTerminate 호출
// 3. "stopped" 이벤트 발생 → onEvent 호출
await bridge.call('stopTask', { taskId: 'lifecycle-task' });
}
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 에러Info.plist에 추가:
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>processing</string>
</array>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>expo.modules.custombackground.refresh</string>
</array>
1. registerTask() - 태스크 등록 (TaskManager에 저장)
2. startTask() - 태스크 시작
│
├─ Android: Foreground Service 시작
├─ HeadlessWebView 초기화
└─ callback 함수 WebView에 주입
│
▼
3. "started" 이벤트 발생 (네이티브 → 브릿지)
4. "init" 트리거 발생 (WebView 콜백 실행) ◄── 자동
5. 태스크 실행 중...
1. 앱 포그라운드/백그라운드 전환 감지
- Android: OnActivityEntersForeground/OnActivityEntersBackground
- iOS: willEnterForegroundNotification/didEnterBackgroundNotification
2. 실행 중인 태스크 확인
3. triggers 배열에 해당 트리거가 포함되어 있는지 확인
│
├─ 포함됨 → HeadlessWebView에서 callback 실행
│ "trigger" 이벤트 발생
│
└─ 포함 안 됨 → 무시
※ init/terminate는 triggers 배열과 무관하게 항상 호출됨
1. stopTask() 호출 또는 시스템에 의한 종료
│
▼
2. "terminate" 트리거 발생 (WebView 콜백 실행) ◄── 자동
│
▼
3. "terminating" 이벤트 발생 (네이티브 → 브릿지, best-effort)
│
▼
4. HeadlessWebView 정리
│
▼
5. "stopped" 이벤트 발생
1. 앱이 강제 종료됨
- isRunning=true 상태로 저장됨
- terminate 트리거 호출되지 않음 (프로세스 즉시 kill)
2. 앱 재시작 시
│
▼
3. TaskManager가 이전 상태 복원
│
▼
4. isRunning=true인 태스크 감지
│
▼
5. "terminated" 이벤트 발생
- type: 'terminated'
- reason: 'unexpected'
- lastStartedAt: 이전 시작 시간
※ 비정상 종료 시에는 terminate 트리거가 호출되지 않으므로
중요한 데이터는 app_background 트리거에서 저장하는 것을 권장
callback 함수는 toString()으로 변환되어 WebView에 주입됩니다.
외부 스코프 변수는 WebView에서 접근할 수 없습니다.
// ❌ 작동 안 함
const config = { url: 'https://api.com' };
callback: (trigger, data) => {
fetch(config.url); // config is undefined
}
// ✅ 올바른 방법
callback: (trigger, data) => {
fetch('https://api.com');
}
TypeScript 타입 어노테이션은 컴파일 시 제거되므로 정상 작동합니다.
// ✅ 정상 작동
callback: (trigger: TriggerType, data: TriggerData) => {
// ...
}
async/await 사용 가능합니다.
// ✅ 정상 작동
callback: async (trigger, data) => {
const response = await fetch('https://api.com');
// ...
}
Headless WebView에서 fetch, XMLHttpRequest, WebSocket 사용 가능합니다.
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
Cloudflare is acquiring Deno’s team, which will provide another year of bug fixes and security updates before ending support for the runtime.

Security News
Anthropic’s free OSS Scanner uses Mythos and other Claude models to find open source vulnerabilities and send maintainers reports with proposed fixes.

Security News
On Front Lines’ Early Adopters: CISO podcast, Andrew Becherer shares lessons from Datadog, weighs the long-term costs of AI-built tools, and explains how founders can actually gain a CISO’s trust.