Progressive Web Apps (PWA) 소개
HTTPS 위에 매니페스트와 서비스 워커를 구성해 설치·오프라인 캐시·푸시 알림을 지원하는 PWA 원리를 익힙니다.
우리는 지금까지 프론트엔드와 백엔드 핵심 기술을 익히고, 데이터베이스, 보안, 성능, 개발 도구까지 폭넓게 다뤘습니다.
이제 기본 웹 애플리케이션을 기획하고 배포할 수 있는 기반이 갖춰졌습니다.
다음 질문은 앞으로 무엇을 더 준비해야 하는가입니다.
웹 기술은 빠르게 바뀌고, 오늘의 선택이 내일의 기본이 되기도 합니다.
15장에서는 이런 변화 속에서 실제로 영향력이 큰 흐름을 살펴봅니다.
첫 주제는 PWA(Progressive Web App)입니다.
PWA는 웹 앱을 네이티브 앱에 가까운 경험으로 끌어올리는 접근입니다.
설치, 오프라인 지원, 푸시 알림처럼 웹의 활용 범위를 넓히는 기술을 중심으로 구성됩니다.
PWA CAPABILITIES
PWA 기능은 사용자에게 필요한 것부터 하나씩 선택합니다
설치·오프라인·알림·백그라운드 동기화는 서로 다른 문제를 해결합니다. 대상 기기의 지원과 권한을 확인하고, 쓸 수 없는 경우의 일반 웹 경로를 함께 설계합니다.
먼저: 사용자 문제와 지원 환경
반복 방문, 연결 불안정, 시간에 민감한 알림 중 무엇을 해결할지 정합니다. 서비스 워커는 HTTPS 등 secure context가 필요하며 localhost는 개발 예외입니다.
설치 · 반복 진입을 줄이기
매니페스트에 이름·아이콘·시작 URL·표시 방식을 정의합니다. 브라우저와 OS별 설치 조건·설치 UI를 확인합니다.
미지원·설치하지 않은 사용자도 URL로 계속 이용할 수 있게 합니다.
오프라인 · 끊겨도 핵심 작업 유지
서비스 워커가 캐시한 자원과 요청 처리 정책으로 가능한 기능을 제공합니다. 캐시에 없는 데이터까지 자동으로 생기지는 않습니다.
저장소 한도·만료·캐시 갱신을 정하고 연결 복구 후 서버 데이터와 맞춥니다.
푸시 알림 · 필요한 변화 전달
Push API·서비스 워커·서버의 발송 경로와 알림 권한이 필요합니다. 플랫폼별 지원 조건과 사용자 동의를 확인합니다.
권한을 거부하거나 해지해도 앱 안에서 알림을 확인할 수 있게 합니다.
백그라운드 동기화 · 나중에 전송
지원 환경에서 연결 복구 후 미뤄 둔 작업을 재시도합니다. 실행 시점·배터리·브라우저 정책의 제약을 확인합니다.
미지원 환경에서는 사용자가 앱을 다시 열 때 재시도하는 경로를 둡니다.
매니페스트와 서비스 워커를 추가했다는 사실만으로 모든 플랫폼의 설치·오프라인·푸시가 보장되지는 않습니다. 필요한 기능을 각각 검증합니다.
PWA (Progressive Web App)란?
PWA (Progressive Web App)는 웹 기술(HTML, CSS, JavaScript)을 사용하여 개발되었지만, 네이티브 애플리케이션(iOS, Android 앱)과 유사한 사용자 경험을 제공하는 웹 애플리케이션입니다.
PWA는 웹의 장점(설치 필요 없음, URL 기반 접근 용이성, 빠른 개발 및 배포)과 네이티브 앱의 장점(오프라인 지원, 푸시 알림, 홈 화면 아이콘, 시스템 자원 접근)을 결합하여 사용자에게 더욱 풍부하고 편리한 경험을 제공하는 것을 목표로 합니다.
왜 PWA가 중요한가?- 설치 장벽 낮춤: 사용자는 앱 스토어 방문, 다운로드, 설치 과정 없이 웹사이트 방문만으로 PWA를 사용하고, 필요하면 홈 화면에 추가할 수 있습니다. 이는 사용자 유입 장벽을 크게 낮춥니다.
- 연결 불안정에 대응: 미리 저장한 공개 콘텐츠와 복구 정책이 있는 범위에서 오프라인 이용을 지원할 수 있습니다. 푸시는 대상 환경의 지원과 사용자 권한이 있어야 하며, 설치만으로 이 기능들이 생기는 것은 아닙니다.
- 개발 및 유지보수 효율: 웹 코드와 배포 흐름을 여러 환경에서 재사용할 수 있습니다. 설치 UI·푸시·백그라운드 기능의 지원 범위와 권한은 플랫폼별로 확인해야 하므로 하나의 코드베이스가 모든 기능의 동일 동작을 보증하지는 않습니다.
- 웹 검색으로 발견 가능: 공개 URL과 검색 엔진이 읽을 수 있는 콘텐츠를 유지할 수 있습니다. PWA라는 이름이나 설치 기능만으로 색인·상위 노출이 보장되지는 않습니다. Google의 페이지 경험 안내도 좋은 성능 점수만으로 상위 순위가 보장되지 않는다고 설명합니다.
PWA의 핵심 구성 요소
앞 장의 HTTPS·캐시·배포 흐름을 알고 있다면, 이번에는 “파일을 서버에 올렸는가”와 “현재 클라이언트가 어느 버전의 응답을 쓰는가”를 나누어 봅니다. 설치 가능성, 오프라인 화면, 알림은 각각 필요한 기능을 선택하며 한 기능의 성공으로 나머지를 검증하지 않습니다.
이 장에서는 매니페스트로 설치 정보를 선언하고, 서비스 워커로 필요한 오프라인·백그라운드 기능을 구현하며, HTTPS로 전달합니다. 설치 안내처럼 서비스 워커는 설치 가능성 자체의 보편적인 필수 조건은 아닙니다. 브라우저가 제공하는 설치와 오프라인 구현을 구분합니다.
서비스 워커 (Service Worker)
-
이벤트 기반 실행 계층: 서비스 워커는 페이지와 분리된 워커에서 실행되며 DOM을 직접 조작하지 않습니다. 요청·푸시 등의 이벤트를 처리하고 캐시를 제어할 수 있지만 항상 실행 중인 서버나 타이머가 아닙니다. 브라우저는 유휴 워커를 중단할 수 있으므로 중요한 상태는 메모리 변수에만 두지 않습니다.
-
주요 기능
- 오프라인 지원 (Offline-first): 네트워크 연결이 없거나 불안정한 환경에서도 캐시된 콘텐츠를 제공하여 웹 앱이 동작하도록 합니다.
- 빠른 로딩: 적절한 캐시 적중은 네트워크 대기를 줄일 수 있습니다. 미캐시 요청·오래된 데이터·저장소 한도도 고려해야 하며 모든 페이지가 자동으로 빨라지는 것은 아닙니다.
- 푸시 알림 (Push Notifications): 사용자가 웹 앱을 닫은 상태에서도 서버로부터 푸시 메시지를 받아 알림을 표시할 수 있게 합니다.
- 백그라운드 동기화: 지원 환경에서는 미뤄 둔 작업을 저장하고 재시도할 수 있습니다. 실행 시점·권한·배터리·브라우저 지원에 제약이 있으며, 재전송 중복과 서버 승인도 설계해야 합니다.
-
동작 방식 (간략)
웹 페이지에서 서비스 워커를 등록(Register)합니다.
브라우저는 서비스 워커 파일을 다운로드하고 설치(Install)합니다.
이 단계에서 필요한 에셋들을 캐시에 저장할 수 있습니다.
활성 서비스 워커가 scope 안의 client를 제어하면 그 client의 요청을 fetch 이벤트로 처리할 수 있습니다. 첫 등록 직후 현재 페이지가 바로 제어되는 것은 아니며, 캐싱은 작성한 핸들러가 결정합니다.
웹 앱 매니페스트 (Web App Manifest)
- PWA의 신분증: 웹 앱 매니페스트는 JSON 형식의 파일로, PWA의 메타데이터(이름, 아이콘, 시작 URL, 표시 방식 등)를 정의합니다.
-
주요 기능
- 홈 화면 아이콘: 사용자가 PWA를 홈 화면에 추가했을 때 표시될 아이콘을 정의합니다.
- 시작 화면 정보: 이름·아이콘·배경색 등의 메타데이터를 제공합니다. 브라우저나 OS가 이를 사용해 시작 화면을 구성할 수 있으며, 매니페스트가 임의의 스플래시 화면을 모든 기기에 지정하는 것은 아닙니다.
- 표시 모드 (Display Mode): PWA가 브라우저 UI 없이 독립적인 앱처럼 실행될지(standalone), 최소한의 UI를 가질지(minimal-ui), 아니면 일반 웹 페이지처럼 보일지(browser) 등을 설정합니다.
- 시작 URL: PWA를 실행했을 때 로드될 초기 URL을 지정합니다.
이름과 표시 모드를 선언했다고 설치·시작 화면의 모습이 모든 브라우저에서 같아지는 것은 아닙니다. 아래 예제는 해당 크기의 실제 /icons/icon-192.png, /icons/icon-512.png와 나머지 공개 파일을 먼저 준비했다고 가정합니다. sizes는 이미지 파일의 실제 해상도를 설명하며 파일을 변환하는 명령이 아닙니다.
index.html에 연결:<link rel="manifest" href="/manifest.json">형태로 HTML 파일에 연결합니다.
HTTPS
- PWA의 필수 조건: PWA는 서비스 워커의 보안 및 무결성 문제 때문에 반드시 HTTPS 환경에서 호스팅되어야 합니다. 로컬 개발 환경(
localhost)에서는 예외적으로 HTTP가 허용됩니다. - 보안과 신뢰: HTTPS는 전송 중 도청·변조 위험을 줄입니다. 악성 코드·잘못된 권한 검사·개인 정보의 캐시 저장까지 해결하지는 않으므로 애플리케이션 보안도 필요합니다.
PWA의 주요 특징 및 장점
PWA는 네이티브 앱의 장점을 웹으로 가져오면서도 웹 본연의 장점을 유지합니다.
| 특징 | PWA | 네이티브 앱 (iOS/Android) |
|---|---|---|
| 설치 | 지원 브라우저에서 웹으로 설치; 스토어 배포도 가능 | 주로 스토어로 설치; 플랫폼·배포 정책에 따름 |
| 접근성 | URL로 직접 접근 가능, 즉시 사용 | 검색/설치 과정 필요 |
| 오프라인 | 저장·복구 정책을 구성해 지원 가능 | 앱의 로컬 데이터·복구 정책에 따라 지원 |
| 푸시 알림 | 지원 가능 | 지원 가능 |
| 업데이트 | 새 버전 적용 시점은 활성 서비스 워커·캐시 정책에 따름 | 앱 스토어/플레이 스토어를 통해 수동 업데이트 또는 자동 |
| 개발 비용 | 웹 코드를 재사용할 수 있으나 기기별 검증 필요 | 개별 구현 또는 공유 코드 도구를 선택; 기능·검증 범위에 따름 |
| 배포 | 웹 배포와 캐시·활성 버전 적용을 구분 | 스토어·기업 배포 등 선택한 경로의 정책에 따름 |
| 시스템 접근 | 웹 API 지원과 사용자 권한 범위 안에서 가능 | OS가 허용한 API·사용자 권한 범위 안에서 가능 |
| 용량 | 코드·이미지·오프라인 저장 범위에 따름 | 앱 코드·포함 자원·다운로드 데이터에 따름 |
| 발견 경로 | 공개 웹 콘텐츠의 검색·URL 공유 | 스토어 검색·연결된 웹 콘텐츠 등 별도 경로 |
PWA 구현의 간단한 흐름 (예시)
manifest.json) 생성{
"name": "My Awesome PWA",
"short_name": "My PWA",
"start_url": ".",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#000000",
"icons": [
{
"src": "/icons/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icons/icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}<head>
<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#000000">
<link rel="apple-touch-icon" href="/logo.png">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
</head>sw.js) 생성 및 캐싱 전략 정의// sw.js: 이 목록은 로그인 상태와 무관한 공개 파일이어야 합니다.
const CACHE_PREFIX = 'my-pwa-public-';
const CACHE_NAME = `${CACHE_PREFIX}v1`;
const urlsToCache = [
'/', '/index.html', '/styles.css', '/script.js',
'/icons/icon-192.png', '/icons/icon-512.png',
];
const publicPaths = new Set(urlsToCache);
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => cache.addAll(urlsToCache))
);
});
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
if (event.request.method !== 'GET'
|| url.origin !== self.location.origin
|| url.search !== ''
|| !publicPaths.has(url.pathname)) {
return; // 개인 API·POST·외부 요청은 기본 네트워크 처리로 둡니다.
}
event.respondWith((async () => {
const cache = await caches.open(CACHE_NAME);
const cached = await cache.match(event.request);
if (cached) return cached;
try {
const response = await fetch(event.request);
if (response.status === 200 && response.type === 'basic') {
try {
await cache.put(event.request, response.clone());
} catch (error) {
console.warn('공개 파일을 저장하지 못했습니다');
}
}
return response;
} catch (error) {
if (event.request.mode === 'navigate') {
return new Response('오프라인입니다. 연결 후 다시 시도하세요.', {
status: 503,
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
});
}
return Response.error(); // CSS·JS 요청에 HTML을 대신 보내지 않습니다.
}
})());
});
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(names => Promise.all(
names.filter(name => name.startsWith(CACHE_PREFIX) && name !== CACHE_NAME)
.map(name => caches.delete(name))
))
);
});if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('Service Worker registered: ', registration);
})
.catch(error => {
console.error('Service Worker registration failed: ', error);
});
});
}HTTPS 환경에서 서비스 배포.
(또는 개발 시 localhost 사용)
참고: 빌드 도구나 프레임워크가 설치·푸시·오프라인을 모두 자동으로 구현하지는 않습니다. 도구가 제공하는 매니페스트·서비스 워커 통합 방식과 필요한 플러그인을 확인합니다. 예를 들어 Next.js 공식 PWA 안내는 매니페스트와 서비스 워커를 각각 구성하는 과정을 설명합니다.
PWA는 구현 후에도 캐시 갱신, 설치 가능성, 오프라인 동작을 실제 기기에서 점검해야 합니다. 예제는 사이트 루트 배포와 공개 정적 파일만 다룹니다. 같은 경로가 로그인 사용자마다 다른 HTML을 반환한다면 이 목록에 넣으면 안 됩니다. 주문·결제 API와 개인 정보는 캐시 대상에서 제외했고, 저장소를 열거나 읽는 오류·저장소 삭제·버전 호환성은 여전히 별도로 확인합니다.
코드를 따라갈 때 install의 waitUntil()과 fetch의 respondWith()를 구별하세요. 전자는 설치 작업 완료를 기다리고, 후자는 요청에 돌려줄 응답 Promise를 정합니다. 이 예제는 그 Promise 안에서 cache.put()까지 기다립니다. 저장이 실패해도 온라인 응답은 반환하므로 “응답을 받음”만으로 캐시 저장 성공을 단정하지 않습니다. 미캐시 탐색의 네트워크 실패는 503 안내 응답으로, CSS·JS 실패는 네트워크 오류로 처리하며 거래 성공으로 바꾸지 않습니다.
이 예제는 이름으로 연 cache.match()로 현재 버전만 조회합니다. CacheStorage.match는 여러 캐시를 검색하는 다른 API입니다. activate에서는 이 앱에만 할당한 접두사로 시작하는 이전 캐시만 삭제합니다. 또 Cache API는 HTTP 캐시 헤더를 자동으로 따르지 않으므로 Cache-Control: no-store만으로 코드의 cache.put()을 막을 수 있다고 생각하면 안 됩니다. 공개 파일 허용 목록과 서버의 응답 계약을 함께 관리합니다.
제어 중인 클라이언트가 허용 목록의 공개 GET 파일을 쿼리 없이 요청한다고 정합니다. 현재 버전 캐시에 있으면 오프라인이어도 반환합니다. 캐시가 없고 네트워크도 실패하면 탐색은 503 안내, 다른 자원은 오류가 됩니다. 온라인 basic 200 응답은 복제본의 저장 완료 또는 저장 실패 처리를 기다린 뒤 원 응답을 반환하고, basic 404는 저장하지 않고 반환합니다. 이는 예제 분기의 설명이며 실제 기기에서 서비스 워커를 등록해 관측한 결과는 아닙니다.
독립 확인캐시가 비어 있을 때 네트워크가 실패한 경우와 basic 404를 반환한 경우를 구별해 보세요. 응답을 이미 받았다는 사실만으로 다음 오프라인 요청도 성공한다고 말할 수 있나요? 새 버전의 설치 작업이 실패했거나 기존 버전이 제어 중인 탭이 남아 있다면 적용 시점을 어떻게 확인해야 하나요?
풀이 확인
미캐시 파일의 네트워크 실패는 탐색인지에 따라 503 안내 응답 또는 네트워크 오류가 됩니다. basic 404는 HTTP 응답을 받았지만 저장하지 않습니다. basic 200에서 put을 기다려도 저장이 실패하거나 나중에 저장소가 지워질 수 있으므로 다음 오프라인 성공을 영구 보증하지 않습니다. 설치 실패는 새 버전 전환 성공이 아니며, 설치 성공 뒤에도 기존 client 사용에 따라 새 버전이 waiting에 남을 수 있습니다. 등록 성공·설치 성공·활성 버전·현재 client의 controller를 구별해 확인합니다.
서비스 워커 설치·갱신 생명주기 — 상태 추적
새 버전 설치 성공에도 기존 client가 활성 버전을 사용하면 waiting에 남는다. old/new worker와 client controller 상태의 변화를 따라 activate와 제어를 구분할 가치가 크다.
SERVICE WORKER UPDATE
새 서비스 워커가 설치되어도 기존 탭은 이전 버전을 쓸 수 있습니다
아래는 기존 워커를 새 버전으로 갱신하는 기본 흐름입니다. 설치 성공, 대기 종료, 활성화와 페이지 제어를 구분해야 화면 코드와 캐시 자원의 버전이 어긋나는 일을 줄일 수 있습니다.
- 등록·새 버전 확인
브라우저가 sw.js를 가져와 변경을 확인합니다. 기존 활성 워커는 자신이 제어하던 페이지를 계속 처리합니다.
- install · 새 자원 준비
예제는 waitUntil과 cache.addAll로 자원을 준비합니다. 설치 작업이 실패하면 새 워커는 사용되지 않고 기존 버전은 유지될 수 있습니다.
installed · waiting 기존 워커가 제어하는 페이지가 남아 있으면 새 워커가 기다립니다. 사용자에게 갱신 안내를 보여주는 UI와 대기를 끝내는 API 호출은 별개입니다.
- activate · 갱신 적용
기본적으로 기존 워커의 client가 없어지고 진행 중 작업이 끝나면 활성화합니다. skipWaiting()은 대기를 건너뛰는 별도 정책이므로 구버전 화면·새 자원의 호환성을 먼저 확인합니다.
- activated · 제어하는 요청 처리
활성화된 워커가 제어하는 client의 fetch 이벤트를 처리합니다. 새 워커가 처음 설치된 경우 현재 문서는 보통 다음 탐색부터 제어되며, clients.claim()은 기존 client 제어를 위한 별도 호출입니다.
예제의 캐시 우선 처리
허용 목록의 공개 GET 파일을 현재 버전 캐시에서 먼저 찾습니다. 없으면 네트워크로 요청하며, 네트워크 실패 시 탐색에는 503 안내를, CSS·JS에는 오류를 반환합니다.
운영용 갱신 정책으로 보완
예제는 앱 접두사의 이전 캐시만 정리하고 공개 파일만 저장하며 put 처리를 기다립니다. 저장 실패·저장소 삭제·개인 API·버전 호환성은 별도 정책과 실제 기기 검증이 필요합니다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 도표 이름 | 설치 성공과 현재 요청 제어를 별도로 확인한다 |
| 전체 관계·조건 설명 | 노드 6개·관계 6개는 기본 업데이트 모델이다. 성공적으로 설치한 새 버전은 기존 활성 버전의 client가 사용 중이면 waiting으로 가고 사용 종료 뒤 activate한다. 기존 버전의 사용이 없으면 waiting 대기를 건너 활성화할 수 있다. 마지막 노드는 제어된 client의 요청만 처리하며 첫 등록 시 현재 페이지의 즉시 제어를 보증하지 않는다. 설치 실패/skipWaiting/clients.claim 변경은 원래 보충 설명에서 구별하고 mobile도 같다. |
| 원도표 조건·주석 | 등록·새 버전 확인: 브라우저가 sw.js를 가져와 변경을 확인합니다. 기존 활성 워커는 자신이 제어하던 페이지를 계속 처리합니다. |
| 원도표 조건·주석 | install · 새 자원 준비: 예제는 waitUntil과 cache.addAll로 자원을 준비합니다. 설치 작업이 실패하면 새 워커는 사용되지 않고 기존 버전은 유지될 수 있습니다. |
| 원도표 설명 | 기존 워커가 제어하는 페이지가 남아 있으면 새 워커가 기다립니다. 사용자에게 갱신 안내를 보여 주는 UI와 대기를 끝내는 API 호출은 별개입니다. |
| 원도표 조건·주석 | installed · waiting: 기존 워커가 제어하는 페이지가 남아 있으면 새 워커가 기다립니다. 사용자에게 갱신 안내를 보여 주는 UI와 대기를 끝내는 API 호출은 별개입니다. |
| 원도표 조건·주석 | activate · 갱신 적용: 기본적으로 기존 워커의 client가 없어지고 진행 중 작업이 끝나면 활성화합니다. skipWaiting()은 대기를 건너뛰는 별도 정책이므로 구버전 화면·새 자원의 호환성을 먼저 확인합니다. |
| 원도표 조건·주석 | activated · 제어하는 요청 처리: 활성화된 워커가 제어하는 client의 fetch 이벤트를 처리합니다. 새 워커가 처음 설치된 경우 현재 문서는 보통 다음 탐색부터 제어되며, clients.claim()은 기존 client 제어를 위한 별도 호출입니다. |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| 기존 client | controller: 구버전 / 새 worker: 아직 없음 | 기존 worker가 열린 client를 제어한다 | 등록된 worker와 client controller는 다르다 |
| 새 worker 대기 | controller: 구버전 유지 / 새 worker: waiting | 새 설치가 끝나도 열린 client 때문에 waiting일 수 있다 | 설치 완료가 즉시 takeover라는 뜻은 아니다 |
| 활성화와 제어 | 새 worker: activated / client: 정책에 따라 새 제어 | 활성화 조건과 claim/navigation 정책에 따라 새 controller를 얻는다 | 활성화 상태와 현재 client 제어를 각각 확인한다 |
서비스 워커 설치·갱신 생명주기 — 상태 추적
새 버전 설치 성공에도 기존 client가 활성 버전을 사용하면 waiting에 남는다. old/new worker와 client controller 상태의 변화를 따라 activate와 제어를 구분할 가치가 크다.
SERVICE WORKER UPDATE
새 서비스 워커가 설치되어도 기존 탭은 이전 버전을 쓸 수 있습니다
아래는 기존 워커를 새 버전으로 갱신하는 기본 흐름입니다. 설치 성공, 대기 종료, 활성화와 페이지 제어를 구분해야 화면 코드와 캐시 자원의 버전이 어긋나는 일을 줄일 수 있습니다.
- 등록·새 버전 확인
브라우저가 sw.js를 가져와 변경을 확인합니다. 기존 활성 워커는 자신이 제어하던 페이지를 계속 처리합니다.
- install · 새 자원 준비
예제는 waitUntil과 cache.addAll로 자원을 준비합니다. 설치 작업이 실패하면 새 워커는 사용되지 않고 기존 버전은 유지될 수 있습니다.
installed · waiting 기존 워커가 제어하는 페이지가 남아 있으면 새 워커가 기다립니다. 사용자에게 갱신 안내를 보여주는 UI와 대기를 끝내는 API 호출은 별개입니다.
- activate · 갱신 적용
기본적으로 기존 워커의 client가 없어지고 진행 중 작업이 끝나면 활성화합니다. skipWaiting()은 대기를 건너뛰는 별도 정책이므로 구버전 화면·새 자원의 호환성을 먼저 확인합니다.
- activated · 제어하는 요청 처리
활성화된 워커가 제어하는 client의 fetch 이벤트를 처리합니다. 새 워커가 처음 설치된 경우 현재 문서는 보통 다음 탐색부터 제어되며, clients.claim()은 기존 client 제어를 위한 별도 호출입니다.
예제의 캐시 우선 처리
허용 목록의 공개 GET 파일을 현재 버전 캐시에서 먼저 찾습니다. 없으면 네트워크로 요청하며, 네트워크 실패 시 탐색에는 503 안내를, CSS·JS에는 오류를 반환합니다.
운영용 갱신 정책으로 보완
예제는 앱 접두사의 이전 캐시만 정리하고 공개 파일만 저장하며 put 처리를 기다립니다. 저장 실패·저장소 삭제·개인 API·버전 호환성은 별도 정책과 실제 기기 검증이 필요합니다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 도표 이름 | 설치 성공과 현재 요청 제어를 별도로 확인한다 |
| 전체 관계·조건 설명 | 노드 6개·관계 6개는 기본 업데이트 모델이다. 성공적으로 설치한 새 버전은 기존 활성 버전의 client가 사용 중이면 waiting으로 가고 사용 종료 뒤 activate한다. 기존 버전의 사용이 없으면 waiting 대기를 건너 활성화할 수 있다. 마지막 노드는 제어된 client의 요청만 처리하며 첫 등록 시 현재 페이지의 즉시 제어를 보증하지 않는다. 설치 실패/skipWaiting/clients.claim 변경은 원래 보충 설명에서 구별하고 mobile도 같다. |
| 원도표 조건·주석 | 등록·새 버전 확인: 브라우저가 sw.js를 가져와 변경을 확인합니다. 기존 활성 워커는 자신이 제어하던 페이지를 계속 처리합니다. |
| 원도표 조건·주석 | install · 새 자원 준비: 예제는 waitUntil과 cache.addAll로 자원을 준비합니다. 설치 작업이 실패하면 새 워커는 사용되지 않고 기존 버전은 유지될 수 있습니다. |
| 원도표 설명 | 기존 워커가 제어하는 페이지가 남아 있으면 새 워커가 기다립니다. 사용자에게 갱신 안내를 보여 주는 UI와 대기를 끝내는 API 호출은 별개입니다. |
| 원도표 조건·주석 | installed · waiting: 기존 워커가 제어하는 페이지가 남아 있으면 새 워커가 기다립니다. 사용자에게 갱신 안내를 보여 주는 UI와 대기를 끝내는 API 호출은 별개입니다. |
| 원도표 조건·주석 | activate · 갱신 적용: 기본적으로 기존 워커의 client가 없어지고 진행 중 작업이 끝나면 활성화합니다. skipWaiting()은 대기를 건너뛰는 별도 정책이므로 구버전 화면·새 자원의 호환성을 먼저 확인합니다. |
| 원도표 조건·주석 | activated · 제어하는 요청 처리: 활성화된 워커가 제어하는 client의 fetch 이벤트를 처리합니다. 새 워커가 처음 설치된 경우 현재 문서는 보통 다음 탐색부터 제어되며, clients.claim()은 기존 client 제어를 위한 별도 호출입니다. |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| 기존 client | controller: 구버전 / 새 worker: 아직 없음 | 기존 worker가 열린 client를 제어한다 | 등록된 worker와 client controller는 다르다 |
| 새 worker 대기 | controller: 구버전 유지 / 새 worker: waiting | 새 설치가 끝나도 열린 client 때문에 waiting일 수 있다 | 설치 완료가 즉시 takeover라는 뜻은 아니다 |
| 활성화와 제어 | 새 worker: activated / client: 정책에 따라 새 제어 | 활성화 조건과 claim/navigation 정책에 따라 새 controller를 얻는다 | 활성화 상태와 현재 client 제어를 각각 확인한다 |
PWA 핵심 정리
핵심은 서비스 워커, 매니페스트, HTTPS를 기능 체크리스트가 아니라 배포 전략으로 함께 다루는 것입니다.
이번 장에서는 웹의 한계를 넘어 네이티브 앱과 같은 사용자 경험을 제공하는 PWA(Progressive Web App)에 대해 학습했습니다.
PWA의 개념과 서비스 워커(Service Worker), 웹 앱 매니페스트(Web App Manifest), HTTPS의 역할을 정리했습니다.
이 요소들이 오프라인 지원, 푸시 알림, 홈 화면 추가 기능과 어떻게 연결되는지도 함께 확인했습니다.
PWA는 오프라인 동작, 설치 가능성, 푸시 알림처럼 웹 앱의 사용성을 높이는 선택지입니다.
모든 웹 애플리케이션에 필요한 것은 아니므로, 서비스 특성과 사용자 환경에 맞춰 적용 여부를 판단해야 합니다.
지원하지 않거나 사용자가 권한을 거부한 경우에도 일반 웹 화면의 핵심 작업이 가능한 대체 경로를 정합니다. 오프라인에 보일 공개 정보와 최신 서버 승인이 필요한 결제·권한 변경을 구별하고, 캐시된 화면만으로 거래 성공을 선언하지 않습니다.
PWA 흐름은 설치 가능성, 서비스 워커 요청 처리, 캐시 갱신을 배포 후 점검 루틴으로 연결해야 안정적으로 유지됩니다.
직접 연습: 캐시 범위와 갱신 시점을 추적한다
- 같은 출처에서
GET /styles.css,POST /api/orders,GET /api/me,GET /styles.css?v=2를 요청합니다. 위 허용 목록과 조건에서 서비스 워커가 응답 Promise를 지정하는 요청은 무엇인가요? 주문 API 실패를 캐시된 성공 화면으로 대신해도 되나요?
풀이
쿼리 없는 GET /styles.css만 처리합니다. POST는 메서드 조건, /api/me는 공개 경로 목록, 쿼리가 있는 CSS는 search 조건에서 제외되어 기본 네트워크 처리로 갑니다. 실패한 주문에 성공 화면을 반환하면 거래 상태를 거짓으로 표시하므로 안 됩니다. 오프라인에서는 공개 안내를 보일 수 있어도 서버가 확인하지 않은 주문을 완료로 선언하지 않습니다.
- 저장소에
my-pwa-public-v0,my-pwa-public-v1,other-app-cache-v3가 있고 v1이 활성화됩니다. 어떤 이름을 삭제하나요?/styles.css가 v0에만 있어도 v1 코드의cache.match()가 그 파일을 찾나요?
풀이
앱 접두사가 맞고 현재 이름과 다른 v0만 삭제합니다. v1과 다른 앱의 캐시는 유지합니다. v1으로 연 캐시의 match()는 v0까지 검색하지 않습니다. 공개 자원 준비와 버전별 조회·삭제를 같은 정책으로 맞추어야 합니다.
- 기존 v1이 탭 두 개를 제어합니다. v2의 install이 성공했고
skipWaiting()·clients.claim()은 호출하지 않았습니다. 배포 직후 두 탭이 자동으로 v2를 쓰나요? install의 addAll이 실패한 경우와 어떻게 다른가요?
풀이
v2는 기존 client가 있는 동안 waiting에 남을 수 있고 두 탭은 v1을 계속 사용합니다. 기존 사용이 끝나 활성화 조건을 충족하면 v2로 전환합니다. 반면 addAll의 거부가 install의 waitUntil에 전달되면 새 설치가 실패하여 v2를 사용할 준비가 끝난 것이 아닙니다. 기존 v1은 유지될 수 있습니다. 서비스 워커 등록 결과만 보지 말고 installing·waiting·active와 현재 페이지의 controller를 각각 확인합니다.