필사 모드: PWA & Service Workers 2026 — Workbox 7 / vite-pwa / Capacitor / TWA / iOS Web Push / Storage Buckets 심층 가이드
한국어프롤로그 — "PWA는 죽었다"는 말은 2026년에 두 번째로 틀린 예측이 되었다
2019년 즈음, 한국과 일본의 컨퍼런스 무대에서 가장 많이 들렸던 말은 "PWA는 죽었다"였다. 이유는 단순했다 — iOS가 Web Push를 막았고, 홈 화면 추가 UX가 끔찍했고, Service Worker는 디버깅 지옥이었고, 결국 React Native와 Flutter가 시장을 가져갔다.
2026년 5월, 그 예측은 두 번째로 틀렸다.
- **iOS 16.4(2023년 3월)** 이후 Web Push가 정식으로 동작한다. APN을 거치지만 표준 Push API다.
- **iOS 17.4(2024년 2월)** — 애플이 EU의 DMA(Digital Markets Act) 시행을 앞두고 홈 화면 웹앱을 잠시 제거했다가, 개발자·EU 위원회·언론의 일제 반발에 **3주 만에 복구**했다.
- **Workbox 7**(Google, 2023~2025)이 캐시 전략의 표준이 되었고, 더 이상 손으로 Service Worker를 쓸 이유가 거의 없다.
- **vite-pwa / next-pwa / Astro PWA**가 프레임워크 통합을 제공한다.
- **Capacitor 6 / PWABuilder 3 / TWA**가 "스토어에 올리고 싶다"는 마지막 욕망까지 해결한다.
- **Storage Buckets API**(Chrome 122, 2024년 2월)가 스토리지를 파티션 단위로 다룬다.
- **View Transitions cross-document**(Chrome 126, 2024년 6월)가 MPA에서도 SPA 같은 전환을 만든다.
- **Speculation Rules**가 다음 페이지를 prerender해서 클릭이 0ms처럼 느껴지게 한다.
이 글은 그 풍경을 — 정치 드라마부터 코드 예시, 그리고 한국·일본 사례까지 — 한 호흡으로 정리한다.
1장 · 2026년 PWA 지도 — 부활인가 또 다른 침체인가
먼저 그림 한 장. "PWA"라는 단어가 가리키는 건 사실 **다섯 가지가 합쳐진 것**이다.
[웹 페이지]
|
v
1. Manifest (web app manifest) -- 아이콘·이름·테마·display:standalone
|
v
2. Service Worker -- 백그라운드 스레드·캐시·인터셉트
|
v
3. HTTPS -- Service Worker의 전제
|
v
4. Installable -- 홈 화면 추가·beforeinstallprompt
|
v
5. Capable APIs -- Push·Notification·Bluetooth·FS Access·...
이 다섯 가지가 다 있으면 PWA, 셋 정도면 "Installable Web", 두 개면 그냥 웹사이트다.
2026년의 변화는 1·2·3은 거의 바뀌지 않았는데, **4와 5에서 큰 일이 일어났다는 것**이다.
| 영역 | 2019년 상태 | 2026년 상태 |
| ---------------- | ------------------------ | -------------------------------------------------------------------------- |
| iOS Web Push | 안 됨 | 동작 (iOS 16.4+) |
| iOS 홈 화면 웹앱 | 동작하지만 한정적 | 동작, 한 차례 제거 위기 후 복구 |
| Push API | Chrome·Firefox·Edge 한정 | 모든 메이저 브라우저 |
| File System | 다운로드만 | File System Access API (Chrome/Edge) |
| 스토리지 | 단일 origin 풀 | Storage Buckets로 파티션 가능 |
| 페이지 전환 | SPA에서만 부드러움 | View Transitions cross-document로 MPA도 가능 |
| Prerender | rel=prefetch만 | Speculation Rules |
| 프레임워크 통합 | 손으로 다 짜야 함 | vite-pwa·next-pwa·Astro PWA |
| 스토어 배포 | 어렵거나 불가 | PWABuilder·Capacitor·TWA |
부활인가? 절반은 그렇다. **"App Store가 끝이다"라는 시대는 끝났다.** 하지만 동시에 React Native·Flutter·Capacitor가 차지한 영역도 그대로다. 2026년의 PWA는 "또 하나의 선택지"가 되었고, 그게 가장 건강한 위치다.
2장 · iOS Web Push (iOS 16.4+) — 마침내 도착
PWA를 죽인 단 하나의 결격 사유가 iOS Web Push의 부재였다. 2023년 3월 iOS 16.4가 그걸 끝냈다.
조건은 명확하다.
1. 홈 화면에 추가된 PWA여야 한다 (브라우저 탭에서는 안 된다).
2. 사용자가 명시적으로 `Notification.requestPermission()`을 호출한 결과 `granted`여야 한다.
3. 사이트는 HTTPS여야 한다.
4. `display: standalone` 또는 `display: fullscreen`이어야 한다.
코드는 표준 Push API 그대로다.
// 1) 권한 요청 — 반드시 사용자 제스처(click) 안에서
button.addEventListener('click', async () => {
const permission = await Notification.requestPermission()
if (permission !== 'granted') return
const reg = await navigator.serviceWorker.ready
const subscription = await reg.pushManager.subscribe({
userVisibleOnly: true, // iOS는 무조건 true여야 함
applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY),
})
// 서버로 구독 정보 전송
await fetch('/api/push/subscribe', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(subscription),
})
})
function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - (base64String.length % 4)) % 4)
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/')
const raw = atob(base64)
return Uint8Array.from([...raw].map((c) => c.charCodeAt(0)))
}
Service Worker 쪽도 표준 그대로.
// sw.js
self.addEventListener('push', (event) => {
const data = event.data?.json() ?? { title: 'New', body: '' }
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.body,
icon: '/icons/icon-192.png',
badge: '/icons/badge-72.png',
data: { url: data.url },
})
)
})
self.addEventListener('notificationclick', (event) => {
event.notification.close()
event.waitUntil(clients.openWindow(event.notification.data.url))
})
iOS의 함정.
- **APNs 경유.** 애플이 표준 Web Push 프로토콜을 APN으로 변환해서 보낸다. 백엔드에서 보내는 코드는 그대로지만, 도착 지연이 Android보다 평균 2~5초 더 있다.
- **`userVisibleOnly: true` 필수.** 사용자에게 보이지 않는 silent push는 iOS에서 불가능하다.
- **권한 요청은 반드시 사용자 제스처에서.** 페이지 로드 직후 자동으로 호출하면 무조건 거부된다.
- **알림 권한 UX가 OS 설정으로 깊이 묻혀 있다.** 한번 거부하면 사용자가 직접 설정 앱에서 되돌려야 한다.
체감으로 말하면 **Android Chrome의 95% 수준은 된다.** 카카오·라인·메르카리가 다 채택한 이유가 여기에 있다.
3장 · iOS 17.4 PWA 제거 → 복구 (2024.2) — EU 정치 드라마
2024년 2월, 애플은 iOS 17.4 베타에 충격적인 변경을 넣었다 — **EU 사용자의 홈 화면 웹앱이 일반 브라우저 단축아이콘처럼 동작한다.** 전체 화면도, Service Worker도, Push도, 별도 스토리지도 없어졌다.
이유는 DMA(Digital Markets Act). EU의 DMA는 "게이트키퍼"인 Apple에게 제3자 브라우저 엔진 허용을 강제했다. 애플의 논리는 **"Safari가 아닌 엔진이 PWA를 호스팅하면 보안·프라이버시 모델을 모든 엔진에 대해 만들어야 하는데, 그건 우리 일정에 없다"**였다.
업계 반응.
- Mozilla, Vivaldi, Open Web Advocacy 같은 단체가 일제히 EU 위원회에 항의서를 냈다.
- 영국·EU·미국의 개발자 커뮤니티가 "이건 DMA 우회"라며 들고일어났다.
- EU 위원회는 공식적으로 "조사하겠다"고 발표했다.
**3주 후 애플은 결정을 뒤집었다.** iOS 17.4 정식 릴리스(2024년 3월 5일)에서 PWA는 EU에서도 그대로 동작하게 되었다. 다만 "WebKit 전용"이라는 조건이 붙었다 — 다른 브라우저 엔진을 쓰는 PWA는 여전히 불가능하다.
| 일자 | 사건 |
| ------------- | ------------------------------------------------- |
| 2023.3 | iOS 16.4 — Web Push 정식 지원 |
| 2024.2 초 | iOS 17.4 베타 — EU PWA 제거 발표 |
| 2024.2 중 | Mozilla·OWA·EU 위원회 항의 시작 |
| 2024.2.27 | 애플 입장 발표 — "복구하겠다" |
| 2024.3.5 | iOS 17.4 정식 — PWA 복구 (WebKit 한정) |
| 2025 | iOS 18 시리즈 — PWA 동작은 안정, Web Push 확대 |
| 2026 현재 | iOS 18.x — PWA + Web Push가 표준 동작 |
이 드라마는 PWA 진영에 두 가지를 가르쳤다.
1. **정치적 위기에 약하다.** 플랫폼 한 곳의 결정으로 글로벌 사용자 절반이 사라질 수 있다.
2. **그래도 살아남는다.** 충분히 많은 개발자가 화내면 결정은 뒤집힌다.
4장 · Service Worker 2026 — 무엇이 바뀌었나
Service Worker 자체의 명세는 2014년부터 거의 그대로다. 바뀐 건 **주변 생태계와 모범 사례**다.
핵심 라이프사이클은 기억해두면 좋다.
register
|
v
parse install --(skipWaiting?)--> activate
| |
| v
| fetch / push / sync / message
| |
| v
| ... 새 버전 install
| |
| v
| waiting (controlled by old)
| |
+-- update found ---------------------+
2026년의 모범 사례.
1. **`skipWaiting()`은 신중하게.** 즉시 활성화하면 이미 열린 탭의 클라이언트가 새 SW와 통신하면서 충돌할 수 있다. **사용자에게 "업데이트가 있어요, 새로고침" 토스트를 보여주는 패턴**이 표준이다.
2. **`fetch` 핸들러는 짧게.** 핸들러 내부에서 동기 코드가 길면 모든 네트워크 요청이 느려진다.
3. **타임아웃을 직접 걸어라.** `fetch()`에는 기본 타임아웃이 없다. `AbortController`를 써라.
4. **버전 관리는 명시적으로.** `CACHE_VERSION` 상수를 두고 활성화 시 옛 캐시를 청소하라.
가장 작은 Service Worker.
// sw.js
const CACHE_VERSION = 'v2026-05-16'
const PRECACHE = `precache-${CACHE_VERSION}`
const RUNTIME = `runtime-${CACHE_VERSION}`
const PRECACHE_URLS = ['/', '/offline.html', '/styles.css', '/app.js']
self.addEventListener('install', (event) => {
event.waitUntil(
caches
.open(PRECACHE)
.then((cache) => cache.addAll(PRECACHE_URLS))
.then(() => self.skipWaiting())
)
})
self.addEventListener('activate', (event) => {
const valid = new Set([PRECACHE, RUNTIME])
event.waitUntil(
caches
.keys()
.then((keys) => Promise.all(keys.filter((k) => !valid.has(k)).map((k) => caches.delete(k))))
.then(() => self.clients.claim())
)
})
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url)
if (event.request.method !== 'GET') return
if (url.origin !== self.location.origin) return
// 네트워크 우선, 실패하면 캐시
event.respondWith(
fetch(event.request)
.then((res) => {
const copy = res.clone()
caches.open(RUNTIME).then((cache) => cache.put(event.request, copy))
return res
})
.catch(() => caches.match(event.request).then((r) => r || caches.match('/offline.html')))
)
})
이게 길게 느껴진다면 정상이다. **2026년에는 거의 모두 Workbox로 쓴다.**
5장 · Workbox 7 — 표준 라이브러리
Workbox는 구글이 만든 Service Worker 라이브러리다. Workbox 7(2024년 1월 릴리스, 이후 7.x로 안정화)가 사실상 표준이다.
핵심 모듈.
- `workbox-routing` — 라우팅. `registerRoute(matcher, handler)`.
- `workbox-strategies` — 캐시 전략 5종. `CacheFirst`, `NetworkFirst`, `StaleWhileRevalidate`, `CacheOnly`, `NetworkOnly`.
- `workbox-precaching` — 빌드 매니페스트 기반 프리캐시.
- `workbox-expiration` — TTL·LRU.
- `workbox-background-sync` — Background Sync API 래퍼.
- `workbox-broadcast-update` — 새 응답이 있을 때 메인 스레드에 알림.
- `workbox-window` — 메인 스레드 쪽 SW 관리 헬퍼.
다섯 줄짜리 Workbox SW.
// sw.js
// 빌드 시 주입되는 매니페스트 (vite-pwa / next-pwa가 자동으로 채움)
precacheAndRoute(self.__WB_MANIFEST)
// 이미지 — 캐시 우선, 30일 보존, 최대 60개
registerRoute(
({ request }) => request.destination === 'image',
new CacheFirst({
cacheName: 'images',
plugins: [new ExpirationPlugin({ maxEntries: 60, maxAgeSeconds: 30 * 24 * 60 * 60 })],
})
)
// API — 네트워크 우선, 3초 타임아웃, 실패 시 캐시
registerRoute(
({ url }) => url.pathname.startsWith('/api/'),
new NetworkFirst({ cacheName: 'api', networkTimeoutSeconds: 3 })
)
// 페이지 — Stale-While-Revalidate (옛 캐시 즉시 반환, 백그라운드에서 갱신)
registerRoute(
({ request }) => request.mode === 'navigate',
new StaleWhileRevalidate({ cacheName: 'pages' })
)
다섯 줄에 일주일치 사고가 들어갔다. 손으로 짜면 100줄이 넘는 코드다.
전략을 언제 쓰는지가 PWA 설계의 절반이다.
| 전략 | 언제 |
| -------------------- | -------------------------------------------------------- |
| CacheFirst | 이미지·폰트·해시된 JS — 거의 변하지 않는 정적 자산 |
| NetworkFirst | API·뉴스피드 — 최신성이 중요 |
| StaleWhileRevalidate | HTML 페이지·CSS — 즉시 표시 + 백그라운드 갱신 |
| CacheOnly | 오프라인 전용 자산 |
| NetworkOnly | POST·결제·로그 — 절대 캐시하면 안 되는 요청 |
6장 · vite-pwa / next-pwa / Astro PWA — 프레임워크 통합
손으로 sw.js를 만지는 시대는 끝났다. 2026년에는 빌드 시스템이 알아서 해준다.
vite-pwa
`@vite-pwa/vite`(2024~2025년에 가장 빠르게 성장)가 사실상 Vite의 표준이다.
// vite.config.ts
export default defineConfig({
plugins: [
VitePWA({
registerType: 'autoUpdate', // 새 SW 발견 시 자동 업데이트
injectRegister: 'auto',
workbox: {
globPatterns: ['**/*.{js,css,html,svg,png,ico,woff2}'],
runtimeCaching: [
{
urlPattern: /^https:\/\/api\.example\.com\//,
handler: 'NetworkFirst',
options: { cacheName: 'api', networkTimeoutSeconds: 3 },
},
],
},
manifest: {
name: 'My App',
short_name: 'MyApp',
theme_color: '#000000',
background_color: '#ffffff',
display: 'standalone',
icons: [
{ src: '/icons/icon-192.png', sizes: '192x192', type: 'image/png' },
{ src: '/icons/icon-512.png', sizes: '512x512', type: 'image/png' },
{
src: '/icons/maskable-512.png',
sizes: '512x512',
type: 'image/png',
purpose: 'maskable',
},
],
},
}),
],
})
이게 전부다. `vite build`만 하면 sw.js, manifest.webmanifest, 매니페스트 주입이 다 자동.
React 쪽 등록은.
const updateSW = registerSW({
onNeedRefresh() {
if (confirm('새 버전이 있습니다. 새로고침할까요?')) updateSW(true)
},
onOfflineReady() {
console.log('오프라인에서 사용 가능합니다')
},
})
next-pwa
Next.js는 사정이 좀 복잡하다. App Router(13.4+)가 RSC를 도입하면서 SW와 충돌하는 부분이 늘었고, 메인테이너 shadowwalker의 `next-pwa`(2018~)가 2024년 후반에 손을 떼면서, 커뮤니티가 `@ducanh2912/next-pwa`로 옮겨갔다. **2026년 5월 현재 가장 많이 쓰이는 패키지는 `@ducanh2912/next-pwa` 또는 `@serwist/next`**이다.
// next.config.js
const withPWA = require('@ducanh2912/next-pwa').default({
dest: 'public',
cacheOnFrontEndNav: true, // App Router 네비게이션 캐시
aggressiveFrontEndNavCaching: true,
reloadOnOnline: true,
swcMinify: true,
workboxOptions: {
disableDevLogs: true,
},
})
module.exports = withPWA({
reactStrictMode: true,
})
App Router 함정 — RSC payload(`?_rsc=`) 요청을 캐시해버리면 콘텐츠가 영구히 오래된 상태로 멈춘다. 최신 `@ducanh2912/next-pwa`는 이걸 자동으로 회피하지만, 직접 워크박스 라우트를 짠다면 RSC 요청 패턴을 NetworkOnly로 빼야 한다.
Astro PWA
`@vite-pwa/astro` — Astro는 빌드 산물이 정적이라 PWA와 궁합이 좋다.
// astro.config.mjs
export default {
integrations: [
AstroPWA({
registerType: 'autoUpdate',
manifest: { /* ... */ },
workbox: { /* ... */ },
}),
],
}
블로그·문서·랜딩에서 가장 잘 동작한다. 동적 라우트가 적을수록 PWA가 단순해진다는 일반 법칙의 한 사례.
7장 · Capacitor — Ionic 팀
Capacitor는 "내 웹앱을 네이티브 셸로 감싸서 App Store에 올리고 싶다"는 욕망의 답이다. Ionic 팀이 만들었고, 2026년 5월 기준 **Capacitor 6**(2024년 4월 메이저, 이후 6.x 안정화)이 안정 버전이다.
새 Capacitor 프로젝트 (이미 있는 웹앱에 얹기)
npm i @capacitor/core @capacitor/cli
npx cap init my-app com.example.myapp --web-dir=dist
npm i @capacitor/ios @capacitor/android
npx cap add ios
npx cap add android
빌드 후 동기화
npm run build
npx cap sync
npx cap open ios # Xcode 열기
npx cap open android # Android Studio 열기
Capacitor 플러그인 생태계.
| 플러그인 | 용도 |
| ----------------------------------- | ----------------------------------- |
| @capacitor/camera | 카메라·갤러리 |
| @capacitor/filesystem | 파일 시스템 |
| @capacitor/push-notifications | APN·FCM |
| @capacitor/geolocation | 위치 |
| @capacitor/preferences | 키-값 저장소 |
| @capacitor/share | OS 공유 시트 |
| @capacitor/biometric | Face ID·지문 |
| @capacitor/in-app-browser | 내부 브라우저 |
| @capacitor/local-notifications | 로컬 알림 |
| @capacitor/app | 앱 라이프사이클 |
호출 패턴은 표준 JS API와 매우 비슷하다.
const photo = await Camera.getPhoto({
quality: 90,
allowEditing: false,
resultType: CameraResultType.Uri,
})
imgElement.src = photo.webPath
Capacitor의 장점.
- 웹앱 한 벌로 iOS·Android·웹 모두 커버.
- 네이티브 API가 필요할 때만 플러그인을 끼움 — 나머지는 그냥 웹.
- React Native와 달리 **렌더링은 WebView**라서 웹 기술 그대로 쓸 수 있음.
단점.
- WebView 한 단계가 끼어 있어 React Native보다 미세하게 느림.
- iOS에서는 WKWebView, Android에서는 Chrome Custom Tabs — 둘의 동작 차이가 가끔 디버깅 함정.
- 앱 스토어 심사 — "단순한 웹뷰 래퍼"라는 이유로 거절되는 경우가 종종 있음. 네이티브 기능을 최소 하나는 써야 한다.
8장 · PWABuilder (MS) — 스토어 래핑
마이크로소프트가 만든 PWABuilder는 **PWA URL 하나만 넣으면 모든 스토어용 패키지를 만들어주는 마법사**다. https://www.pwabuilder.com 에 URL을 넣으면 점수와 함께 다음을 다운로드받을 수 있다.
- **Microsoft Store(MSIX)** — Windows
- **Google Play(Android, TWA 기반)** — `.aab` 파일
- **App Store(iOS, Capacitor 기반)** — Xcode 프로젝트
- **Meta Quest Store** — VR 앱
- **Samsung Galaxy Store** — Android 변종
CLI로도 쓸 수 있다.
npm i -g @pwabuilder/cli
Android 패키지 생성 (TWA)
pwabuilder package --type android --url https://example.com
iOS 패키지 생성
pwabuilder package --type ios --url https://example.com
Windows 패키지 생성
pwabuilder package --type windows --url https://example.com
생성된 Android 패키지를 그대로 Play Console에 올리면 끝이다. 서명만 따로 해야 한다.
PWABuilder의 진짜 가치.
- **마니페스트 점수.** PWABuilder는 manifest, SW, 보안, 아이콘, 메타데이터를 다 검사하고 점수를 준다. "100점 받기" 자체가 좋은 체크리스트다.
- **퀄리티 게이트.** Google Play는 이미 PWA 패키지를 받지만, 마이크로소프트가 검증해준 패키지라는 신뢰가 따라온다.
9장 · TWA (Trusted Web Activities) — Android Chrome
TWA는 Chrome Custom Tabs의 확장판이다. **"내 도메인의 PWA를 풀스크린으로, Chrome UI 없이, 마치 네이티브 앱처럼 띄운다."**
작동 원리.
1. Android 앱(`.apk` 또는 `.aab`)에 내 PWA의 URL을 박는다.
2. Android는 그 URL을 Chrome으로 렌더하지만, Chrome UI를 숨긴다.
3. **Digital Asset Links**로 도메인이 그 앱을 신뢰함을 증명한다 — 이게 "trusted"의 의미.
`assetlinks.json`을 도메인의 `/.well-known/assetlinks.json`에 둔다.
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.example.myapp",
"sha256_cert_fingerprints": [
"AA:BB:CC:..."
]
}
}
]
Bubblewrap CLI(구글 공식)로 TWA 패키지를 한 줄에 만들 수 있다.
npm i -g @bubblewrap/cli
bubblewrap init --manifest https://example.com/manifest.webmanifest
bubblewrap build
→ app-release-signed.aab 생성
TWA의 장점.
- 진짜 Chrome 엔진이라 호환성이 100%.
- Service Worker·Push·File System Access 등 모든 Chrome 기능 사용 가능.
- 업데이트가 즉시 반영됨 — 앱 재배포 없이 웹만 갱신하면 끝.
함정.
- iOS에는 없다. iOS에서는 Capacitor·PWABuilder iOS 경로를 써야 한다.
- 사용자가 Chrome을 비활성화하면 동작하지 않는다 (이 경우 시스템 WebView로 폴백).
10장 · File System Access API — Chrome/Edge 한정
웹 앱이 사용자 로컬 파일을 직접 읽고 쓰는 API. Photoshop on Web, Figma, VS Code Web이 다 이걸 쓴다.
// 파일 열기
const [handle] = await window.showOpenFilePicker({
types: [{ description: 'Text', accept: { 'text/plain': ['.txt', '.md'] } }],
})
const file = await handle.getFile()
const text = await file.text()
// 같은 파일에 쓰기 (사용자 추가 권한 없이)
const writable = await handle.createWritable()
await writable.write(text + '\n수정됨')
await writable.close()
// 디렉터리 통째로
const dirHandle = await window.showDirectoryPicker()
for await (const [name, entry] of dirHandle.entries()) {
console.log(name, entry.kind) // 'file' 또는 'directory'
}
2026년 5월 지원 상태.
- Chrome·Edge·Opera: 정식
- Firefox: 미지원 (논쟁 중 — Mozilla는 보안 모델에 우려를 표시)
- Safari: 미지원
폴백 패턴 — `<input type="file">` + Blob URL로 대체. Figma·VS Code Web도 Firefox에서는 폴백 모드로 동작한다.
// 폴리필 라이브러리 — browser-fs-access (Google)
const blob = await fileOpen({ extensions: ['.txt'] })
// 어디서든 동작. Chrome에서는 File System Access, 그 외에서는 input/Blob 폴백.
11장 · 더 알아야 할 권한 있는 API들 — Push / Background Sync / Periodic Sync / Bluetooth / USB
Push API + Notifications
이미 2장에서 다뤘다. 핵심 — 2026년에는 iOS 포함 모든 메이저 브라우저에서 동작.
Background Sync API
오프라인에서 사용자가 한 동작(폼 제출, 메시지 전송)을 네트워크가 돌아왔을 때 자동으로 재전송.
// 메인 페이지
const reg = await navigator.serviceWorker.ready
await reg.sync.register('send-message')
// sw.js
self.addEventListener('sync', (event) => {
if (event.tag === 'send-message') {
event.waitUntil(flushPendingMessages())
}
})
지원: Chrome·Edge·Opera·Samsung Internet. Firefox·Safari 미지원.
Periodic Background Sync
주기적으로(최소 24시간) 백그라운드에서 데이터를 갱신.
const status = await navigator.permissions.query({ name: 'periodic-background-sync' })
if (status.state === 'granted') {
await reg.periodicSync.register('refresh-feed', { minInterval: 24 * 60 * 60 * 1000 })
}
지원: Chrome·Edge만. 설치된 PWA에서만 동작.
Web Bluetooth API
블루투스 LE 장치와 직접 통신.
const device = await navigator.bluetooth.requestDevice({
filters: [{ services: ['heart_rate'] }],
})
const server = await device.gatt.connect()
const service = await server.getPrimaryService('heart_rate')
const characteristic = await service.getCharacteristic('heart_rate_measurement')
characteristic.addEventListener('characteristicvaluechanged', (event) => {
console.log('HR:', event.target.value.getUint8(1))
})
await characteristic.startNotifications()
지원: Chrome·Edge·Opera. Firefox·Safari 미지원.
WebUSB API
USB 장치 직접 접근. Arduino 플래싱, 결제 단말기, 산업 장비 제어 등.
const device = await navigator.usb.requestDevice({ filters: [{ vendorId: 0x2341 }] })
await device.open()
await device.selectConfiguration(1)
await device.claimInterface(0)
const result = await device.transferIn(1, 64)
지원: Chrome·Edge·Opera. Firefox·Safari 미지원.
Web Share API
OS 공유 시트 띄우기. 2026년에는 모든 메이저 브라우저에서 동작 (iOS Safari 포함).
await navigator.share({
title: 'My PWA',
text: '이거 봐봐',
url: 'https://example.com',
})
12장 · Storage Buckets API (Chrome 122, 2024.2) — 파티션 스토리지
기존 웹 스토리지(IndexedDB, Cache, localStorage)는 모두 **하나의 origin 풀**에 묶여 있었다. 사용자가 "쿠키 및 사이트 데이터 삭제"를 누르면 다 같이 사라졌고, quota도 한 덩어리로 관리됐다.
Storage Buckets는 그걸 **버킷 단위로 쪼갠다.**
// 버킷 생성
const userDataBucket = await navigator.storageBuckets.open('user-data', {
durability: 'strict', // 절대 잃지 마라
persisted: true, // 영구 저장 권한
quota: 100 * 1024 * 1024, // 100MB
})
// 캐시·IDB·SW를 각 버킷에서 따로
const cache = await userDataBucket.caches.open('user-files')
const idb = await userDataBucket.indexedDB.open('users', 1)
const sw = await userDataBucket.getDirectory() // OPFS
// 임시 버킷 — 사용자가 비밀번호 잊을 때 같이 지움
const tempBucket = await navigator.storageBuckets.open('temp-uploads', {
durability: 'relaxed',
persisted: false,
})
// 버킷 삭제 — origin 데이터 전체 말고, 이 버킷만
await navigator.storageBuckets.delete('temp-uploads')
왜 중요한가.
- **선택적 삭제** — 게임의 세이브 파일은 보존하고 캐시만 비울 수 있다.
- **선택적 영구화** — 결제 영수증만 `persisted: true`로 설정.
- **버킷별 quota** — 한 도메인 안에서 멀티 테넌트(예: 이메일·캘린더·노트가 한 도메인에 있다면).
지원: Chrome·Edge 122+(2024.2). Firefox·Safari는 작업 중.
13장 · View Transitions cross-document (Chrome 126, 2024.6) — SPA 같은 MPA
페이지 간 전환을 부드럽게 만드는 API. 처음에는 SPA(같은 문서 안 라우팅) 전용이었지만, **Chrome 126(2024.6)부터 cross-document(서로 다른 페이지 사이)도 지원**한다.
활성화는 한 줄이다.
@view-transition {
navigation: auto;
}
이게 다다. 같은 origin 안의 페이지 전환에서 브라우저가 자동으로 페이드를 넣어준다.
고급 — 두 페이지에 같은 `view-transition-name`을 주면 그 요소가 페이지를 가로질러 모핑한다.
/* product-list.html */
.product-card img {
view-transition-name: product-image;
}
/* product-detail.html */
.product-detail img {
view-transition-name: product-image;
}
리스트의 썸네일을 클릭하면 그 이미지가 디테일 페이지의 큰 이미지로 매끄럽게 확대된다. Instagram·메르카리 앱에서 보던 그 동작이다.
이게 왜 PWA의 부활인가? — "MPA(서버 렌더)도 SPA만큼 부드럽다"는 게 증명되면, **SPA가 가져갔던 UX 우위 절반이 무너진다.** PWA는 본질적으로 MPA에도 잘 어울리는 모델이다.
지원: Chrome 126+, Edge, Opera. Safari·Firefox 작업 중.
14장 · Speculation Rules — prerender 미래 페이지
`<link rel="prefetch">`의 강화판. 다음에 사용자가 갈 페이지를 **HTML·JS·CSS 다 받아서 백그라운드 탭으로 미리 렌더**한다.
{
"prerender": [
{ "where": { "href_matches": "/product/*" }, "eagerness": "moderate" }
],
"prefetch": [
{ "where": { "href_matches": "/category/*" } }
]
}
`eagerness` 옵션.
- `immediate` — 발견 즉시 prerender.
- `eager` — 사용자가 링크를 hover하면.
- `moderate` — 사용자가 링크를 hover 200ms 이상.
- `conservative` — 마우스다운 직후.
`prerender`된 페이지로 이동하면 **즉시 표시된다 (LCP 0ms)**. 카카오·라인·메르카리의 카탈로그 페이지에서 다음 카드 클릭이 "0ms"로 느껴지는 비결.
함정.
- prerender된 페이지는 백그라운드에서 SW·JS가 실제로 돌아간다. **분석 코드가 false hit를 보낼 수 있다.** `document.prerendering`을 체크해서 prerender 중에는 보고를 미루는 게 표준이다.
- 모바일은 prerender 동시 1개만 허용된다. 욕심내지 마라.
지원: Chrome·Edge. Safari·Firefox 미지원이지만 polyfill 없이 그냥 무시되므로 안전하다.
15장 · 한국 / 일본 PWA 사례 — 카카오, 네이버, 메르카리, 픽시브
한국
- **카카오 모바일 웹** — 카카오톡 채널 페이지, 카카오 페이의 일부 흐름이 PWA 패턴(설치 가능 manifest, SW 캐시, Push)을 적극 활용한다. 특히 m.kakao.com 도메인 일부는 standalone 모드로 추가 가능.
- **네이버 웹툰** — 모바일 웹 버전(comic.naver.com)이 SW로 이미지 캐시. 만화 한 편 처음 보면 다음 페이지 미리 받아둠.
- **쿠팡** — 모바일 웹이 Service Worker로 이미지·JS 캐시. App Banner는 자체 앱 유도에 집중.
- **당근마켓** — 웹은 한정적이지만 모바일 웹의 매물 페이지가 SW 캐시 활용.
한국 PWA가 약한 이유는 분명하다. **카카오톡·네이버앱이라는 슈퍼앱 안에서 웹뷰로 동작하기 때문에**, 사용자 입장에서 "별도 앱"의 경계가 흐려진다. 카카오 인앱 브라우저에 PWA를 설치할 수는 없다.
일본
- **메르카리(Mercari)** — 모바일 웹이 PWA로 운영된다. jp.mercari.com에서 manifest, SW, Push 모두 동작. 일본은 App Store 신뢰도가 높아 네이티브 앱이 압도적이지만, 메르카리는 **웹 전용 사용자(가벼운 구매자) 비율이 의외로 높다.**
- **Pixiv Sketch** — 라이브 드로잉 플랫폼. 모바일 브라우저에서 PWA로 동작. 그림 그릴 때 오프라인에서도 끊김 없이 보존.
- **Yahoo! JAPAN** — 일부 페이지(특히 뉴스)가 적극적인 SW 캐싱. 페이지 전환이 매우 빠른 비결.
- **Pixiv 일러스트 본관** — pixiv.net 자체는 풀 PWA는 아니지만 Web Push 사용 (좋아요·코멘트 알림).
일본에서 PWA가 의외로 통하는 이유는 **iOS 점유율이 60%를 넘는 시장에서 iOS Web Push가 풀린 게 큰 영향**이었다. 라인은 자체 메신저 인프라가 있어 굳이 Web Push가 필요 없지만, 작은 서비스들은 푸시 채널을 가질 수 있게 된 것만으로도 의미가 컸다.
16장 · 누가 PWA를 골라야 하나 — 결정 매트릭스
| 카테고리 | PWA 적합도 | 이유 |
| -------------------- | ---------- | -------------------------------------------------------- |
| 미디어·블로그 | 매우 적합 | SW 캐시·오프라인 읽기·Web Push 알림 |
| 이커머스 | 적합 | 카탈로그 prerender, View Transitions, 가벼운 설치 |
| SaaS 대시보드 | 매우 적합 | 데스크톱 PWA 설치, 단축키, 전체화면 |
| 채팅·메신저 | 보통 | Push는 좋지만 네이티브 알림·VoIP는 한계 |
| 게임 (캐주얼) | 적합 | Canvas·WebGL·WebGPU·Storage Buckets |
| 게임 (3A·실시간) | 부적합 | 그래픽 한계·메모리 한계·App Store 마케팅 |
| 도구 (오디오·비디오) | 매우 적합 | File System Access, Web MIDI, Audio API |
| 결제·핀테크 | 보통 | 보안 모델·OS 인증·생체 — 일부는 가능, 핵심은 네이티브 |
| 컨퍼런스·이벤트 앱 | 매우 적합 | 단기 사용·설치 마찰 회피 |
| 사진·그림 도구 | 적합 | File System Access, Canvas, OPFS |
체크리스트 — PWA를 고르기 전 5개 질문.
1. **사용자가 이걸 매일 쓸 것인가, 가끔 쓸 것인가?** 매일 쓴다면 네이티브가 보통 낫다. 가끔이면 PWA.
2. **Push 알림이 필수인가?** 이제는 iOS도 됨. 단, "정확한 배달 시간 보장"이 필요하면 네이티브.
3. **카메라·블루투스·USB 같은 디바이스 API가 필요한가?** Chrome/Android면 PWA로 충분. iOS면 Capacitor.
4. **앱 스토어 노출이 마케팅의 핵심인가?** 그렇다면 PWABuilder + TWA, 또는 Capacitor.
5. **오프라인이 진짜 필요한가, 아니면 "있으면 좋은" 정도인가?** "진짜 필요"는 PWA가 거의 무조건 답.
체감 한 줄 — **모바일 우선 + 미디어/도구 + 가끔 쓰는 = PWA. 매일 + 디바이스 깊이 사용 = 네이티브 또는 Capacitor.**
에필로그 — 두 번째 부활
2026년 5월의 PWA는 2019년의 PWA와 같은 단어지만, 같은 기술 스택이 아니다.
- 명세는 거의 그대로다 — Service Worker, Manifest, Cache API.
- 도구는 완전히 다르다 — 손으로 짜는 SW에서 Workbox 7로, frameworks 통합으로, 빌드 매니페스트로.
- 권한은 폭발했다 — iOS Web Push, File System Access, Bluetooth, USB, Storage Buckets.
- UX는 SPA를 흉내 내지 않는다 — View Transitions cross-document, Speculation Rules로 MPA가 SPA를 흉내 낼 수 있게 되었다.
- 배포 경로도 다양해졌다 — PWABuilder, TWA, Capacitor.
가장 큰 변화는 **"PWA냐 네이티브냐"라는 이분법이 끝났다는 것**이다. 2026년의 답은 **"내 사용자가 원하는 가장 가벼운 경로"** 다. 어떤 사용자에게는 그게 PWA고, 어떤 사용자에게는 그게 Capacitor 셸이고, 어떤 사용자에게는 그게 진짜 네이티브다. **그리고 셋 다 같은 웹 코드베이스에서 갈라져 나갈 수 있다.**
iOS의 정치 드라마가 한 번 보여줬듯, 플랫폼은 변덕스럽다. 하지만 웹 표준은 멈추지 않는다. 2027년의 다음 라운드는 — Storage Foundation, Bluetooth on iOS, RTC on Web Push — 또 다른 부활이 될 것이다.
> "PWA는 죽지 않았다. 그저 우리가 너무 일찍 추도식을 열었을 뿐이다."
— PWA & Service Workers 2026, 끝.
참고 / References
- [W3C — Service Workers 1](https://www.w3.org/TR/service-workers/)
- [W3C — Web App Manifest](https://www.w3.org/TR/appmanifest/)
- [W3C — Push API](https://www.w3.org/TR/push-api/)
- [MDN — Service Worker API](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API)
- [MDN — Progressive Web Apps](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps)
- [Workbox 공식](https://developer.chrome.com/docs/workbox)
- [vite-pwa 공식](https://vite-pwa-org.netlify.app/)
- [@ducanh2912/next-pwa GitHub](https://github.com/DuCanhGH/next-pwa)
- [Serwist (next-pwa 후속)](https://serwist.pages.dev/)
- [@vite-pwa/astro](https://vite-pwa-org.netlify.app/frameworks/astro)
- [Capacitor 공식](https://capacitorjs.com/)
- [PWABuilder 공식](https://www.pwabuilder.com/)
- [Bubblewrap CLI (TWA)](https://github.com/GoogleChromeLabs/bubblewrap)
- [Android — Trusted Web Activities](https://developer.chrome.com/docs/android/trusted-web-activity)
- [WebKit Blog — Web Push for Web Apps on iOS](https://webkit.org/blog/13878/web-push-for-web-apps-on-ios-and-ipados/)
- [Open Web Advocacy — PWA in iOS 17.4 timeline](https://open-web-advocacy.org/)
- [File System Access API](https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API)
- [browser-fs-access (폴리필)](https://github.com/GoogleChromeLabs/browser-fs-access)
- [Storage Buckets API](https://developer.chrome.com/docs/web-platform/storage-buckets)
- [View Transitions API — cross-document](https://developer.chrome.com/docs/web-platform/view-transitions/cross-document)
- [Speculation Rules API](https://developer.mozilla.org/en-US/docs/Web/API/Speculation_Rules_API)
- [Web Bluetooth API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API)
- [WebUSB API](https://developer.mozilla.org/en-US/docs/Web/API/WebUSB_API)
- [Web Share API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Share_API)
- [Background Sync API](https://developer.mozilla.org/en-US/docs/Web/API/Background_Synchronization_API)
- [Periodic Background Sync](https://developer.chrome.com/docs/capabilities/periodic-background-sync)
- [Chrome Status — PWA features](https://chromestatus.com/features#pwa)
- [Web.dev — Learn PWA](https://web.dev/learn/pwa/)
- [메르카리 엔지니어링 블로그](https://engineering.mercari.com/)
- [LINE 엔지니어링 블로그](https://engineering.linecorp.com/ko)
현재 단락 (1/568)
2019년 즈음, 한국과 일본의 컨퍼런스 무대에서 가장 많이 들렸던 말은 "PWA는 죽었다"였다. 이유는 단순했다 — iOS가 Web Push를 막았고, 홈 화면 추가 UX가 끔찍...