- Published on
PWA & Service Workers 2026 — Workbox 7 / vite-pwa / Capacitor / TWA / iOS Web Push / Storage Buckets 심층 가이드
- Authors

- Name
- Youngju Kim
- @fjvbn20031
프롤로그 — "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가 그걸 끝냈다.
조건은 명확하다.
- 홈 화면에 추가된 PWA여야 한다 (브라우저 탭에서는 안 된다).
- 사용자가 명시적으로
Notification.requestPermission()을 호출한 결과granted여야 한다. - 사이트는 HTTPS여야 한다.
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 진영에 두 가지를 가르쳤다.
- 정치적 위기에 약하다. 플랫폼 한 곳의 결정으로 글로벌 사용자 절반이 사라질 수 있다.
- 그래도 살아남는다. 충분히 많은 개발자가 화내면 결정은 뒤집힌다.
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년의 모범 사례.
skipWaiting()은 신중하게. 즉시 활성화하면 이미 열린 탭의 클라이언트가 새 SW와 통신하면서 충돌할 수 있다. 사용자에게 "업데이트가 있어요, 새로고침" 토스트를 보여주는 패턴이 표준이다.fetch핸들러는 짧게. 핸들러 내부에서 동기 코드가 길면 모든 네트워크 요청이 느려진다.- 타임아웃을 직접 걸어라.
fetch()에는 기본 타임아웃이 없다.AbortController를 써라. - 버전 관리는 명시적으로.
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
import { precacheAndRoute } from 'workbox-precaching'
import { registerRoute } from 'workbox-routing'
import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies'
import { ExpirationPlugin } from 'workbox-expiration'
// 빌드 시 주입되는 매니페스트 (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
import { defineConfig } from 'vite'
import { VitePWA } from 'vite-plugin-pwa'
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 쪽 등록은.
import { registerSW } from 'virtual:pwa-register'
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
import AstroPWA from '@vite-pwa/astro'
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와 매우 비슷하다.
import { Camera, CameraResultType } from '@capacitor/camera'
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 없이, 마치 네이티브 앱처럼 띄운다."
작동 원리.
- Android 앱(
.apk또는.aab)에 내 PWA의 URL을 박는다. - Android는 그 URL을 Chrome으로 렌더하지만, Chrome UI를 숨긴다.
- 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)
import { fileOpen, fileSave } from 'browser-fs-access'
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 다 받아서 백그라운드 탭으로 미리 렌더한다.
<script type="speculationrules">
{
"prerender": [
{ "where": { "href_matches": "/product/*" }, "eagerness": "moderate" }
],
"prefetch": [
{ "where": { "href_matches": "/category/*" } }
]
}
</script>
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개 질문.
- 사용자가 이걸 매일 쓸 것인가, 가끔 쓸 것인가? 매일 쓴다면 네이티브가 보통 낫다. 가끔이면 PWA.
- Push 알림이 필수인가? 이제는 iOS도 됨. 단, "정확한 배달 시간 보장"이 필요하면 네이티브.
- 카메라·블루투스·USB 같은 디바이스 API가 필요한가? Chrome/Android면 PWA로 충분. iOS면 Capacitor.
- 앱 스토어 노출이 마케팅의 핵심인가? 그렇다면 PWABuilder + TWA, 또는 Capacitor.
- 오프라인이 진짜 필요한가, 아니면 "있으면 좋은" 정도인가? "진짜 필요"는 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
- W3C — Web App Manifest
- W3C — Push API
- MDN — Service Worker API
- MDN — Progressive Web Apps
- Workbox 공식
- vite-pwa 공식
- @ducanh2912/next-pwa GitHub
- Serwist (next-pwa 후속)
- @vite-pwa/astro
- Capacitor 공식
- PWABuilder 공식
- Bubblewrap CLI (TWA)
- Android — Trusted Web Activities
- WebKit Blog — Web Push for Web Apps on iOS
- Open Web Advocacy — PWA in iOS 17.4 timeline
- File System Access API
- browser-fs-access (폴리필)
- Storage Buckets API
- View Transitions API — cross-document
- Speculation Rules API
- Web Bluetooth API
- WebUSB API
- Web Share API
- Background Sync API
- Periodic Background Sync
- Chrome Status — PWA features
- Web.dev — Learn PWA
- 메르카리 엔지니어링 블로그
- LINE 엔지니어링 블로그