오프라인 전송을 위한 서비스 워커 파일 캐싱
서비스 워커를 사용해 오프라인 파일 전송 기능을 활성화하세요. 캐시 전략, 백그라운드 동기화, 프로그레시브 웹 앱 패턴입니다.
서비스 워커는 네트워크가 끊겨도 파일 전송 앱이 계속 작동하게 합니다. Cache API로 HTML 셸과 JS를 캐시하고, Background Sync API로 실패한 업로드를 큐에 넣고, 부분 청크를 IndexedDB에 저장하고, 연결이 복구되면 모든 것을 재실행합니다. 워커는 자체 이벤트 루프를 가진 별도 스레드에서 실행되고, 자신의 범위에 대한 fetch 이벤트를 가로채며, 탭이 닫혀도 지속됩니다. 업로드 도구의 올바른 레시피: 앱 셸에는 stale-while-revalidate 전략, 진행 중인 전송에는 IndexedDB 기반 청크 큐잉, 성공할 때까지 15분마다 업로드를 재시도하는 주기적 Background Sync 등록.
서비스 워커가 전송 앱에 실제로 하는 일
가장 큰 이점은 navigator.serviceWorker가 탭 재로드, 오프라인 기간, 심지어 폰 슬립에서도 살아남는다는 것입니다. 사용자가 불안정한 열차 Wi-Fi에서 2 GB 업로드를 시작하면, 이미 성공한 청크는 성공 상태를 유지하고, 진행 중인 청크는 재연결 시 재시도하며, 브라우저가 메모리 회수를 위해 탭을 종료해도 전체 상태가 복구 가능해야 합니다. 특정 탭과 독립적으로 실행되는 서비스 워커가 이 모든 것을 가능하게 하는 요소입니다.
API는 세 가지 구성 요소를 제공합니다. URL로 응답을 저장하는 Cache, 구조화된 데이터(청크 큐, 세션 상태)를 위한 IndexedDB, 기기가 온라인일 때 발생하는 재시도를 예약하는 SyncManager.
워커 등록 및 버전 관리
앱 로드 시 한 번 등록하고 업데이트를 명시적으로 처리하세요.
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js', { scope: '/' })
.then((reg) => reg.addEventListener('updatefound', () => {
const sw = reg.installing;
sw.addEventListener('statechange', () => {
if (sw.state === 'installed' && navigator.serviceWorker.controller) {
// 새 버전 준비됨, 사용자에게 새로고침 요청
}
});
}));
}
새 배포 시 오래된 JavaScript를 남기지 않고 이전 자산을 무효화하도록 캐시 키에 버전을 지정하세요(transfer-v7). 고전적인 버그: index.html이 영원히 캐시되어 사용자가 업데이트를 받지 못하고 몇 주 동안 Slack으로 디버깅합니다. 캐시를 빌드 해시에 고정하고 activate 이벤트에서 정리하세요.
앱 셸 대 사용자 데이터를 위한 캐싱 전략
다른 리소스는 다른 전략이 필요합니다.
- 앱 셸(HTML, CSS, JS, 아이콘): 네트워크 폴백이 있는 캐시 우선. 즉각적인 로드, 오프라인 작동.
- API 메타데이터(
/shares/:id): 60초 TTL이 있는 캐시 폴백이 있는 네트워크 우선. 온라인일 때 신선하고 아닐 때도 사용 가능. - 파일 바이트: 절대 캐시하지 마세요. 파일은 종종 멀티 기가바이트이며 Cache API는 오리진 할당량이 있습니다(일반적으로 빈 디스크의 60%).
- CDN의 폰트: stale-while-revalidate. 빠르고 자동 갱신.
fetch 핸들러에서:
self.addEventListener('fetch', (e) => {
const url = new URL(e.request.url);
if (url.pathname.startsWith('/assets/')) {
e.respondWith(cacheFirst(e.request, 'shell-v7'));
} else if (url.pathname.startsWith('/api/shares/')) {
e.respondWith(networkFirst(e.request, 'api-v1', 60));
}
});
바이너리 파일 업로드 요청은 절대 가로채지 마세요. e.request.method === 'PUT'을 확인하고 일찍 반환하여 우회하세요. 워커를 통해 기가바이트 PUT를 프록시하면 메모리 재앙이 됩니다.
Background Sync로 실패한 업로드 큐잉
SyncManager는 탄력적인 업로드의 핵심입니다. 청크 PUT가 실패하면 IndexedDB에 저장하고 동기화를 등록하세요.
// 페이지 코드에서
const reg = await navigator.serviceWorker.ready;
await reg.sync.register('flush-uploads');
// sw.js에서
self.addEventListener('sync', (event) => {
if (event.tag === 'flush-uploads') {
event.waitUntil(flushPendingUploads());
}
});
브라우저는 네트워크가 돌아오면 지수 백오프를 사용해 최대 약 24시간까지 sync 이벤트를 발생시킵니다. Chrome과 Edge가 지원하고, Safari는 17.5에서 "Background Fetch" 플래그로 서브셋을 출시했지만 사용자 권한이 필요합니다. Safari의 경우 visibilitychange를 통해 다음 페이지 열기 시 재시도로 폴백하세요.
Background Fetch API는 대용량 파일 작업을 위한 별도 도구입니다. 탭을 닫은 후에도 사용자가 진행 상황을 추적할 수 있도록 브라우저 UI에 영구 알림을 표시합니다. 500 MB 이상의 업로드에 사용할 가치가 있습니다.
IndexedDB에 부분 업로드 저장
IndexedDB는 내구성 있는 임시 공간입니다. 한 번 소규모 데이터베이스를 열고 세션 메타데이터와 청크 오프셋을 저장하세요.
const db = await openDB('transfers', 1, {
upgrade(db) {
db.createObjectStore('sessions', { keyPath: 'id' });
db.createObjectStore('chunks', { keyPath: ['sessionId', 'index'] });
}
});
await db.put('sessions', {
id, fileName, fileSize, fileFingerprint, createdAt: Date.now(),
completedIndexes: [], partUrls
});
원시 청크 바이트는 저장하지 마세요. 바이트는 File 핸들에서 오며, IndexedDB는 재로드에서도 유효한 구조화된 클론 참조로 유지할 수 있습니다. 핸들을 저장하면 데이터베이스에 2 GB의 바이트를 중복하는 것을 피할 수 있습니다.
오리진 스토리지에는 한계가 있습니다. 데스크톱 Chrome에서 약 빈 디스크의 60%, iOS Safari에서 오리진당 1 GB(제거 압력이 시작되기 전). 브라우저가 자동으로 제거하지 않는 "영구" 버킷을 얻으려면 navigator.storage.persist()를 요청하세요.
오프라인 및 온라인 전환 처리
페이지와 서비스 워커 모두에서 online과 offline 이벤트를 수신하세요.
// 페이지
window.addEventListener('online', () => {
ui.showBanner('인터넷 연결 복구 — 업로드 재개 중');
navigator.serviceWorker.controller?.postMessage({ type: 'resume' });
});
window.addEventListener('offline', () => {
ui.showBanner('오프라인 — 업로드 일시 중지됨');
});
navigator.onLine은 기업 캡티브 포털에서 악명 높을 정도로 신뢰할 수 없습니다. 기기에 로컬 네트워크 연결은 있지만 인터넷이 없는 경우 true를 보고합니다. 신뢰할 수 있는 감지를 위해 3초 타임아웃으로 작은 fetch('/ping', { cache: 'no-store' })를 수행하세요.
제대로 된 PWA 만들기
display: standalone, 아이콘 세트, start_url: /이 있는 manifest.json을 제공하세요. iOS를 위한 apple-touch-icon 링크를 추가하세요. OS가 앱을 특정 확장자와 연결할 수 있도록 파일 처리를 선언하세요.
{
"name": "Hex Transfer",
"file_handlers": [{
"action": "/share-target",
"accept": { "application/*": [".pdf", ".zip", ".docx"] }
}]
}
Web Share Target과 결합하면 사용자가 OS 공유 시트에서 직접 앱으로 파일을 공유할 수 있습니다. Chrome Android와 데스크톱 Chromium에서 PWA가 선언한 파일 유형의 기본 핸들러로 등록할 수 있습니다. 이렇게 하면 브라우저 페이지가 네이티브 업로드 도구처럼 동작하는 앱이 됩니다.
오프라인 시나리오 테스트
자동화된 오프라인 테스트가 불안정하므로 수동으로 테스트할 세 가지 시나리오:
- 빠른 Wi-Fi에서 500 MB 업로드를 시작하고 30%에서 비행기 모드로 전환하고 30초 기다린 뒤 Wi-Fi를 다시 켭니다. 업로드가 사용자 작업 없이 중단된 곳에서 재개되어야 합니다.
- 업로드를 시작하고 60%에서 탭을 닫고 2분 기다린 뒤 다시 엽니다. 세션 재개를 제안해야 합니다.
- 모바일에서 업로드를 시작하고 5분 동안 화면을 잠급니다. 잠금을 해제하면 백그라운드 동기화가 발생해 전송을 완료해야 합니다.
Chrome DevTools의 "오프라인" 체크박스와 Application > Service Workers > Update on reload는 필수입니다. 네트워크 패널의 "스로틀링" 프로필로 Fast 3G와 Slow 3G를 시뮬레이션해 오류 UI의 동작을 확인하세요.
HexaTransfer의 웹 앱은 앱 셸 캐싱을 위한 서비스 워커와 진행 중인 세션 상태를 위한 IndexedDB를 사용합니다. 재로드와 짧은 오프라인 기간이 업로드 진행 상황을 잃지 않습니다. hexatransfer.com에서 사용해보세요 — 무료, 계정 불필요, 최대 10 GB.
알아두어야 할 함정
서비스 워커에는 초보자가 물리는 작은 문제들이 있습니다. HTTPS에서만 작동합니다(localhost 제외). 캐시 할당량은 브라우저마다 크게 다릅니다. iOS Safari는 Background Sync를 위해 워커를 신뢰성 있게 깨우지 않습니다. DevTools는 오래된 워커를 공격적으로 캐시할 수 있습니다(개발 중 항상 "Bypass for network" 클릭). importScripts는 설치 중 동기적으로 실행되므로 느린 서드파티 스크립트를 절대 거기서 가져오지 마세요. 워커가 활성화되고, 클라이언트를 클레임하고, 오프라인 페이지를 제공하는지 확인하는 작은 통합 테스트를 작성하세요. 그 하나의 테스트가 프로덕션에서 겪을 회귀의 80%를 잡습니다.
엔드투엔드 암호화로 대용량 파일을 안전하게 전송
엔드투엔드 암호화로 최대 10GB의 파일을 무료로 전송하세요. 계정이 필요하지 않습니다. 업로드 전에 브라우저에서 파일이 암호화되어 다른 사람은 읽을 수 없습니다.
파일 보내기