블로그: 오프라인에서도 되는 블로그 — PWA 적용기
블로그: 오프라인에서도 되는 블로그에 대해 "왜?"라고 물으면 답할 수 있으신가요?
\n\n> 블로그: 오프라인에서도 되는 블로그에 대해 "왜?"라고 물으면 답할 수 있으신가요?\n블로그에 PWA를 적용하면 한 번 방문한 글을 오프라인에서도 읽을 수 있습니다. 지하철에서 글 읽다가 터널 들어가도 끊기지 않는다는 거죠.
솔직히 개인 블로그에 PWA가 꼭 필요한가 싶기도 했는데, vite-plugin-pwa 하나면 설정이 끝나니까 안 할 이유가 없었습니다.
PWA가 뭔지 간단히
| 기능 | 설명 |
|---|---|
| 오프라인 지원 | 캐시된 페이지를 네트워크 없이 볼 수 있음 |
| 홈 화면 추가 | 앱 아이콘으로 바로가기 생성 |
| 전체화면 모드 | 브라우저 주소창 없이 앱처럼 동작 |
핵심은 Service Worker 입니다. 브라우저와 네트워크 사이에 프록시처럼 동작하면서, 요청을 가로채서 캐시된 응답을 돌려줍니다.
vite-plugin-pwa 설정
Service Worker를 직접 작성하면 꽤 복잡한데, vite-plugin-pwa가 Workbox 기반으로 자동 생성해줍니다.
// vite.config.js
import { VitePWA } from "vite-plugin-pwa";
export default defineConfig({
plugins: [
react(),
VitePWA({
registerType: "autoUpdate",
manifest: {
name: "sim.junghun — 개발 블로그",
short_name: "sim.junghun",
description: "개발 블로그",
theme_color: "#0a0a0f",
background_color: "#0a0a0f",
display: "standalone",
start_url: "/",
scope: "/",
icons: [
{ src: "/web-app-manifest-192x192.png", sizes: "192x192", type: "image/png", purpose: "any" },
{ src: "/web-app-manifest-192x192.png", sizes: "192x192", type: "image/png", purpose: "maskable" },
{ src: "/web-app-manifest-512x512.png", sizes: "512x512", type: "image/png", purpose: "any" },
{ src: "/web-app-manifest-512x512.png", sizes: "512x512", type: "image/png", purpose: "maskable" },
],
},
devOptions: {
enabled: false,
},
}),
],
});
몇 가지 포인트:
registerType: "autoUpdate"— 새 버전이 감지되면 자동 업데이트합니다. 사용자가 새로고침 안 해도 최신 버전을 봅니다.display: "standalone"— 홈 화면에서 열면 브라우저 UI 없이 앱처럼 보입니다.devOptions.enabled: false— 개발 중에는 Service Worker를 끕니다. 안 끄면 코드 수정이 캐시에 가려서 반영 안 되는 지옥을 맛봅니다.
Workbox 캐싱 전략
어떤 리소스를 어떻게 캐싱할지 설정하는 부분입니다.
workbox: {
// 프리캐싱: 빌드 결과물을 Service Worker 설치 시 미리 캐싱
globPatterns: ["**/*.{js,css,html,ico,svg,woff,woff2}"],
maximumFileSizeToCacheInBytes: 3 * 1024 * 1024,
// SPA 라우팅: 모든 경로에서 index.html 반환
navigateFallback: "/index.html",
// 런타임 캐싱: 동적 리소스
runtimeCaching: [
{
urlPattern: /\.(?:png|jpg|jpeg|svg|gif|webp)$/i,
handler: "CacheFirst",
options: {
cacheName: "images",
expiration: {
maxEntries: 100,
maxAgeSeconds: 30 * 24 * 60 * 60, // 30일
},
},
},
{
urlPattern: /\/src\/assets\/posts\//,
handler: "CacheFirst",
options: {
cacheName: "post-assets",
expiration: {
maxEntries: 200,
maxAgeSeconds: 30 * 24 * 60 * 60,
},
},
},
],
},
프리캐싱 vs 런타임 캐싱
| 프리캐싱 | 런타임 캐싱 | |
|---|---|---|
| 시점 | Service Worker 설치 시 | 사용자가 실제 접근할 때 |
| 대상 | JS, CSS, HTML, 폰트 | 이미지, 포스트 에셋 |
| 특징 | 방문 전에 미리 다운로드 | 한 번 본 것만 캐시 |
JS/CSS 같은 핵심 에셋은 프리캐싱으로 미리 받아두고, 이미지처럼 양이 많은 건 런타임에 캐싱합니다.
CacheFirst 전략
캐시에 있으면 캐시에서 바로 반환하고, 없으면 네트워크로 요청합니다. 이미지나 폰트처럼 잘 안 바뀌는 리소스에 적합합니다. 한 번 로드된 이미지는 30일 동안 네트워크 요청 없이 바로 나옵니다.
Web App Manifest
manifest 설정에서 주의할 점 몇 가지:
- 아이콘은 192px, 512px 두 가지 필수 — 둘 다 없으면 설치 프롬프트가 안 뜸
purpose: "maskable"— Android에서 기기별 아이콘 형태(원형, 사각형 등)에 맞게 잘려서 표시됨.any와maskable둘 다 지정하는 게 좋음theme_color— 모바일 브라우저 상단 색상. 블로그 배경색이랑 맞추면 일체감이 생김
빌드 결과
빌드하면 이런 로그가 나옵니다.
PWA v0.x.x
mode: generateSW
precache: 25 entries (1564.42 KiB)
25개 항목(약 1.5MB)이 프리캐싱됩니다. JS, CSS, HTML, 폰트 파일이 전부 포함된 겁니다.
개발 중 주의사항
개발 모드에서 Service Worker가 켜져 있으면 진짜 골치 아픕니다.
- 코드 수정해도 캐시된 파일이 계속 나옴
- "왜 안 되지?" 하고 2시간 삽질 후 캐시 문제인 걸 발견
- Chrome DevTools에서 "Application > Service Workers > Unregister" 해야 함
그래서 devOptions.enabled: false로 개발 중에는 완전히 꺼뒀습니다. 프로덕션에서만 동작하도록요.
정리
| 설정 | 역할 |
|---|---|
vite-plugin-pwa | Service Worker + Manifest 자동 생성 |
registerType: autoUpdate | 새 버전 감지 시 자동 업데이트 |
globPatterns | 빌드 에셋 프리캐싱 |
runtimeCaching (CacheFirst) | 이미지/포스트 에셋 동적 캐싱 |
manifest | 앱 이름, 아이콘, 디스플레이 모드 |
vite-plugin-pwa 덕분에 Service Worker 코드를 직접 작성하지 않아도 됩니다. 설정 파일 하나면 오프라인 지원이랑 홈 화면 추가까지 전부 됩니다.