Files
2nd/10_Wiki/Topics/Coding/Web_PWA_Service_Worker.md
T
2026-05-09 21:08:02 +09:00

6.6 KiB

id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
id title category status source_trust_level verification_status created_at updated_at tags tech_stack applied_in aliases
web-pwa-service-worker PWA / Service Worker — Offline / Push / Install Coding draft B conceptual 2026-05-09 2026-05-09
web
pwa
service-worker
offline
vibe-coding
language applicable_to
TS / Workbox
Frontend
PWA
Service Worker
Workbox
manifest
install prompt
push notification web

PWA / Service Worker

Web app 가 native 처럼. Manifest + Service Worker = installable + offline + push. Workbox 가 표준 라이브러리. iOS 16.4+ web push 지원.

📖 핵심 개념

  • Manifest: 앱 메타 (이름, icon, theme).
  • Service Worker: 네트워크 인터셉트, 캐싱, push.
  • Cache API: 자원 저장.
  • Background sync: 오프라인 → 온라인 시 sync.

💻 코드 패턴

Manifest

// public/manifest.webmanifest
{
  "name": "Acme",
  "short_name": "Acme",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#3b82f6",
  "icons": [
    { "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" },
    { "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" },
    { "src": "/icon-maskable.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
  ]
}
<link rel="manifest" href="/manifest.webmanifest">
<meta name="theme-color" content="#3b82f6">
<link rel="apple-touch-icon" href="/icon-192.png">

Service Worker (단순 cache)

// public/sw.js
const CACHE = 'app-v1';
const ASSETS = ['/', '/app.css', '/app.js'];

self.addEventListener('install', (e) => {
  e.waitUntil(caches.open(CACHE).then(c => c.addAll(ASSETS)));
});

self.addEventListener('activate', (e) => {
  e.waitUntil(
    caches.keys().then(keys =>
      Promise.all(keys.filter(k => k !== CACHE).map(k => caches.delete(k)))
    )
  );
});

self.addEventListener('fetch', (e) => {
  if (e.request.method !== 'GET') return;
  e.respondWith(
    caches.match(e.request).then(r => r ?? fetch(e.request))
  );
});

Register

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js').then(reg => {
    reg.addEventListener('updatefound', () => {
      const w = reg.installing;
      w?.addEventListener('statechange', () => {
        if (w.state === 'installed' && navigator.serviceWorker.controller) {
          showUpdatePrompt(); // 새 버전 있음
        }
      });
    });
  });
}

Workbox (high-level)

// workbox.config.ts (Vite plugin / Next config)
import { generateSW } from 'workbox-build';

generateSW({
  swDest: 'dist/sw.js',
  globDirectory: 'dist',
  globPatterns: ['**/*.{js,css,html,png,jpg,svg,woff2}'],
  runtimeCaching: [
    {
      urlPattern: /^https:\/\/api\.example\.com/,
      handler: 'NetworkFirst',
      options: {
        cacheName: 'api',
        networkTimeoutSeconds: 5,
        expiration: { maxEntries: 50, maxAgeSeconds: 5 * 60 },
      },
    },
    {
      urlPattern: /\.(?:png|jpg|webp)$/,
      handler: 'CacheFirst',
      options: { cacheName: 'images', expiration: { maxAgeSeconds: 30 * 24 * 3600 } },
    },
  ],
});

Vite-PWA plugin

// vite.config.ts
import { VitePWA } from 'vite-plugin-pwa';
plugins: [
  VitePWA({
    registerType: 'autoUpdate',
    workbox: { globPatterns: ['**/*.{js,css,html,png,svg}'] },
    manifest: { /* 위 */ },
  }),
];

Install prompt

let deferred: BeforeInstallPromptEvent | null = null;

window.addEventListener('beforeinstallprompt', (e) => {
  e.preventDefault();
  deferred = e as BeforeInstallPromptEvent;
  showInstallButton();
});

installBtn.onclick = async () => {
  if (!deferred) return;
  await deferred.prompt();
  const choice = await deferred.userChoice;
  console.log(choice.outcome); // 'accepted' / 'dismissed'
  deferred = null;
};

Push notification

// 권한
const perm = await Notification.requestPermission();
if (perm !== 'granted') return;

// Subscribe
const reg = await navigator.serviceWorker.ready;
const sub = await reg.pushManager.subscribe({
  userVisibleOnly: true,
  applicationServerKey: VAPID_PUBLIC_KEY,
});

// 서버에 보내 — DB 에 저장
await fetch('/api/push/subscribe', { method: 'POST', body: JSON.stringify(sub) });
// SW
self.addEventListener('push', (event) => {
  const data = event.data?.json() ?? {};
  event.waitUntil(
    self.registration.showNotification(data.title, {
      body: data.body,
      icon: '/icon-192.png',
      badge: '/badge.png',
      data: { url: data.url },
    })
  );
});

self.addEventListener('notificationclick', (event) => {
  event.notification.close();
  event.waitUntil(self.clients.openWindow(event.notification.data.url));
});

Server push (web-push)

import webpush from 'web-push';

webpush.setVapidDetails('mailto:admin@acme.com', VAPID_PUBLIC, VAPID_PRIVATE);

await webpush.sendNotification(subscription, JSON.stringify({
  title: 'New message',
  body: 'You have a new message',
  url: '/inbox',
}));

Background sync

// SW
self.addEventListener('sync', (event) => {
  if (event.tag === 'sync-orders') {
    event.waitUntil(syncOrders());
  }
});

// App
const reg = await navigator.serviceWorker.ready;
await reg.sync.register('sync-orders');
// 오프라인 → 다음 online 에 자동

Update flow (smooth)

// 사용자에 새 버전 알림 → "Refresh" 버튼
// 한번에 reload — 데이터 잃지 않게 자동 X
// Workbox skipWaiting
import { Workbox } from 'workbox-window';
const wb = new Workbox('/sw.js');
wb.addEventListener('waiting', () => {
  if (confirm('New version available. Refresh?')) {
    wb.messageSkipWaiting();
    window.location.reload();
  }
});

🤔 의사결정 기준

사용 추천
Static + cache CacheFirst
API + freshness NetworkFirst (timeout fallback)
Image / font CacheFirst with TTL
Real-time Network only
Offline-first Workbox + IndexedDB
Push VAPID + web-push

안티패턴

  • 모든 자원 cache + version 안 bump: 새 버전 안 받음.
  • API 까지 long cache: stale 데이터.
  • SW 무한 cache 자라남: expiration / quota.
  • Update 강제 (skipWaiting): 진행 중 작업 잃음.
  • Push 권한 즉시 page load 시: 사용자 거부 + 차단.
  • VAPID key client 노출 (private): server only.
  • iOS push 가정 — 안 install: PWA 설치 후만 push 작동.

🤖 LLM 활용 힌트

  • Workbox 가 SW 직접 보다 단순.
  • VitePWA / next-pwa plugin.
  • Push = VAPID + web-push + iOS 설치 필요.

🔗 관련 문서