1. PWA란?
PWA는 웹 애플리케이션을 네이티브 앱처럼 동작하게 만드는 기술입니다. PWA는 다음과 같은 기능을 제공해줍니다.
- 오프라인 지원: 서비스 워커(service worker)를 사용하여 네트워크 연결이 없거나 불안정한 상황에서도 애플리케이션을 사용할 수 있음
- 푸시 알림: 사용자가 애플리케이션을 사용하지 않을 때에도 푸시 알림을 통해 정보 전달이 가능
- 앱 설치: 사용자가 애플리케이션을 설치하여 네이티브 앱 처러 사용할 수 있음
- 빠른 로딩 속도: 서비스 워커의 캐싱 기능을 사용하여, 애플리케이션의 로딩 속도를 향상시킬 수 있음
2. Next.js 15에서 PWA 설정하기
2.1. manifest.json 생성
다음과 같이 app/manifest.webmanifest/route.ts
작성하여 PWA의 메타데이터(앱 이름, 아이콘, 시작 URL 등)를 설정합니다.
// app/manifest.webmanifest/route.ts
export function GET() {
const manifest = {
name: 'My PWA App',
short_name: 'PWAApp',
description: 'A Progressive Web App built with Next.js',
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',
},
],
};
return new Response(JSON.stringify(manifest), {
headers: {
'Content-Type': 'application/manifest+json',
},
});
}
그리고, manifest의 경로를 metadata에 명시합니다.
export const metadata: Metadata = {
// ...
icons: { icon: '/favicon.ico' },
manifest: '/manifest.webmanifest',
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
// ...
}
- 참고 문서: Mozilla - Manifest
2.2. service worker 설정
public/sw.js
파일을 생성하여 service worker를 설정합니다. 이 파일은 PWA의 오프라인 기능과 푸시 알림을 처리하는 데 사용됩니다.
self.addEventListener('install', (event) => {
console.log('서비스 워커 설치됨');
});
self.addEventListener('activate', (event) => {
console.log('서비스 워커 활성화됨');
});
이후 sw.js를 등록하는 hook을 만듭니다. app/layout.tsx
파일에 다음 코드를 추가합니다.
import { useEffect } from 'react';
export default function useRegisterServiceWorker() {
useEffect(() => {
if (typeof window !== 'undefined' && 'serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker
.register('/sw.js')
.then((registration) => {
console.log('서비스 워커 등록 성공:', registration);
})
.catch((error) => {
console.error('서비스 워커 등록 실패:', error);
});
});
}
}, []);
}
최초에는 Service Worker가 설치되고 활성화되는 콘솔 로그가 출력됩니다. 이후 페이지를 새로고침하면 Service Worker가 등록되는 콘솔 로그만 출력됩니다.

2.3. 블로그 글 캐싱
Service Worker 등록까지 완료되었으니, 블로그 글을 캐싱해야 합니다. 블로그 글을 캐싱하기 위해서는 fetch
이벤트를 사용하여 네트워크 요청을 가로채고, 캐시된 응답을 반환하는 로직을 추가해야 합니다.
// sw.js
// ...
const CACHE_NAME = 'v1';
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
// Serice Worker가 자기 자신(sw.js)은 캐싱하면 안 되므로 제외
if (/\/sw\.js$/.test(url.pathname)) return;
if (url.pathname.startsWith('/posts/')) {
event.respondWith(
caches.open(CACHE_NAME).then((cache) => {
return cache.match(event.request).then((cachedResponse) => {
const fetchPromise = fetch(event.request)
.then((networkResponse) => {
cache.put(event.request, networkResponse.clone());
return networkResponse;
})
// 캐시가 있으면 먼저 응답하고, 없으면 네트워크 응답을 사용
return cachedResponse || fetchPromise;
});
}),
);
}
});
이제 /posts/
하위 경로는 Service Worker에 의해 캐싱됩니다. 블로그 글을 열어보면, 캐싱된 블로그 글이 로드되고 오프라인 상태에서도 블로그 글을 확인할 수 있습니다.
3번째 줄에는
const CACHE_NAME = 'v1';
가 있습니다. 캐싱 키 값은 새로운 내용이 있을때, 강제로 내용을 업데이트 하기 위해 사용됩니다. 이어서 캐싱 삭제하는 방법에 대해 소개합니다.
2.4. 블로그 글 캐싱 제거
블로그 글이 캐싱되어 글을 수정해도 변경된 내용이 반영되지 않습니다. 따라서, 캐싱된 블로그 글을 제거하는 로직이 필요합니다.
caches.delete()
메서드를 사용하여 캐시를 삭제할 수 있습니다. 다음과 같이 sw.js
파일에 캐시 삭제 로직을 추가합니다.
self.addEventListener('activate', (event) => {
const cacheWhitelist = [CACHE_NAME];
event.waitUntil(
caches
.keys()
.then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (!cacheWhitelist.includes(cacheName)) {
return caches.delete(cacheName);
}
}),
);
})
.then(() => self.clients.claim()),
);
});
activate 이벤트를 사용하여, Service Worker가 활성화가 되었을때, 새로운 캐시 버전이 있다면 이전 캐시를 삭제합니다.
2.5. 빌드시 캐시명 자동 업데이트
CACHE_NAME
을 수동으로 업데이트하는 것은 번거롭습니다. 따라서, 빌드 시점에 자동으로 캐시 이름을 업데이트하는 방법을 적용하기 위해 스크립트를 작성해봅시다.
sw.js 캐시명에 __CACHE_NAME__
을 할당하여, 빌드시간을 치환하는 방식을 사용할겁니다. 우선 public/build-sw.js
를 작성합니다.
// public/build-sw.js
const fs = require('fs');
const path = require('path');
const cacheVersion = `blog-${Date.now()}`;
const templatePath = path.join(__dirname, '../public/sw.template.js');
const outputPath = path.join(__dirname, '../public/sw.js');
let swTemplate = fs.readFileSync(templatePath, 'utf8');
// __CACHE_NAME__을 실제 버전(빌드 시간)으로 치환
swTemplate = swTemplate.replace('__CACHE_NAME__', cacheVersion);
fs.writeFileSync(outputPath, swTemplate);
console.log(`sw.js 생성 완료. CACHE_NAME = ${cacheVersion}`);
위 스크립트는 public/sw.template.js
의 __CACHE_NAME__
을 빌드 시간으로 치환하여 sw.js
파일을 새롭게 생성해주는 스크립트 입니다. 그리고, sw.template.js
의 캐시명을 수정합니다.
// public/sw.template.js
const CACHE_NAME = '__CACHE_NAME__';
// ...
마지막으로 package.json 스크립트에 prebuild
를 추가하여, 빌드 전에 node scripts/build-sw.js
명령어가 실행될 수 있도록 합니다.
prebuild는 build 전에 실행되는 스크립트입니다.
{
"scripts": {
"prebuild": "node scripts/build-sw.js",
"build": "next build"
},
...
}
다음 사진과 같이 빌드 할때마다 캐시 값이 변경되므로 따로 관리하지 않아도 됩니다.
