Progressive Web App (PWA) 이해하기
개요
Progressive Web App(PWA)은 웹 기술로 만들어진 애플리케이션이 네이티브 앱과 유사한 경험을 제공할 수 있도록 하는 웹 앱 개발 방식입니다. PWA를 사용하면 웹사이트를 사용자의 홈 화면에 설치할 수 있고, 오프라인에서도 작동하며, 푸시 알림을 보낼 수 있습니다.
PWA는 기존 웹사이트에 점진적으로 적용할 수 있으며, 별도의 앱스토어 배포 없이 사용자에게 네이티브 앱과 유사한 경험을 제공합니다.
배경
왜 필요한가?
웹 애플리케이션은 다음과 같은 한계가 있었습니다:
- 플랫폼 의존성: iOS와 Android에서 각각 다른 네이티브 앱을 개발해야 했습니다
- 네트워크 의존성: 인터넷 연결이 없으면 앱을 사용할 수 없었습니다
- 설치의 어려움: 앱스토어를 거쳐야만 사용자 기기에 설치할 수 있었습니다
- 사용자 재참여: 푸시 알림 같은 네이티브 기능을 사용할 수 없었습니다
PWA는 이러한 문제를 해결하여 웹 앱이 네이티브 앱의 장점을 누릴 수 있게 합니다.
등장 이전의 방식
PWA가 등장하기 전에는:
- 하이브리드 앱: Cordova, Ionic 같은 프레임워크로 웹뷰를 네이티브 앱으로 감쌌습니다
- 반응형 웹: 모바일 브라우저에 최적화된 웹사이트를 만들었지만, 오프라인 기능이나 설치는 불가능했습니다
- 네이티브 앱: iOS와 Android 각각 개발해야 했고, 개발 비용과 시간이 많이 들었습니다
핵심 구성 요소
PWA는 세 가지 핵심 기술로 구성됩니다:
1. Service Worker
Service Worker는 브라우저가 백그라운드에서 실행하는 JavaScript 파일입니다. 네트워크 요청을 가로채고 캐싱을 관리하여 오프라인 기능을 제공합니다.
주요 역할:
- 네트워크 요청 캐싱
- 오프라인 콘텐츠 제공
- 백그라운드 동기화
- 푸시 알림 수신
2. Web App Manifest
Web App Manifest는 JSON 파일로, 앱의 메타데이터를 정의합니다. 이를 통해 브라우저가 앱을 설치하는 방법을 알 수 있습니다.
주요 정보:
- 앱 이름과 설명
- 아이콘
- 시작 URL
- 테마 색상
- 표시 모드 (전체 화면, 독립 실행형 등)
3. HTTPS
PWA는 보안을 위해 HTTPS에서만 작동합니다. Service Worker가 네트워크 요청을 가로채기 때문에 중간자 공격을 방지하기 위해 필수입니다.
예외:
localhost에서는 개발 목적으로 HTTP 허용
동작 원리
Service Worker 생명주기
1. 등록 (Register)
↓
2. 설치 (Install)
↓
3. 활성화 (Activate)
↓
4. 대기 (Idle/Fetch/Message)
↓
5. 종료 (Terminated)캐싱 전략
PWA는 다양한 캐싱 전략을 사용합니다:
1. Cache First (캐시 우선)
요청 → 캐시 확인 → 있으면 반환
↓ 없으면
네트워크 요청 → 캐시 저장 → 반환2. Network First (네트워크 우선)
요청 → 네트워크 요청 → 성공하면 반환
↓ 실패하면
캐시 확인 → 반환3. Stale While Revalidate (캐시 반환 후 갱신)
요청 → 캐시 즉시 반환
↓
동시에 네트워크 요청 → 캐시 업데이트코드로 이해하기
// Service Worker 등록 (main.js)
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker
.register('/sw.js')
.then(registration => {
console.log('Service Worker 등록 성공:', registration.scope);
})
.catch(error => {
console.error('Service Worker 등록 실패:', error);
});
});
}
// Service Worker 파일 (sw.js)
const CACHE_NAME = 'my-pwa-cache-v1';
const urlsToCache = [
'/',
'/styles/main.css',
'/scripts/main.js',
'/images/logo.png'
];
// 설치 단계: 필요한 파일을 캐시에 저장
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
console.log('캐시 열기');
return cache.addAll(urlsToCache);
})
);
});
// Fetch 단계: 네트워크 요청 가로채기
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
// 캐시에 있으면 캐시 반환, 없으면 네트워크 요청
return response || fetch(event.request);
})
);
});주요 기능
기능 1: 오프라인 지원
PWA는 Service Worker를 통해 오프라인에서도 작동합니다.
사용자 경험:
- 인터넷 연결이 끊겨도 이전에 방문한 페이지를 볼 수 있습니다
- 오프라인 전용 페이지를 표시할 수 있습니다
- 백그라운드에서 데이터를 동기화합니다
구현 예시:
// 오프라인 폴백 페이지
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
return response || fetch(event.request);
})
.catch(() => {
// 네트워크 요청 실패 시 오프라인 페이지 반환
return caches.match('/offline.html');
})
);
});기능 2: 설치 가능 (Install to Home Screen)
사용자가 PWA를 홈 화면에 추가하여 네이티브 앱처럼 실행할 수 있습니다.
manifest.json 예시:
{
"name": "My Progressive Web App",
"short_name": "MyPWA",
"description": "A sample PWA demonstrating core features",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#000000",
"icons": [
{
"src": "/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}설치 프롬프트 제어:
let deferredPrompt;
window.addEventListener('beforeinstallprompt', event => {
// 브라우저 기본 설치 배너 방지
event.preventDefault();
deferredPrompt = event;
// 커스텀 설치 버튼 표시
showInstallButton();
});
// 사용자가 설치 버튼 클릭 시
installButton.addEventListener('click', async () => {
if (deferredPrompt) {
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
console.log(`사용자 선택: ${outcome}`);
deferredPrompt = null;
}
});기능 3: 푸시 알림
Service Worker를 통해 백그라운드에서 푸시 알림을 받을 수 있습니다.
알림 권한 요청:
// 푸시 알림 구독
async function subscribeToPush() {
const registration = await navigator.serviceWorker.ready;
try {
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(vapidPublicKey)
});
// 서버에 구독 정보 전송
await sendSubscriptionToServer(subscription);
} catch (error) {
console.error('푸시 구독 실패:', error);
}
}
// Service Worker에서 푸시 이벤트 처리
self.addEventListener('push', event => {
const data = event.data.json();
const options = {
body: data.body,
icon: '/icons/icon-192x192.png',
badge: '/icons/badge-72x72.png',
vibrate: [200, 100, 200]
};
event.waitUntil(
self.registration.showNotification(data.title, options)
);
});기능 4: 백그라운드 동기화
네트워크가 다시 연결될 때 데이터를 자동으로 동기화합니다.
사용 사례:
- 오프라인에서 작성한 메시지를 온라인 시 전송
- 폼 제출 데이터 저장 후 나중에 전송
- 파일 업로드 재시도
구현 예시:
// 백그라운드 동기화 등록
async function syncData() {
const registration = await navigator.serviceWorker.ready;
try {
await registration.sync.register('sync-messages');
console.log('동기화 등록 완료');
} catch (error) {
console.error('동기화 등록 실패:', error);
}
}
// Service Worker에서 동기화 이벤트 처리
self.addEventListener('sync', event => {
if (event.tag === 'sync-messages') {
event.waitUntil(sendPendingMessages());
}
});
async function sendPendingMessages() {
const messages = await getMessagesFromIndexedDB();
for (const message of messages) {
try {
await fetch('/api/messages', {
method: 'POST',
body: JSON.stringify(message)
});
await removeMessageFromIndexedDB(message.id);
} catch (error) {
console.error('메시지 전송 실패:', error);
}
}
}실제 사용 사례
사례 1: 뉴스/미디어 앱
요구사항:
- 오프라인에서 이전 기사 읽기
- 새 기사 알림
- 빠른 로딩 속도
PWA 활용:
// 기사 캐싱 전략
self.addEventListener('fetch', event => {
if (event.request.url.includes('/api/articles')) {
event.respondWith(
caches.open('articles-cache').then(cache => {
return fetch(event.request)
.then(response => {
// 네트워크 응답을 캐시에 저장
cache.put(event.request, response.clone());
return response;
})
.catch(() => {
// 네트워크 실패 시 캐시에서 반환
return cache.match(event.request);
});
})
);
}
});사례 2: 전자상거래
요구사항:
- 상품 브라우징
- 장바구니 오프라인 저장
- 재입고 알림
PWA 활용:
// IndexedDB를 사용한 장바구니 저장
async function addToCart(product) {
const db = await openDB('shopping-cart', 1, {
upgrade(db) {
db.createObjectStore('cart', { keyPath: 'id' });
}
});
await db.put('cart', product);
// 온라인 시 서버와 동기화
if (navigator.onLine) {
await syncCartWithServer();
} else {
// 오프라인 시 백그라운드 동기화 등록
const registration = await navigator.serviceWorker.ready;
await registration.sync.register('sync-cart');
}
}사례 3: 소셜 미디어
요구사항:
- 오프라인 포스트 작성
- 실시간 알림
- 빠른 콘텐츠 로딩
PWA 활용:
// 포스트 작성 후 동기화
async function createPost(postData) {
try {
// 온라인이면 즉시 전송
if (navigator.onLine) {
const response = await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(postData)
});
return await response.json();
} else {
// 오프라인이면 IndexedDB에 저장
await savePostToIndexedDB(postData);
// 백그라운드 동기화 등록
const registration = await navigator.serviceWorker.ready;
await registration.sync.register('sync-posts');
return { status: 'pending' };
}
} catch (error) {
console.error('포스트 생성 실패:', error);
throw error;
}
}장점과 한계
장점
- ✅ 크로스 플랫폼: 하나의 코드베이스로 모든 플랫폼에서 작동
- ✅ 빠른 로딩: 캐싱을 통해 네트워크 요청을 줄이고 로딩 속도를 개선
- ✅ 오프라인 지원: 네트워크 연결 없이도 앱 사용 가능
- ✅ 낮은 진입 장벽: 앱스토어 승인 없이 즉시 배포 가능
- ✅ 비용 절감: 네이티브 앱 개발 대비 개발 비용과 시간 절약
- ✅ 자동 업데이트: Service Worker 업데이트로 즉시 반영
- ✅ SEO 친화적: 웹 검색 엔진에 노출 가능
한계
- ⚠️ iOS 지원 제한: iOS Safari에서 일부 기능 제한 (예: 푸시 알림은 iOS 16.4+)
- ⚠️ 배터리 소모: 백그라운드 동기화와 푸시 알림이 배터리를 소모
- ⚠️ 스토리지 제한: 브라우저별로 캐시 용량 제한이 다름
- ⚠️ 하드웨어 접근 제한: 네이티브 앱만큼 기기 하드웨어 접근이 자유롭지 않음
- ⚠️ 앱스토어 부재: 앱스토어를 통한 자연 유입이 없음
트레이드오프
PWA를 선택해야 하는 경우:
- 빠른 출시가 필요한 경우
- 웹 기반 서비스가 이미 있는 경우
- 크로스 플랫폼 지원이 중요한 경우
- 지속적인 업데이트가 필요한 경우
네이티브 앱을 선택해야 하는 경우:
- 복잡한 하드웨어 기능이 필요한 경우 (카메라, GPS, 센서 등)
- 앱스토어 가시성이 중요한 경우
- iOS에서 완전한 푸시 알림 지원이 필수인 경우
- 오프라인에서 복잡한 데이터 처리가 필요한 경우
Next.js에서 PWA 구현하기
Next.js 14 App Router 환경에서 PWA를 구현하는 방법을 알아봅니다.
1단계: next-pwa 설치
npm install next-pwa2단계: next.config.js 설정
// next.config.js
const withPWA = require('next-pwa')({
dest: 'public',
register: true,
skipWaiting: true,
// 개발 환경에서 Service Worker 비활성화
disable: process.env.NODE_ENV === 'development',
// 캐시할 파일 패턴 설정
runtimeCaching: [
{
urlPattern: /^https:\/\/fonts\.(?:googleapis|gstatic)\.com\/.*/i,
handler: 'CacheFirst',
options: {
cacheName: 'google-fonts',
expiration: {
maxEntries: 4,
maxAgeSeconds: 365 * 24 * 60 * 60 // 1년
}
}
},
{
urlPattern: /^https:\/\/api\.example\.com\/.*/i,
handler: 'NetworkFirst',
options: {
cacheName: 'api-cache',
expiration: {
maxEntries: 32,
maxAgeSeconds: 24 * 60 * 60 // 1일
},
networkTimeoutSeconds: 10
}
},
{
urlPattern: /\.(?:png|jpg|jpeg|svg|gif|webp)$/i,
handler: 'CacheFirst',
options: {
cacheName: 'image-cache',
expiration: {
maxEntries: 64,
maxAgeSeconds: 30 * 24 * 60 * 60 // 30일
}
}
}
]
});
/** @type {import('next').NextConfig} */
const nextConfig = {
// Next.js 설정
};
module.exports = withPWA(nextConfig);3단계: Web App Manifest 생성
// public/manifest.json
{
"name": "My Next.js PWA",
"short_name": "NextPWA",
"description": "A Progressive Web App built with Next.js",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#000000",
"orientation": "portrait-primary",
"icons": [
{
"src": "/icons/icon-72x72.png",
"sizes": "72x72",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "/icons/icon-96x96.png",
"sizes": "96x96",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "/icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "/icons/icon-144x144.png",
"sizes": "144x144",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "/icons/icon-152x152.png",
"sizes": "152x152",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "/icons/icon-384x384.png",
"sizes": "384x384",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable any"
}
]
}4단계: 메타데이터 설정 (App Router)
// app/layout.tsx
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'My Next.js PWA',
description: 'A Progressive Web App built with Next.js',
manifest: '/manifest.json',
themeColor: '#000000',
appleWebApp: {
capable: true,
statusBarStyle: 'default',
title: 'NextPWA'
},
formatDetection: {
telephone: false
},
openGraph: {
type: 'website',
siteName: 'My Next.js PWA',
title: 'My Next.js PWA',
description: 'A Progressive Web App built with Next.js'
},
twitter: {
card: 'summary',
title: 'My Next.js PWA',
description: 'A Progressive Web App built with Next.js'
}
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ko">
<head>
<link rel="icon" href="/favicon.ico" />
<link rel="apple-touch-icon" href="/icons/icon-192x192.png" />
</head>
<body>{children}</body>
</html>
);
}5단계: 설치 프롬프트 컴포넌트 (선택사항)
// components/InstallPrompt.tsx
'use client';
import { useEffect, useState } from 'react';
interface BeforeInstallPromptEvent extends Event {
prompt: () => Promise<void>;
userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>;
}
export function InstallPrompt() {
const [deferredPrompt, setDeferredPrompt] =
useState<BeforeInstallPromptEvent | null>(null);
const [showInstallButton, setShowInstallButton] = useState(false);
useEffect(() => {
const handler = (e: Event) => {
// 브라우저 기본 설치 배너 방지
e.preventDefault();
setDeferredPrompt(e as BeforeInstallPromptEvent);
setShowInstallButton(true);
};
window.addEventListener('beforeinstallprompt', handler);
return () => {
window.removeEventListener('beforeinstallprompt', handler);
};
}, []);
const handleInstallClick = async () => {
if (!deferredPrompt) return;
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
console.log(`사용자 선택: ${outcome}`);
setDeferredPrompt(null);
setShowInstallButton(false);
};
if (!showInstallButton) return null;
return (
<div className="fixed bottom-4 right-4 bg-white shadow-lg rounded-lg p-4 max-w-sm">
<h3 className="text-lg font-semibold mb-2">앱 설치</h3>
<p className="text-sm text-gray-600 mb-4">
홈 화면에 추가하여 빠르게 접근하세요
</p>
<div className="flex gap-2">
<button
onClick={handleInstallClick}
className="flex-1 bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
>
설치하기
</button>
<button
onClick={() => setShowInstallButton(false)}
className="px-4 py-2 rounded border border-gray-300 hover:bg-gray-50"
>
나중에
</button>
</div>
</div>
);
}6단계: 오프라인 페이지 생성 (선택사항)
// app/offline/page.tsx
export default function OfflinePage() {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="text-center">
<h1 className="text-4xl font-bold text-gray-900 mb-4">
오프라인 상태입니다
</h1>
<p className="text-gray-600 mb-8">
인터넷 연결을 확인하고 다시 시도해주세요
</p>
<button
onClick={() => window.location.reload()}
className="bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700"
>
다시 시도
</button>
</div>
</div>
);
}7단계: 빌드 및 배포
# 프로덕션 빌드
npm run build
# 로컬에서 프로덕션 빌드 테스트
npm start배포 시 주의사항:
- HTTPS 필수 (Vercel, Netlify 등은 자동 지원)
- Service Worker 파일 (
sw.js)이 올바르게 서빙되는지 확인 - 아이콘 파일들이 모두 포함되어 있는지 확인
8단계: PWA 동작 확인
Chrome DevTools 확인 방법:
- 개발자 도구 열기 (F12)
- Application 탭 선택
- 왼쪽 메뉴에서 확인:
- Manifest: manifest.json 설정 확인
- Service Workers: Service Worker 등록 상태 확인
- Cache Storage: 캐시된 리소스 확인
- Storage: IndexedDB, Local Storage 확인
Lighthouse 점수 확인:
- Chrome DevTools > Lighthouse 탭
- Categories에서 Progressive Web App 선택
- Analyze page load 클릭
- PWA 체크리스트 결과 확인
모바일에서 테스트:
# 로컬 네트워크에서 접근 가능하도록 실행
npm run dev -- -H 0.0.0.0모바일 기기에서 http://[컴퓨터-IP]:3000 접속하여 설치 배너 테스트
성능 최적화 팁
1. 효율적인 캐싱 전략
// next.config.js - runtimeCaching 최적화
{
urlPattern: /^https:\/\/cdn\.example\.com\/.*/i,
handler: 'CacheFirst',
options: {
cacheName: 'cdn-cache',
expiration: {
maxEntries: 100,
maxAgeSeconds: 7 * 24 * 60 * 60, // 1주일
// 오래된 캐시 자동 정리
purgeOnQuotaError: true
},
cacheableResponse: {
// 성공 응답만 캐시
statuses: [0, 200]
}
}
}2. 프리캐싱 전략
// public/sw.js - 중요 리소스 미리 캐싱
const PRECACHE_URLS = [
'/',
'/offline',
'/styles/critical.css',
'/scripts/main.js'
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open('precache-v1')
.then(cache => cache.addAll(PRECACHE_URLS))
);
});3. 네트워크 우선 전략 (동적 콘텐츠)
// API 요청은 항상 최신 데이터 우선
{
urlPattern: /^https:\/\/api\.example\.com\/posts/i,
handler: 'NetworkFirst',
options: {
cacheName: 'api-posts',
networkTimeoutSeconds: 5, // 5초 후 캐시 사용
expiration: {
maxEntries: 50,
maxAgeSeconds: 5 * 60 // 5분
}
}
}4. 리소스 크기 최적화
// next.config.js - 이미지 최적화
const nextConfig = {
images: {
formats: ['image/avif', 'image/webp'],
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
},
};디버깅 및 트러블슈팅
일반적인 문제
문제 1: Service Worker가 등록되지 않음
증상:
Failed to register ServiceWorker: The script has an unsupported MIME type해결 방법:
// next.config.js에서 설정 확인
const withPWA = require('next-pwa')({
dest: 'public',
// MIME type 문제 해결
buildExcludes: [/middleware-manifest\.json$/],
});문제 2: 캐시가 업데이트되지 않음
원인: Service Worker가 오래된 버전을 사용 중
해결 방법:
// Service Worker 강제 업데이트
if ('serviceWorker' in navigator) {
navigator.serviceWorker.getRegistrations().then(registrations => {
registrations.forEach(registration => {
registration.update(); // 강제 업데이트
});
});
}문제 3: iOS Safari에서 설치 배너가 표시되지 않음
원인: iOS는 자체 설치 방식 사용
해결 방법:
// iOS 전용 설치 안내
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
const isStandalone = ('standalone' in window.navigator) &&
(window.navigator as any).standalone;
if (isIOS && !isStandalone) {
// iOS 전용 설치 안내 표시
return (
<div>
<p>Safari 공유 버튼을 눌러 "홈 화면에 추가"를 선택하세요</p>
</div>
);
}Chrome DevTools 활용
Service Worker 디버깅:
1. Chrome DevTools > Application > Service Workers
2. "Update on reload" 체크 - 새로고침 시 항상 최신 버전 사용
3. "Bypass for network" 체크 - 캐시 우회하여 네트워크 직접 호출
4. "Unregister" - Service Worker 등록 해제캐시 확인 및 삭제:
1. Chrome DevTools > Application > Cache Storage
2. 각 캐시 항목 확인
3. 마우스 우클릭 > Delete - 특정 캐시 삭제
4. Clear site data - 모든 캐시 삭제관련 개념
유사 개념
-
Hybrid Apps (하이브리드 앱): Cordova, Capacitor 같은 프레임워크로 웹뷰를 네이티브 앱으로 패키징
- PWA vs Hybrid: PWA는 설치 없이 웹에서 바로 사용 가능하지만, 하이브리드는 앱스토어 배포 필요
-
SPA (Single Page Application): 단일 페이지에서 동적으로 콘텐츠를 로드하는 웹 앱
- PWA vs SPA: PWA는 SPA에 오프라인 기능과 설치 기능을 추가한 확장 개념
대안
-
React Native / Flutter:
- 언제 사용: 복잡한 네이티브 기능이 필요하거나 앱스토어 가시성이 중요한 경우
- 장단점:
- 장점: 완전한 네이티브 경험, 모든 하드웨어 접근 가능
- 단점: 플랫폼별 코드 유지보수, 앱스토어 승인 필요, 웹 검색 불가
-
Electron (데스크톱):
- 언제 사용: 데스크톱 앱이 필요한 경우
- 장단점:
- 장점: 데스크톱 환경에서 완전한 기능
- 단점: 큰 앱 크기, 메모리 사용량 높음
더 알아보기
심화 학습
- Service Worker API 상세: MDN - Service Worker API
- Web App Manifest: MDN - Web App Manifest
- Push API: MDN - Push API
- Background Sync: MDN - Background Sync
실습 자료
- Workbox: Workbox 공식 문서 - Google의 Service Worker 라이브러리
- PWA Builder: PWA Builder - PWA 빌더 도구
- Next.js PWA 예제: Next.js PWA Examples
커뮤니티 및 도구
- Lighthouse: Chrome 내장 PWA 점수 측정 도구
- PWA Stats: PWA Stats - PWA 사례 연구 모음
- Can I Use: Can I Use - Service Worker - 브라우저 호환성 확인
모범 사례
참고 자료
공식 문서
유용한 도구
- PWA Asset Generator - PWA 아이콘/스플래시 자동 생성
- Workbox - Service Worker 라이브러리
- Lighthouse CI - CI/CD PWA 점수 자동 측정