Skip to content

필사 모드: PWA & Service Workers 2026 — Workbox 7 / vite-pwa / Capacitor / TWA / iOS Web Push / Storage Buckets 심층 가이드

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

프롤로그 — "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가 끔찍...

작성 글자: 0원문 글자: 21,468작성 단락: 0/568