PWA 설정
웹 앱 매니페스트와 서비스 워커를 설정하고 캐시·오프라인·설치 가능 여부를 운영 빌드에서 검증합니다.
현대 웹 애플리케이션은 단순 정보 제공을 넘어 네이티브 앱에 가까운 경험을 제공하는 방향으로 진화하고 있습니다.
프로그레시브 웹 앱(Progressive Web App, PWA)은 이 목표를 달성하기 위한 웹 기술 집합입니다.
앱 스토어 설치 없이도 모바일 앱처럼 동작하도록 웹의 접근성과 기능을 함께 끌어올립니다.
Next.js는 PWA 구현에 필요한 기능을 지원하고, 라이브러리와 설정을 통해 비교적 쉽게 PWA로 전환할 수 있습니다.
이 절에서는 App Router의 매니페스트 규칙과 직접 작성한 서비스 워커를 바탕으로 설치, 오프라인 지원, 업데이트, 푸시 알림의 경계를 익힙니다.
먼저 PWA 설정을 manifest, service worker, 캐시 전략, 운영 검증이라는 네 축으로 나눠 큰 그림을 잡습니다.
Next.js에서 PWA를 붙일 때는 파일 하나가 아니라 manifest, service worker, 캐시 정책, 운영 검증이 같이 맞아야 한다.
- identityapp/manifest.ts
identity MetadataRoute.Manifest를 반환해 이름, 아이콘, start_url, display를 선언한다.
- workerservice worker
worker 요청을 가로채 캐시와 오프라인 fallback을 결정한다. 개발 모드에서는 보통 비활성화한다.
- cacheruntime caching
cache 정적 파일, 이미지, 페이지, API마다 freshness 요구가 달라 전략을 분리해야 한다.
- releaseproduction check
release HTTPS, Application 탭, Offline 모드, Lighthouse로 실제 설치성과 오프라인 동작을 확인한다.
| 항목 | 확인 위치 | 좋은 상태 | 나쁜 신호 |
|---|---|---|---|
| manifest | app/manifest.ts | 아이콘과 start_url이 유효 | 설치 버튼이 뜨지 않음 |
| service worker | Application / Service Workers | activated, controlled | dev 캐시가 계속 남음 |
| offline | Network Offline | 핵심 화면 fallback 표시 | 빈 화면 또는 500 |
| update | 새 배포 후 새로고침 | 새 버전 안내 가능 | 오래된 asset 고착 |
PWA란 무엇이며 왜 중요한가요?
PWA는 웹 기술(HTML, CSS, JavaScript)로 구축되지만, 웹의 장점(접근성, SEO)과 네이티브 앱의 장점(오프라인 지원, 푸시 알림, 홈 화면 설치)을 결합한 애플리케이션입니다.
PWA는 점진적 향상(Progressive Enhancement) 원칙을 따르므로, 최신 PWA 기능을 지원하는 브라우저에서는 앱 같은 경험을 제공하고 그렇지 않은 브라우저에서는 일반 웹사이트처럼 작동합니다.
PWA의 핵심 기능 및 이점- 신뢰성 (Reliable): 서비스 워커(Service Worker)를 사용하여 네트워크 연결이 불안정하거나 오프라인 상태에서도 빠르게 로드되고 작동합니다. 캐싱 전략을 통해 반복 방문 시 거의 즉시 로드됩니다.
- 빠른 로딩 (Fast): 자산과 응답 특성에 맞는 캐싱 전략을 적용하면 반복 방문과 불안정한 네트워크에서 로딩 시간을 줄일 수 있습니다. 잘못된 캐시 정책은 오래된 데이터나 불필요한 저장 공간을 만들 수 있으므로 측정이 필요합니다.
-
높은 참여도 (Engaging)
- 홈 화면 설치(Add to Home Screen): 사용자가 웹사이트를 스마트폰의 홈 화면에 추가하여 앱처럼 실행할 수 있습니다.
- 푸시 알림(Push Notifications): 사용자에게 실시간 알림을 보내 재방문을 유도하고 참여도를 높입니다.
- 풀스크린 모드: 브라우저 UI 없이 전체 화면으로 실행되어 몰입감을 제공합니다.
- 접근성 및 발견 용이성: URL을 통해 접근 가능하고 검색 엔진에 노출됩니다. 앱 스토어 설치 과정 없이 바로 사용할 수 있습니다.
- 점진적 호환성: 일반 웹 경험은 폭넓은 브라우저와 장치에서 제공할 수 있습니다. 다만 설치, 푸시 알림, 백그라운드 동작,
beforeinstallprompt같은 PWA 기능의 지원 범위는 브라우저와 운영체제마다 다르므로 기능 감지와 대체 경로를 함께 준비해야 합니다. - 적은 데이터 사용량: 효율적인 캐싱으로 데이터 사용량을 줄입니다.
App Router에서 PWA 구성하기
Next.js 16은 App Router의 매니페스트 파일 규칙을 기본 제공하며, 서비스 워커는 앱의 캐시 정책에 맞게 직접 작성할 수 있습니다.
이 기본 구성에는 별도 PWA 패키지가 필요하지 않습니다.
웹 앱 매니페스트 작성
src/app/manifest.ts는 이름, 아이콘, 시작 URL, 표시 모드를 타입 안전하게 정의하는 특별 파일입니다.
import type { MetadataRoute } from 'next';
export default function manifest(): MetadataRoute.Manifest {
return {
name: 'My Next.js PWA App',
short_name: 'Next PWA',
description: 'A Next.js PWA example.',
start_url: '/ko',
display: 'standalone',
background_color: '#ffffff',
theme_color: '#0f172a',
icons: [
{
src: '/icons/icon-192.png',
sizes: '192x192',
type: 'image/png',
},
{
src: '/icons/icon-512.png',
sizes: '512x512',
type: 'image/png',
},
],
};
}아이콘 파일은 public/icons에 두고 실제 픽셀 크기와 sizes 값을 일치시킵니다.
Next.js가 이 파일을 /manifest.webmanifest로 제공하고 필요한 <link rel="manifest">를 추가하므로 직접 링크 태그를 만들 필요가 없습니다.
서비스 워커 작성
서비스 워커는 public/sw.js에 두고 캐시할 범위를 최소한으로 시작합니다.
다음 예제는 오프라인 안내 화면만 미리 저장하고, 문서 이동이 네트워크에서 실패했을 때 그 화면으로 대체합니다.
const CACHE_NAME = 'app-shell-v1';
const OFFLINE_URL = '/offline.html';
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => cache.add(OFFLINE_URL)),
);
});
self.addEventListener('activate', (event) => {
event.waitUntil(
caches
.keys()
.then((keys) =>
Promise.all(
keys
.filter(
(key) => key.startsWith('app-shell-') && key !== CACHE_NAME,
)
.map((key) => caches.delete(key)),
),
),
);
});
self.addEventListener('fetch', (event) => {
if (event.request.mode !== 'navigate') return;
event.respondWith(
fetch(event.request).catch(() => caches.match(OFFLINE_URL)),
);
});public/offline.html에는 특정 locale 라우트나 서버 렌더링에 의존하지 않는 간단한 안내 화면을 만듭니다.
정적 파일이므로 서비스 워커 설치 시 어느 언어 경로에서도 같은 URL을 캐시할 수 있습니다.
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>오프라인</title>
</head>
<body>
<main>
<h1>네트워크 연결을 확인해 주세요.</h1>
<p>연결이 복구되면 페이지를 다시 불러오세요.</p>
<button type="button" onclick="location.reload()">다시 시도</button>
</main>
</body>
</html>인증 응답, 사용자별 API, 결제 결과는 이 예제처럼 무조건 캐싱하면 안 됩니다.
브라우저에 등록
등록 로직은 작은 클라이언트 컴포넌트로 분리하고 운영 환경에서만 실행합니다.
'use client';
import { useEffect } from 'react';
export function PwaRegister() {
useEffect(() => {
if (
process.env.NODE_ENV === 'production' &&
'serviceWorker' in navigator
) {
navigator.serviceWorker.register('/sw.js');
}
}, []);
return null;
}앞 절에서 만든 src/app/[locale]/layout.tsx에 등록 컴포넌트와 메타데이터 export를 병합합니다.
이미 이 파일이 <html>과 <body>를 반환하는 Root Layout이므로 상위 src/app/layout.tsx를 새로 만들지 않습니다.
import type { Metadata, Viewport } from 'next';
import { notFound } from 'next/navigation';
import { isLocale, locales } from '@/i18n/config';
import { PwaRegister } from './pwa-register';
export const metadata: Metadata = {
title: 'My Next.js PWA App',
description: 'A Next.js PWA example.',
};
export const viewport: Viewport = {
themeColor: '#0f172a',
};
export function generateStaticParams() {
return locales.map((locale) => ({ locale }));
}
export default async function LocaleLayout({
children,
params,
}: {
children: React.ReactNode;
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
if (!isLocale(locale)) {
notFound();
}
return (
<html lang={locale}>
<body>
<PwaRegister />
{children}
</body>
</html>
);
}PWA 기능 구현 및 테스트
구현 단계로 들어가기 전에 서비스 워커가 설치되고 활성화된 뒤 요청을 어떻게 제어하는지 먼저 확인합니다.
PWA 오류의 상당수는 캐시 전략보다 수명 주기 이해 부족에서 생긴다. 새 worker가 바로 화면을 지배하지 않을 수 있다.
- install
precache 목록을 받고 앱 shell 같은 기본 자산을 저장한다.
- waiting
기존 탭이 열려 있으면 새 worker는 대기할 수 있다.
- activate
낡은 캐시를 정리하고 현재 페이지를 제어할 준비를 끝낸다.
- fetch
요청마다 network-first, cache-first, fallback 중 하나를 선택한다.
- 업데이트가 늦어지는 이유
새 서비스 워커가 설치돼도 이전 탭이 닫히지 않으면 waiting 상태가 남을 수 있다. skipWaiting은 편하지만 사용 중 화면과 캐시가 어긋날 수 있다.
- 운영 UX 기준
새 버전 감지 시 조용히 reload하지 말고, 저장 중인 작업이 있는 화면에서는 새로고침 안내를 띄워야 한다.
| 상태 | 브라우저 의미 | 개발자가 확인할 것 |
|---|---|---|
| installing | 캐시 준비 중 | precache 실패와 404 파일 |
| waiting | 새 worker가 교체 대기 | 새 버전 안내 또는 skipWaiting 정책 |
| activated | 요청 제어 가능 | clients.claim 필요 여부 |
오프라인 지원 (Caching Strategy)
서비스 워커는 작성한 fetch 처리 범위만 제어합니다.
캐시 대상과 전략은 요청의 성격에 따라 명시적으로 정해야 합니다.
- 정적 자산: 파일명이 해시로 바뀌는 자산은
cache-first전략을 검토할 수 있습니다. - 공개 문서: 최신성과 오프라인 접근을 함께 원하면
stale-while-revalidate를 검토합니다. - 사용자별 데이터: 인증·권한·개인정보 응답은 공유 캐시에 넣지 않고 네트워크 실패 UI를 따로 설계합니다.
캐시 전략은 요청 종류마다 달라야 합니다.
정적 파일, 문서 페이지, 사용자별 API를 같은 방식으로 캐싱하면 빠른 화면 대신 오래된 화면이 남을 수 있습니다.
캐시는 빠르게 보이게 하는 장치이지만, 데이터 성격별로 신선도 요구가 다르다.
- 빠른 화면
반복 방문 자산은 캐시에 둔다.
- 정확한 데이터
변하는 데이터는 네트워크를 먼저 본다.
- 실패 UX
네트워크 실패 시 재시도와 stale 표시를 제공한다.
| 요청 종류 | 권장 전략 | 오프라인 처리 | 위험 신호 |
|---|---|---|---|
| 앱 shell, CSS, JS | precache / cache-first | 방문 전에도 기본 화면 유지 | 배포 후 낡은 asset 고착 |
| 이미지와 폰트 | stale-while-revalidate | 이전 이미지 표시 | 큰 파일 무제한 캐시 |
| 문서 페이지 | network-first + fallback | offline page 또는 stale page | 404 대신 빈 화면 |
| 사용자별 API | network-first | 명시적 stale 표시 | 개인 데이터가 오래 남음 |
Next.js 앱을 프로덕션 빌드하고 실행합니다 (npm run build && npm run start).
브라우저에서 앱에 접속합니다.
개발자 도구(Chrome DevTools)를 열고 Application 탭으로 이동합니다.
Service Workers 섹션에서 서비스 워커가 활성화되어 있는지 확인합니다.
Network 탭으로 이동하여 네트워크 상태를 Offline 으로 변경합니다.
페이지를 새로고침하거나 다른 페이지로 이동하여 오프라인에서도 앱이 작동하는지 확인합니다.
푸시 알림 (Push Notifications)
푸시 알림은 사용자의 명시적 권한을 받은 뒤 서버가 브라우저 구독 정보로 메시지를 보내는 기능입니다.
구현하려면 다음 단계가 필요합니다.
서비스 워커에서 푸시 이벤트 수신: 서비스 워커 파일(sw.js)에서 push 이벤트를 수신하고 알림을 표시하는 로직을 직접 구현합니다.
푸시 API 사용: 클라이언트 측에서 PushManager API를 사용하여 사용자에게 알림 권한을 요청하고, 구독 정보를 백엔드로 전송하여 저장합니다.
백엔드에서 푸시 메시지 전송: 웹 푸시 라이브러리(예: web-push for Node.js)를 사용하여 저장된 구독 정보로 사용자에게 푸시 알림을 보냅니다.
VAPID 키 설정이 필요합니다.
홈 화면 설치 (Add to Home Screen)
사용자가 PWA를 홈 화면에 설치할 수 있도록 유도하는 기능입니다.
-
설치 조건 충족
- 웹 앱 매니페스트 파일이 유효해야 합니다.
- HTTPS로 서비스되어야 합니다.
- 브라우저별 설치 가능성 기준과 사용자 설정을 충족해야 합니다.
- 서비스 워커의 역할: 최신 브라우저에서 설치 자체의 공통 필수 조건은 아니지만, 오프라인 화면·푸시 알림·백그라운드 동작에는 필요합니다.
- 설치 프롬프트: 지원하는 브라우저는 설치 가능 조건을 충족하면 앱 설치 또는 홈 화면에 추가 UI를 제공합니다. Chromium 계열에서는
beforeinstallprompt로 사용자 제스처에 맞춘 버튼을 만들 수 있지만, 모든 브라우저가 이 이벤트를 제공하는 것은 아닙니다.
홈 화면 설치 버튼은 UI만 만든다고 동작하지 않습니다.
브라우저가 요구하는 설치 가능성 조건을 통과해야 프롬프트를 제어할 수 있습니다.
개발 환경에서 PWA 테스트
예제 등록 코드는 개발 모드에서 서비스 워커를 등록하지 않습니다.
캐시가 개발 결과를 가리지 않도록 PWA 동작은 프로덕션 빌드(npm run build) 후 Next.js 서버를 시작(npm run start)해 확인합니다.
npm run build
npm run start그 후 localhost:3000 (또는 설정된 포트)으로 접속하여 개발자 도구의 Application 탭에서 서비스 워커와 매니페스트를 확인하고, 네트워크를 오프라인으로 전환하여 캐싱 기능을 테스트할 수 있습니다.
프로덕션 검증은 빌드, 서비스 워커 등록, 매니페스트 설치성, 오프라인 fallback을 순서대로 확인해야 합니다.
Next.js core는 Service Worker를 대신 만들지 않는다. public/sw.js를 직접 작성하고 브라우저 등록 결과를 확인한다.
- 1작성
public/sw.js install, activate, fetch와 캐시 범위를 직접 정의한다.
- 2등록
register("/sw.js") 브라우저 전용 코드에서 한 번 등록하고 실패를 기록한다.
- 3제어 확인
Application 탭에서 scope, activated, controlled 상태를 확인한다.
- 4운영 검증
Offline과 새 배포에서 fallback, 캐시 갱신, 이전 버전 정리를 확인한다.
| 검증 | A 기준 | 실패 신호 |
|---|---|---|
| Service Worker | activated and running | 등록 실패, scope 불일치 |
| Manifest | installable | 아이콘 누락, start_url 오류 |
| Offline | fallback 표시 | 빈 화면, 무한 로딩 |
| Update | 새 버전 안내 | 오래된 캐시 유지 |
PWA는 웹 애플리케이션에 오프라인 캐싱, 홈 화면 설치, 푸시 알림 같은 기능을 추가하는 방식입니다.
Next.js PWA를 운영할 때는 캐시 전략, 업데이트 처리, 알림 권한, 브라우저 지원 범위를 함께 확인해야 합니다.
출시 전에는 기능이 켜졌는지보다 실패 상황에서 사용자가 다음 행동을 이해할 수 있는지까지 점검합니다.
설치, 오프라인, 업데이트, 알림 권한은 사용자 기기와 브라우저 상태에 따라 다르게 실패한다.
- 앱 정체성
확인: 이름, 아이콘, start_url, theme_color가 실제 브랜드와 맞는다. 실패: 홈 화면 아이콘이 깨지거나 독립 실행 화면이 웹뷰처럼 보인다.
- 오프라인 전략
확인: 핵심 화면은 fallback 또는 stale 상태를 보여준다. 실패: 네트워크 차단 시 빈 화면이나 오류 페이지만 남는다.
- 설치 유도
확인: 조건을 만족할 때만 설치 CTA가 보이고 결과를 기록한다. 실패: 지원하지 않는 브라우저에서도 같은 버튼을 노출한다.
- 업데이트
확인: 새 worker와 낡은 캐시가 충돌할 때 안내 UX가 있다. 실패: 배포 뒤 일부 사용자만 옛 화면을 계속 본다.
| 출시 전 질문 | 예/아니오 기준 | 증거 |
|---|---|---|
| 앱처럼 열리는가 | standalone display 확인 | 설치 후 새 창 스크린샷 |
| 오프라인 실패가 설명되는가 | fallback 또는 retry UI | Network Offline 캡처 |
| 새 배포가 전달되는가 | 업데이트 안내 또는 새로고침 정책 | worker version 로그 |
마지막으로 manifest, service worker, runtime caching, 설치 UX의 책임을 분리해 전체 PWA 구성을 다시 정리합니다.
Next.js PWA는 파일 역할이 섞이지 않아야 설치와 오프라인 문제가 줄어든다.
- 1핵심 1
manifest는 앱의 정체성, service worker는 네트워크 판단을 맡는다
- 2핵심 2
manifest는 앱의 정체성, service worker는 네트워크 판단을 맡는다
- 3핵심 3
Next.js PWA는 파일 역할이 섞이지 않아야 설치와 오프라인 문제가 줄어든다.
- 4핵심 4
manifest 이름/아이콘/start_url 설치 카드 service worker cache/network/fallback 요…