브라우저 탭을 실수로 닫아 진행 중이던 2GB 동영상 다운로드가 취소된 경험이 있으신가요? Background Fetch API는 페이지 생명주기를 넘어 OS 백그라운드에서 파일 전송을 완수하는 차세대 웹 표준 기술입니다.
웹 애플리케이션이 네이티브 데스크톱 및 모바일 앱의 영역을 빠르게 대체하면서, 4K 동영상 파일, 무손실 음원 앨범, 3D WebGL 자산, 오프라인 맵 데이터 등 기가바이트(GB) 단위의 대용량 자산을 브라우저에서 다루는 일이 흔해졌습니다.
하지만 기존의 웹 네트워킹 기술 방식은 심각한 한계를 지니고 있었습니다:
fetch()나 XMLHttpRequest는 사용자가 탭을 닫거나 다른 URL로 이동하면 즉시 Abort 처리됩니다.fetch 이벤트를 이용하더라도 브라우저는 리소스 절약을 위해 백그라운드에서 Idle 상태인 서비스 워커 프로세스를 수분 내에 강제 종료(Kill)합니다.Background Fetch API는 이러한 문제를 깔끔하게 해결합니다.
Background Fetch API는 메인 스레드나 서비스 워커 프로세스가 종료되어도, 브라우저 엔진(및 운영체제의 다운로드 매니저)이 백그라운드에서 네트워크 요청을 계속해서 수행할 수 있도록 해주는 W3C 웹 API 표준입니다.
Background Fetch는 웹 개발자가 직접 네트워크 연결을 유지하는 방식이 아닙니다. **요청 매니페스트를 작성하여 브라우저에 위임(Delegate)**하는 구조입니다.
navigator.serviceWorker.ready를 통해 BackgroundFetchManager 객체에 접근하고 fetch() 메서드로 요청을 등록합니다.backgroundfetchsuccess 또는 backgroundfetchfail 이벤트를 발화합니다.Background Fetch를 등록하는 방법은 매우 간단합니다. 서비스 워커 등록 객체(ServiceWorkerRegistration)의 backgroundFetch 속성을 이용합니다.
// main.js - 메인 스레드
async function startBackgroundDownload() {
// 1. 서비스 워커 등록 확인
const registration = await navigator.serviceWorker.ready;
if (!('backgroundFetch' in registration)) {
console.warn('Background Fetch API를 지원하지 않는 브라우저입니다.');
return;
}
// 2. 백그라운드 패치 요청 등록
try {
const bgFetch = await registration.backgroundFetch.fetch(
'download-game-assets-v1', // 고유 작업 ID (Tag ID)
[
'/assets/models/character.glb',
'/assets/textures/terrain.png',
'/assets/audio/background.mp3'
], // 단일 URL 또는 URL 배열/Request 객체 배열
{
title: '대용량 게임 리소스 다운로드',
icons: [
{ src: '/images/icon-192.png', sizes: '192x192', type: 'image/png' }
],
downloadTotal: 150 * 1024 * 1024 // 총 예상 다운로드 크기 (150MB, 진행률 표시용)
}
);
console.log('Background Fetch 등록 성공:', bgFetch.id);
} catch (error) {
console.error('Background Fetch 등록 실패:', error);
}
}BackgroundFetchOptions 핵심 옵션 표| 옵션명 | 타입 | 설명 | 필수 여부 |
|---|---|---|---|
title |
string |
OS 및 브라우저 다운로드 UI 알림창에 표기될 제목 | 권장 |
icons |
Array<Icon> |
다운로드 알림창에 표시될 이모지/아이콘 이미지 리스트 | 선택 |
downloadTotal |
number |
총 다운로드 예상 크기(Bytes). 브라우저가 정확한 % 진행률바를 계산할 때 사용 | 권장 |
페이지가 열려 있는 동안에는 메인 스레드에서 BackgroundFetchRegistration 객체의 progress 이벤트를 구독하여 실시간 UI를 업데이트할 수 있습니다.
// main.js
async function trackProgress(bgFetchId) {
const registration = await navigator.serviceWorker.ready;
const bgFetch = await registration.backgroundFetch.get(bgFetchId);
if (!bgFetch) return;
bgFetch.addEventListener('progress', () => {
// bgFetch.downloaded: 지금까지 다운로드된 바이트 수
// bgFetch.downloadTotal: 전체 바이트 수 (등록 시 지정한 값)
const percent = Math.round((bgFetch.downloaded / bgFetch.downloadTotal) * 100);
console.log(`현재 진행률: ${percent}% (${bgFetch.downloaded} / ${bgFetch.downloadTotal} bytes)`);
// DOM 진행률 바 업데이트
const progressBar = document.getElementById('my-progress-bar');
if (progressBar) {
progressBar.value = percent;
}
});
}탭이 닫힌 상태에서 다운로드가 끝나면 서비스 워커가 이벤트를 받아서 후속 조치를 취해야 합니다.
// sw.js - 서비스 워커 스레드
// 1. 모든 파일 다운로드가 성공적으로 완료된 경우
self.addEventListener('backgroundfetchsuccess', (event) => {
const bgFetch = event.registration;
event.waitUntil((
async () => {
// Cache Storage 열기
const cache = await caches.open('offline-game-assets-v1');
// 백그라운드 패치로 전달받은 모든 요청/응답 쌍 가져오기
const records = await bgFetch.matchAll();
const recordPromises = records.map(async (record) => {
const response = await record.responseReady;
// Cache Storage에 저장
await cache.put(record.request, response);
});
await Promise.all(recordPromises);
// 성공 상태 알림 표시
await event.updateUI({ title: '모든 게임 자산 다운로드 완료!' });
}
)());
});
// 2. 다운로드가 실패했거나 취소된 경우
self.addEventListener('backgroundfetchfail', (event) => {
console.error('Background fetch 실패:', event.registration.id);
// 실패한 리소스 정리 작업 수행 가능
});
// 3. 사용자가 브라우저 OS 알림창 UI를 클릭했을 때
self.addEventListener('backgroundfetchclick', (event) => {
const bgFetch = event.registration;
event.waitUntil((
async () => {
// 이미 열려 있는 탭이 있다면 포커스, 없으면 새 창 열기
const windowClients = await self.clients.matchAll({ type: 'window' });
for (const client of windowClients) {
if (client.url.includes('/download-page') && 'focus' in client) {
return client.focus();
}
}
if (self.clients.openWindow) {
return self.clients.openWindow('/download-page');
}
}
)());
});네트워크 요청을 수행하는 웹 표준 기술들의 특성을 명확히 비교해 보겠습니다.
| 구분 | 일반 fetch() |
SW fetch Intercept |
Background Fetch API |
|---|---|---|---|
| 수행 주체 | 메인 JS 스레드 | 서비스 워커 스레드 | OS / 브라우저 네이티브 엔진 |
| 탭 닫힘 지속 여부 | ❌ 즉시 취소 | ⚠️ SW 수명(수분) 후 중단 | ✅ 완료 시까지 완벽 지속 |
| 네이티브 진행 UI | ❌ 직접 구현 필요 | ❌ 직접 구현 필요 | ✅ OS/브라우저 알림창 자동 제공 |
| 자동 재요청(Auto Retry) | ❌ 직접 작성 | ❌ 직접 작성 | ✅ 브라우저 차원 자동 지원 |
| 업로드 지원 | ✅ 가능 | ✅ 가능 | ✅ 가능 (대용량 POST/PUT 지원) |
| 적합한 용도 | API 통신, 소형 자원 | 동적 Caching, PWA 오프라인 | GB 단위 자산, 오프라인 미디어 다운로드 |
Cache Storage는 크기가 수백 MB를 넘어서면 정리를 당할 위험이 있습니다. **Origin Private File System (OPFS)**과 연계하면 백그라운드 패치로 받은 GB 단위 대용량 파일을 고성능 로컬 파일 시스템에 안전하게 보관할 수 있습니다.
// sw.js - Background Fetch + OPFS 연동
self.addEventListener('backgroundfetchsuccess', (event) => {
const bgFetch = event.registration;
event.waitUntil((
async () => {
// OPFS 루터 디렉토리 확보
const root = await navigator.storage.getDirectory();
const mediaFolder = await root.getDirectoryHandle('downloaded_videos', { create: true });
const records = await bgFetch.matchAll();
for (const record of records) {
const response = await record.responseReady;
const blob = await response.blob();
// URL 패턴 기반 파일명 추출
const url = new URL(record.request.url);
const fileName = url.pathname.split('/').pop() || 'file.bin';
// OPFS 파일 핸들 생성 및 작성
const fileHandle = await mediaFolder.getFileHandle(fileName, { create: true });
const accessHandle = await fileHandle.createWritable();
await blob.stream().pipeTo(accessHandle);
}
await event.updateUI({ title: 'OPFS 저장 완료!' });
}
)());
});Background Fetch API를 도입할 때 반드시 경계해야 하는 실무 함정들과 유의사항입니다.
Access-Control-Allow-Origin: * 또는 허용 도메인이 설정되어 있지 않으면 backgroundfetchfail 이벤트가 발생하며 전체 다운로드가 실패합니다.401 Unauthorized를 반환합니다.navigator.storage.estimate()를 사용하여 사용자의 남은 디스크 용량을 사전에 확인해야 합니다. 디스크 용량이 부족하면 브라우저가 패치를 즉시 중단시킵니다.https:// 및 localhost)에서만 작동합니다.backgroundfetchsuccess 이벤트를 통해 Cache Storage나 OPFS에 데이터를 영구 저장할 수 있습니다.✨ 한 줄 요약 페이지가 꺼져도 멈추지 않는 대용량 웹 I/O, Background Fetch API와 서비스 워커로 완벽 구현하자!




