
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 백그라운드 실행 제어 플러그인
npm install rnww-plugin-background
import { registerBackgroundHandlers } from 'rnww-plugin-background';
registerBackgroundHandlers({
bridge: yourBridgeImplementation,
platform: { OS: Platform.OS },
});
백그라운드 작업을 등록합니다.
bridge.call('registerTask', {
taskId: 'sync-task',
mode: 'persistent',
interval: 60000,
triggers: ['network_change', 'battery_low'],
callbackId: 'my-callback',
callback: (event) => {
console.log('Task event:', event);
},
notification: {
title: '백그라운드 실행 중',
body: '동기화 진행 중...',
color: '#4CAF50',
priority: 'high',
ongoing: true,
progress: { current: 0, max: 100 },
actions: [
{
id: 'pause',
title: '일시정지',
onPress: (actionId, taskId) => {
console.log(`Action ${actionId} clicked for task ${taskId}`);
}
}
]
}
});
작업을 시작하거나 중지합니다.
bridge.call('startTask', { taskId: 'sync-task' });
bridge.call('stopTask', { taskId: 'sync-task' });
bridge.call('stopAllTasks');
등록된 작업을 해제합니다.
bridge.call('unregisterTask', { taskId: 'sync-task' });
알림 내용을 동적으로 업데이트합니다.
bridge.call('updateNotification', {
taskId: 'sync-task',
title: '동기화 중',
body: '50% 완료',
progress: { current: 50, max: 100 },
actions: [
{
id: 'cancel',
title: '취소',
onPress: (actionId, taskId) => {
bridge.call('stopTask', { taskId });
}
}
]
});
작업 상태를 조회합니다.
const status = await bridge.call('getTaskStatus', { taskId: 'sync-task' });
const allStatus = await bridge.call('getAllTasksStatus');
백그라운드 권한을 확인하고 요청합니다.
const permission = await bridge.call('checkBackgroundPermission');
// {
// success: true,
// canRunBackground: boolean,
// batteryOptimizationExempt: boolean,
// notificationPermission: boolean, // Android 13+
// requiredPermissions: string[],
// deniedPermissions: string[]
// }
if (!permission.canRunBackground) {
await bridge.call('requestBackgroundPermission');
}
알림 권한을 확인하고 요청합니다. (Android 13+ 필수)
// 알림 권한 확인
const notifPerm = await bridge.call('checkNotificationPermission');
// { success: true, granted: boolean, canAskAgain: boolean }
// 알림 권한 요청
if (!notifPerm.granted) {
const result = await bridge.call('requestNotificationPermission');
// { success: true, granted: boolean, canAskAgain: boolean, openedSettings?: boolean }
}
참고: Android 12 이하에서는 항상 granted: true를 반환합니다.
브릿지 핸들러와 리소스를 정리합니다.
bridge.call('disposeBackgroundHandlers');
작업 등록 시 callback 함수를 지정하면 해당 작업의 모든 이벤트를 수신합니다.
bridge.call('registerTask', {
taskId: 'my-task',
mode: 'persistent',
callback: (event) => {
switch (event.type) {
case 'started':
console.log('작업 시작됨');
break;
case 'stopped':
console.log('작업 중지됨');
break;
case 'trigger':
console.log(`트리거 발생: ${event.trigger}`, event.data);
break;
case 'action':
console.log(`액션 클릭: ${event.actionId}`);
break;
case 'error':
console.error('에러 발생:', event.error);
break;
}
},
notification: { title: 'Task', body: 'Running...' }
});
각 알림 액션 버튼에 onPress 콜백을 개별 지정할 수 있습니다.
bridge.call('registerTask', {
taskId: 'download-task',
mode: 'persistent',
notification: {
title: '다운로드 중',
body: '파일 다운로드...',
actions: [
{
id: 'pause',
title: '일시정지',
icon: 'ic_pause',
dismissOnPress: false,
onPress: async (actionId, taskId) => {
await pauseDownload();
bridge.call('updateNotification', {
taskId,
title: '일시정지됨',
body: '다운로드가 일시정지되었습니다'
});
}
},
{
id: 'cancel',
title: '취소',
dismissOnPress: true,
bringToForeground: false,
onPress: async (actionId, taskId) => {
await cancelDownload();
bridge.call('stopTask', { taskId });
}
}
]
}
});
callbackId를 지정하면 이벤트 수신 시 해당 ID가 함께 전달되어 여러 작업의 이벤트를 구분할 수 있습니다.
// 작업 등록
bridge.call('registerTask', {
taskId: 'task-1',
callbackId: 'sync-callback',
// ...
});
bridge.call('registerTask', {
taskId: 'task-2',
callbackId: 'upload-callback',
// ...
});
// 이벤트 수신
bridge.on('onTaskEvent', (event) => {
if (event.callbackId === 'sync-callback') {
// task-1의 이벤트 처리
} else if (event.callbackId === 'upload-callback') {
// task-2의 이벤트 처리
}
});
| 트리거 | 설명 |
|---|---|
interval | 주기적 실행 (interval 옵션 사용) |
network_change | 네트워크 상태 변경 (연결/해제) |
location_change | 위치 변경 (significant location change) |
time_trigger | 예약된 시간에 실행 |
battery_low | 배터리 부족 (기본 15% 이하) |
battery_okay | 배터리 정상 복귀 |
battery_charging | 충전 시작 |
battery_discharging | 충전 해제 |
app_foreground | 앱이 포그라운드로 전환 |
app_background | 앱이 백그라운드로 전환 |
app_terminate | 앱 종료 시 |
custom | 사용자 정의 트리거 |
문자열로 트리거 타입만 지정:
bridge.call('registerTask', {
taskId: 'my-task',
mode: 'persistent',
triggers: ['network_change', 'battery_low', 'app_background'],
// ...
});
객체로 트리거별 옵션 지정:
bridge.call('registerTask', {
taskId: 'my-task',
mode: 'persistent',
triggers: [
// 배터리 30% 이하일 때 트리거
{
type: 'battery_low',
options: { threshold: 30 }
},
// WiFi 연결 변경만 감지
{
type: 'network_change',
options: { networkTypes: ['wifi'] }
},
// 100m 이상 이동 시 트리거
{
type: 'location_change',
options: { minDistance: 100 }
},
// 사용자 정의 트리거
{
type: 'custom',
customId: 'my-custom-trigger'
}
],
callback: (event) => {
if (event.type === 'trigger') {
switch (event.trigger) {
case 'battery_low':
console.log('배터리 레벨:', event.data?.batteryLevel);
break;
case 'network_change':
console.log('네트워크:', event.data?.networkType, event.data?.isConnected);
break;
case 'location_change':
console.log('위치:', event.data?.location);
break;
case 'custom':
console.log('커스텀 트리거:', event.customTriggerId);
break;
}
}
},
// ...
});
{
type: 'battery_low',
options: {
threshold: 20 // 배터리 임계값 % (기본: 15)
}
}
이벤트 데이터:
event.data?.batteryLevel // 현재 배터리 레벨 (%)
{
type: 'network_change',
options: {
networkTypes: ['wifi', 'cellular'] // 감지할 네트워크 타입
}
}
이벤트 데이터:
event.data?.networkType // 'wifi' | 'cellular' | 'ethernet' | 'none'
event.data?.isConnected // 연결 상태 (boolean)
{
type: 'location_change',
options: {
minDistance: 50 // 최소 이동 거리 (미터)
}
}
이벤트 데이터:
event.data?.location // { latitude: number, longitude: number }
예약 시간에 트리거. scheduledTime 필드와 함께 사용:
bridge.call('registerTask', {
taskId: 'scheduled-task',
mode: 'efficient',
triggers: ['time_trigger'],
scheduledTime: Date.now() + 3600000, // 1시간 후
// ...
});
사용자 정의 트리거. 네이티브 측에서 직접 발생시킬 수 있음:
{
type: 'custom',
customId: 'my-sync-trigger' // 고유 식별자
}
이벤트에서 event.customTriggerId로 식별 가능.
notification: {
title: '백그라운드 작업',
body: '작업이 실행 중입니다',
icon: 'ic_notification', // Android drawable 리소스명
}
notification: {
title: '동기화',
body: '데이터 동기화 중...',
// 아이콘/강조 색상 (hex)
color: '#4CAF50',
// 우선순위
// 'min': 최소 (무음, 상태바만)
// 'low': 낮음 (무음)
// 'default': 기본
// 'high': 높음 (헤드업 알림)
// 'max': 최대 (긴급)
priority: 'high',
// 지속 알림 (사용자가 스와이프로 닫을 수 없음)
// persistent 모드에서는 항상 true
ongoing: true,
// 무음 알림 (소리/진동 없이 내용만 변경)
// updateNotification 시 기본 true
silent: false,
}
notification: {
title: '다운로드 중',
body: '50% 완료',
progress: {
current: 50,
max: 100,
indeterminate: false // true면 무한 진행 표시
}
}
무한 진행 표시 (로딩):
progress: {
current: 0,
max: 100,
indeterminate: true
}
최대 3개까지 지원:
notification: {
title: '음악 재생 중',
body: 'Now Playing...',
actions: [
{
id: 'prev',
title: '이전',
icon: 'ic_prev', // Android drawable (선택)
dismissOnPress: false, // 클릭 시 알림 유지
bringToForeground: false, // 앱을 포그라운드로 가져오지 않음
onPress: (actionId, taskId) => {
playPrevious();
}
},
{
id: 'pause',
title: '일시정지',
icon: 'ic_pause',
onPress: (actionId, taskId) => {
togglePlayPause();
}
},
{
id: 'next',
title: '다음',
icon: 'ic_next',
onPress: (actionId, taskId) => {
playNext();
}
}
]
}
notification: {
title: '알림',
body: '내용',
channelId: 'sync_channel',
channelName: '동기화 알림',
channelDescription: '백그라운드 동기화 알림을 표시합니다'
}
포그라운드 서비스로 항상 실행됩니다. 알림이 필수입니다.
bridge.call('registerTask', {
taskId: 'always-on',
mode: 'persistent',
interval: 10000, // 최소 1초 (1000ms)
notification: { // 필수
title: '실행 중',
body: '백그라운드 서비스 동작 중'
}
});
특징:
시스템이 관리하는 효율적 실행입니다. (WorkManager/BGTaskScheduler)
bridge.call('registerTask', {
taskId: 'periodic-sync',
mode: 'efficient',
interval: 900000, // 최소 15분 (900000ms)
triggers: ['network_change'],
notification: { // 선택
title: '동기화',
body: '대기 중...'
}
});
특징:
모든 작업 이벤트를 수신합니다.
bridge.on('onTaskEvent', (event) => {
console.log('Event:', event);
});
interface TaskEvent {
taskId: string; // 작업 ID
callbackId?: string; // 등록 시 지정한 콜백 ID
type: 'started' | 'stopped' | 'restart' | 'error' | 'trigger' | 'action' | 'terminating' | 'terminated';
reason?: 'unexpected' | 'system_kill' | 'user_force_stop'; // 종료 사유 (terminated일 때)
lastStartedAt?: number; // 마지막 시작 시간 (terminated일 때)
trigger?: TriggerType; // 트리거 종류 (type이 'trigger'일 때)
customTriggerId?: string; // custom 트리거 식별자
actionId?: string; // 액션 버튼 ID (type이 'action'일 때)
error?: string; // 에러 메시지 (type이 'error'일 때)
data?: { // 트리거 관련 데이터
batteryLevel?: number;
networkType?: 'wifi' | 'cellular' | 'ethernet' | 'none';
isConnected?: boolean;
location?: { latitude: number; longitude: number };
[key: string]: unknown;
};
timestamp: number; // 타임스탬프
}
이벤트 타입:
started: 작업 시작됨stopped: 작업 중지됨restart: 작업 재시작됨error: 에러 발생trigger: 트리거 발생action: 알림 액션 클릭terminating: 백그라운드 서비스 종료 직전 (onTerminate 콜백 실행)terminated: 프로세스 강제 종료로 비정상 종료됨 (앱 재시작 시 감지)interface BackgroundTask {
taskId: string;
mode: 'persistent' | 'efficient';
interval?: number;
triggers?: TriggerConfig[];
scheduledTime?: number;
callbackId?: string;
callback?: (event: TaskEvent) => void | Promise<void>;
onTerminate?: (event: TaskEvent) => void | Promise<void>; // 종료 직전 콜백
notification?: NotificationConfig;
}
type TriggerType =
| 'interval'
| 'network_change'
| 'location_change'
| 'time_trigger'
| 'battery_low'
| 'battery_okay'
| 'battery_charging'
| 'battery_discharging'
| 'app_foreground'
| 'app_background'
| 'app_terminate'
| 'custom';
type TriggerConfig = TriggerType | {
type: TriggerType;
customId?: string;
options?: {
threshold?: number;
minDistance?: number;
networkTypes?: Array<'wifi' | 'cellular' | 'ethernet'>;
};
};
interface NotificationConfig {
taskId?: string;
title: string;
body: string;
icon?: string;
color?: string;
priority?: 'min' | 'low' | 'default' | 'high' | 'max';
ongoing?: boolean;
silent?: boolean; // 무음 알림 (기본: registerTask시 false, updateNotification시 true)
progress?: {
current: number;
max: number;
indeterminate?: boolean;
};
actions?: NotificationAction[];
channelId?: string;
channelName?: string;
channelDescription?: string;
}
interface NotificationAction {
id: string;
title: string;
icon?: string;
onPress?: (actionId: string, taskId: string) => void | Promise<void>;
dismissOnPress?: boolean; // 기본: true
bringToForeground?: boolean; // 기본: false
}
type BackgroundError =
| 'TASK_NOT_FOUND'
| 'TASK_ALREADY_EXISTS'
| 'TASK_ALREADY_RUNNING'
| 'TASK_NOT_RUNNING'
| 'PERMISSION_DENIED'
| 'NOTIFICATION_PERMISSION_DENIED' // Android 13+ 알림 권한 없음
| 'SYSTEM_RESTRICTED'
| 'WEBVIEW_INIT_FAILED'
| 'INVALID_INPUT'
| 'INVALID_INTERVAL'
| 'INVALID_TRIGGER'
| 'NOTIFICATION_REQUIRED'
| 'UNKNOWN';
작업별로 데이터를 저장/조회/삭제할 수 있습니다.
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 } }
await bridge.call('removeTaskData', { taskId: 'my-task' });
// { success: true }
백그라운드 서비스가 종료되기 직전에 실행되는 콜백입니다.
bridge.call('registerTask', {
taskId: 'my-task',
mode: 'persistent',
onTerminate: async (event) => {
// 종료 직전 상태 저장
await bridge.call('setTaskData', {
taskId: event.taskId,
data: { terminatedAt: Date.now() }
});
},
notification: { title: 'Task', body: 'Running...' }
});
주의:
terminated 이벤트로 감지됩니다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 런타임 권한이 필요합니다.startTask에서 NOTIFICATION_PERMISSION_DENIED 에러가 반환됩니다.// 권한 체크 후 작업 시작 예시
async function startBackgroundTask() {
// 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 bgPerm = await bridge.call('checkBackgroundPermission');
if (!bgPerm.batteryOptimizationExempt) {
await bridge.call('requestBackgroundPermission');
}
// 3. 백그라운드 작업 시작
const result = await bridge.call('startTask', { taskId: 'my-task' });
if (!result.success) {
console.error('시작 실패:', result.error);
}
}
Info.plist에 추가:
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>processing</string>
</array>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).background</string>
</array>
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.