PWA(Progressive Web App)란?
PWA는 웹 기술로 네이티브 앱 수준의 사용자 경험을 제공하는 웹 애플리케이션입니다. 오프라인 지원, 푸시 알림, 홈 화면 설치 등 모바일 앱의 핵심 기능을 웹에서 구현할 수 있습니다. Google, Twitter, Starbucks 등 대형 서비스에서 이미 PWA를 도입하여 전환율을 크게 향상시켰습니다.
Service Worker 등록과 라이프사이클
Service Worker는 브라우저와 네트워크 사이에 위치하는 프록시 역할을 하며, 오프라인 캐싱과 푸시 알림의 핵심입니다. 등록 → 설치 → 활성화의 라이프사이클을 거칩니다.
// sw.js - Service Worker 파일
const CACHE_NAME = 'app-cache-v1';
const urlsToCache = [
'/',
'/styles/main.css',
'/scripts/app.js',
'/offline.html'
];
// Install 이벤트: 정적 자원 캐싱
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => {
console.log('캐시 열림');
return cache.addAll(urlsToCache);
})
);
// 대기 중인 SW를 즉시 활성화
self.skipWaiting();
});
// Activate 이벤트: 이전 캐시 정리
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames
.filter((name) => name !== CACHE_NAME)
.map((name) => caches.delete(name))
);
})
);
self.clients.claim();
});
// Fetch 이벤트: 캐시 우선, 네트워크 폴백
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then((response) => {
if (response) return response;
return fetch(event.request).then((networkResponse) => {
if (networkResponse.ok) {
const cloned = networkResponse.clone();
caches.open(CACHE_NAME)
.then((cache) => cache.put(event.request, cloned));
}
return networkResponse;
});
})
.catch(() => caches.match('/offline.html'))
);
});
앱에서 Service Worker 등록하기
// main.js
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
try {
const reg = await navigator.serviceWorker.register('/sw.js');
console.log('SW 등록 성공:', reg.scope);
} catch (err) {
console.error('SW 등록 실패:', err);
}
});
}
캐싱 전략 비교
Service Worker에서 사용할 수 있는 주요 캐싱 전략은 다음과 같습니다. 각 전략은 콘텐츠 특성에 따라 선택해야 합니다.
| 전략 | 설명 | 적합한 경우 |
|---|---|---|
| Cache First | 캐시 우선, 없으면 네트워크 | 정적 자원 (CSS, JS, 이미지) |
| Network First | 네트워크 우선, 실패 시 캐시 | API 응답, 동적 콘텐츠 |
| Stale While Revalidate | 캐시 반환 후 백그라운드 갱신 | 자주 바뀌지만 최신 아니어도 되는 콘텐츠 |
| Cache Only | 캐시에서만 응답 | 오프라인 전용 페이지 |
| Network Only | 항상 네트워크 | 실시간 데이터 (결제 등) |
Web Push 알림 구현
푸시 알림은 VAPID(Voluntary Application Server Identification) 키 쌍을 사용하여 서버와 브라우저 간 인증을 처리합니다.
VAPID 키 생성
npx web-push generate-vapid-keys
클라이언트 측 구독
async function subscribePush() {
const reg = await navigator.serviceWorker.ready;
const subscription = await reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY)
});
// 서버에 구독 정보 전송
await fetch('/api/push/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(subscription)
});
}
Service Worker에서 푸시 수신 처리
self.addEventListener('push', (event) => {
const data = event.data.json();
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)
);
});
manifest.json 설정
PWA의 메타 정보를 정의하는 Web App Manifest 파일은 홈 화면 설치 시 앱 이름, 아이콘, 테마 색상 등을 지정합니다.
{
"name": "My PWA App",
"short_name": "MyApp",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#2196F3",
"icons": [
{ "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" }
]
}
실전 팁
- Workbox 라이브러리를 사용하면 캐싱 전략을 선언적으로 관리할 수 있어 생산성이 크게 향상됩니다.
- Lighthouse의 PWA 점수를 활용하여 PWA 호환성을 체크하세요.
- iOS Safari는 Web Push를 iOS 16.4부터 지원하므로, 이전 버전 사용자에 대한 폴백 전략이 필요합니다.
- 캐시 용량은 브라우저마다 다르므로, 캐시 용량 초과 시 LRU 정책으로 오래된 항목을 정리하세요.
댓글 0