PWA 구현 개요
PWA 구현 개요
이 가이드는 Next.js 기반 maknaiagent.net을 PWA로 전환하는 데 필요한 모든 파일과 코드, 설정, 배포·테스트 절차를 단계별로 제공합니다.
포함 항목: 웹 앱 매니페스트, 서비스 워커(sw.js), 클라이언트 등록 코드, 오프라인 페이지, 캐시 전략, 업데이트 알림, 푸시 알림 개요, Next.js 통합 팁, 테스트 체크리스트. 바로 복사·붙여넣어 적용할 수 있도록 예제 파일을 제공합니다.
1 웹 앱 매니페스트 파일
파일 경로: public/manifest.json
설명: 홈 화면 설치, 아이콘, 시작 URL, 테마 색상 등 정의.
{
"name": "Maknai Agent",
"short_name": "Maknai",
"description": "Maknai Agent 블로그 및 콘텐츠",
"start_url": "/?source=pwa",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#0f172a",
"orientation": "portrait",
"scope": "/",
"icons": [
{ "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any maskable" },
{ "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" }
]
}
주의: public/icons/에 192×192, 512×512(권장) PNG 파일을 넣고 maskable 메타를 고려하세요.
2 오프라인 폴백 페이지
파일 경로: public/offline.html
설명: 네트워크가 없을 때 보여줄 간단한 HTML.
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>오프라인</title>
<style>
body { font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; display:flex; align-items:center; justify-content:center; height:100vh; margin:0; background:#f8fafc; color:#0f172a; }
.card { text-align:center; padding:24px; border-radius:12px; box-shadow:0 6px 18px rgba(15,23,42,0.08); background:white; max-width:420px; }
a { color:#0f62fe; text-decoration:none; }
</style>
</head>
<body>
<div class="card">
<h1>오프라인입니다</h1>
<p>인터넷 연결이 필요합니다. 연결 후 새로고침해 주세요.</p>
<p><a href="/">홈으로 돌아가기</a></p>
</div>
</body>
</html>
3 서비스 워커 기본 구현
파일 경로: public/sw.js
설명: 핵심 자산을 사전 캐시하고, 이미지·API는 런타임 캐시 전략을 적용합니다. 캐시 네임에 버전 포함으로 배포 시 무효화 가능.
/* public/sw.js */
const CACHE_VERSION = 'v1.0.0';
const PRECACHE = `precache-${CACHE_VERSION}`;
const RUNTIME = `runtime-${CACHE_VERSION}`;
const PRECACHE_URLS = [
'/', '/offline.html', '/manifest.json',
'/icons/icon-192.png', '/icons/icon-512.png',
// 빌드 시점에 생성되는 정적 파일(예: /_next/static/...)을 여기에 추가하면 좋음
];
// Install: pre-cache core assets
self.addEventListener('install', event => {
self.skipWaiting();
event.waitUntil(
caches.open(PRECACHE).then(cache => cache.addAll(PRECACHE_URLS))
);
});
// Activate: cleanup old caches
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(keys => Promise.all(
keys.filter(k => ![PRECACHE, RUNTIME].includes(k)).map(k => caches.delete(k))
)).then(() => self.clients.claim())
);
});
// Fetch: routing and caching strategies
self.addEventListener('fetch', event => {
const { request } = event;
const url = new URL(request.url);
// Ignore non-GET
if (request.method !== 'GET') return;
// Network-first for HTML pages (fresh content)
if (request.headers.get('accept')?.includes('text/html')) {
event.respondWith(
fetch(request).then(res => {
const copy = res.clone();
caches.open(RUNTIME).then(cache => cache.put(request, copy));
return res;
}).catch(() => caches.match(request).then(r => r || caches.match('/offline.html')))
);
return;
}
// Cache-first for static assets (images, css, js)
if (url.pathname.startsWith('/_next/') || url.pathname.endsWith('.css') || url.pathname.endsWith('.js') || url.pathname.endsWith('.png') || url.pathname.endsWith('.jpg') || url.pathname.endsWith('.webp')) {
event.respondWith(
caches.match(request).then(cached => cached || fetch(request).then(res => {
const copy = res.clone();
caches.open(RUNTIME).then(cache => cache.put(request, copy));
return res;
}).catch(() => caches.match('/offline.html')))
);
return;
}
// API requests: stale-while-revalidate
if (url.pathname.startsWith('/wp-json/') || url.pathname.includes('/api/')) {
event.respondWith(
caches.open(RUNTIME).then(cache => cache.match(request).then(cached => {
const networkFetch = fetch(request).then(res => { cache.put(request, res.clone()); return res; }).catch(() => {});
return cached || networkFetch;
}))
);
return;
}
// Default: try network, fallback to cache
event.respondWith(
fetch(request).catch(() => caches.match(request))
);
});
메모: 위 sw.js는 간단·안정적 전략을 목표로 함. Workbox로 더 정교한 전략(정책, 최대 항목 수, 만료 등)을 적용할 수 있음.
4 서비스 워커 등록 코드 (Next.js)
파일 위치: pages/_app.js 또는 app/layout.js(App Router 사용 시) — 클라이언트 전용으로 useEffect에서 등록.
// pages/_app.js (또는 app/layout.js 내부 클라이언트 컴포넌트)
import { useEffect } from 'react';
import '../styles/globals.css';
function registerServiceWorker(onUpdate) {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js').then(reg => {
// updatefound: new SW installing
reg.addEventListener('updatefound', () => {
const newWorker = reg.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
// 새 버전이 설치됨
onUpdate && onUpdate();
}
});
});
}).catch(err => console.error('SW registration failed', err));
}
}
function MyApp({ Component, pageProps }) {
useEffect(() => {
registerServiceWorker(() => {
// 예: 사용자에게 새 버전 알림 표시
const evt = new CustomEvent('swUpdated');
window.dispatchEvent(evt);
});
}, []);
return <Component {...pageProps} />;
}
export default MyApp;
업데이트 UI: window.addEventListener('swUpdated', ...)로 토스트를 띄워 사용자가 새로고침하도록 유도.
5 Next.js 통합 팁
- **public/**에
manifest.json,sw.js,offline.html,icons/를 둡니다. Next.js 빌드 시 그대로 정적 제공됩니다. - next.config.js:
images.domains등 기존 설정 유지. PWA 플러그인 사용 시next-pwa설정을 추가할 수 있음(단, App Router 호환성 확인).
// next.config.js 예시
module.exports = {
reactStrictMode: true,
images: { domains: ['maknaiagent.net'] },
};
- SSR/ISR 고려: HTML은 Network-first로 처리해 최신 콘텐츠를 우선 제공하되, 오프라인 시 캐시된 HTML(또는 offline.html)로 폴백합니다. ISR 페이지는 빌드 시점의 정적 파일이
_next/static에 있으므로 캐시 정책에 따라 빠르게 제공됩니다. - 버전 관리:
CACHE_VERSION을 배포마다 업데이트(예: CI에서 버전 태그 삽입)하면 서비스 워커가 새 버전으로 교체됩니다.
6 푸시 알림 개요 (선택)
- 요구사항: HTTPS, VAPID 키(서버), 클라이언트에서 PushSubscription 저장, 서버에서 Web Push 전송(예:
web-push라이브러리). - 흐름 요약:
- 서버에서 VAPID 키 생성(
web-push generate-vapid-keys) - 클라이언트에서
Notification.requestPermission()및serviceWorker.ready.then(reg => reg.pushManager.subscribe({...}))로 구독 - 구독 객체를 서버에 저장(사용자별)
- 서버에서
web-push로 알림 전송
- 서버에서 VAPID 키 생성(
- 주의: 푸시 페이로드는 암호화되며, 브라우저별 제한(크기, 빈도)이 있음.
7 Workbox 또는 next-pwa 대안
- Workbox: 복잡한 캐시 규칙, 만료, 정리, 라우팅을 코드로 선언 가능.
workbox-build로 빌드 시 precache manifest 생성. - next-pwa: Next.js 전용 플러그인으로 설정 간소화. App Router와 호환성 이슈가 있으므로 사용 전 문서 확인.
- 권장: 초기에는 수동
sw.js로 시작해 요구가 커지면 Workbox로 전환.
8 배포·테스트 체크리스트
- 필수: HTTPS(이미 구성됨),
manifest.json과 아이콘이public/에 존재,sw.js가public/에 존재 - 로컬 테스트:
npm run build && npm start로 프로덕션 빌드 실행- 브라우저(Chrome) DevTools → Application → Manifest 확인
- DevTools → Application → Service Workers에서 등록 확인
- 오프라인 모드(DevTools)에서 페이지 로드 확인(offline.html 또는 캐시된 페이지)
- Lighthouse: PWA 항목 점검(권장 점수 ≥ 90)
- 실사용 테스트: 모바일에서 홈 화면 설치, 오프라인 시나리오, 업데이트 알림 동작 확인
9 보안·운영 권장사항
- 서비스 워커에서 민감 데이터 저장 금지: 인증 토큰, 개인 정보 등은 캐시하지 마세요.
- CSP:
Content-Security-Policy로 스크립트·이미지 출처 제한. 서비스 워커 등록 스크립트는 인라인이 아닌 외부 파일로 두세요. - 캐시 크기·만료 관리: 이미지·API 캐시 항목 수와 만료를 설정해 디스크 과다 사용 방지.
- 버전 관리 자동화: CI에서
CACHE_VERSION을 빌드 번호로 치환해 배포 시 자동 무효화.
10 예제 배포 순서 (요약)
public/manifest.json,public/sw.js,public/offline.html,public/icons/*추가_app.js에 서비스 워커 등록 코드 추가next.config.js확인 후npm run build- 프로덕션 서버에 배포(HTTPS) → 브라우저에서 SW 등록·Lighthouse 점검
- CI에서
CACHE_VERSION자동 치환 및 배포
원하시면 (A) public/sw.js, manifest.json, offline.html, 아이콘 생성 스크립트까지 서버에 자동으로 생성해 드리거나 (B) next-pwa 기반 설정 예시로 전환해 드리겠습니다. 응답으로 A 또는 B 중 하나만 입력하세요.
