npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

rnww-plugin-background

v1.7.3

Published

React Native WebView Background Execution Plugin with Expo support

Downloads

107

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에서 AppBridge API를 사용할 수 있습니다
  • 기존 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 WebViewHeadless 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