
Security News
Lovable’s OJ Rewrites Vite’s Dev Server in Rust as AI Lowers the Cost of Forking Open Source
Lovable’s OJ rewrites Vite’s dev server in Rust, reducing memory use and preview times as AI lowers the cost of open source reimplementation.
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 콜백을 주입하여 앱 라이프사이클 이벤트 발생 시 실행합니다.
// 콜백 함수가 WebView에 주입되어 트리거 발생 시 실행됨
bridge.call('registerTask', {
taskId: 'my-task',
triggers: ['app_foreground', 'app_background'],
callback: (trigger, data) => {
// 이 코드는 Headless WebView 내에서 실행됨
if (trigger === 'app_background') {
// 앱이 백그라운드로 갈 때 상태 저장
saveCurrentState();
} else if (trigger === 'app_foreground') {
// 앱이 포그라운드로 올 때 데이터 동기화
syncData();
}
},
notification: {
title: '백그라운드 실행 중',
body: '서비스가 실행 중입니다'
}
});
백그라운드 작업을 등록합니다.
const result = await bridge.call('registerTask', {
taskId: string; // 필수: 작업 고유 ID
triggers?: TriggerType[]; // 트리거 목록 ['app_foreground', 'app_background']
callback?: BackgroundCallback; // WebView에서 실행될 콜백
onEvent?: TaskCallback; // 네이티브 이벤트 리스너
onTerminate?: TaskCallback; // 종료 직전 콜백
callbackId?: string; // 이벤트 추적용 식별자
notification?: NotificationConfig; // Android 필수
});
// 결과
// { success: true, taskId: 'my-task' }
// { success: false, error: 'TASK_ALREADY_EXISTS' }
등록된 작업을 해제합니다.
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 }
| 트리거 | 설명 |
|---|---|
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은 Headless WebView 내에서 실행됩니다.
callback: (trigger: TriggerType, data: TriggerData) => void
중요 제한사항: 클로저 사용 불가
// ❌ 작동 안 함 - 외부 변수 참조
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'일 때
reason?: 'unexpected'; // type이 'terminated'일 때
lastStartedAt?: number; // type이 'terminated'일 때
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' // 알림 권한 없음 (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에서 실행될 콜백
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;
}
// 3. 작업 시작
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' });
}
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" 이벤트 발생
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')
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
The npm package rnww-plugin-background receives a total of 1 weekly downloads. As such, rnww-plugin-background popularity was classified as not popular.
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
Lovable’s OJ rewrites Vite’s dev server in Rust, reducing memory use and preview times as AI lowers the cost of open source reimplementation.

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.