본문 바로가기

Progressive Web Apps (PWA) 소개

HTTPS 위에 매니페스트와 서비스 워커를 구성해 설치·오프라인 캐시·푸시 알림을 지원하는 PWA 원리를 익힙니다.

지금까지 프런트엔드와 백엔드의 핵심 기술을 익히고, 데이터베이스, 보안, 성능, 개발 도구까지 두루 다뤘습니다. 이제 기본적인 웹 애플리케이션을 기획하고 배포할 수 있는 기반이 갖춰졌습니다. 마지막 15장 최신 웹 기술 동향에서는 빠르게 바뀌는 웹 기술 가운데 실제로 영향이 큰 흐름을 살펴봅니다.

첫 주제는 PWA(progressive web app)입니다. PWA는 HTML, CSS, 자바스크립트로 만든 웹 애플리케이션이 설치, 오프라인 지원, 푸시 알림처럼 네이티브 앱에 가까운 경험을 주도록 하는 방식입니다. 웹 워커와 서비스 워커에서 배운 서비스 워커가 그 핵심 기술 가운데 하나입니다.


PWA란

PWA는 웹의 장점과 네이티브 앱의 장점을 함께 갖추는 것을 목표로 합니다. 웹은 설치 없이 URL로 바로 접근할 수 있고 빠르게 개발하고 배포할 수 있으며, 네이티브 앱은 오프라인에서도 동작하고 푸시 알림을 받고 홈 화면에 아이콘을 둘 수 있습니다.

PWA를 쓰면 사용자는 앱 스토어에서 찾고 내려받는 과정 없이 웹 사이트를 방문하는 것만으로 앱을 쓰고, 원하면 홈 화면에 추가할 수 있어 처음 쓰는 장벽이 낮아집니다. 미리 저장해 둔 공개 콘텐츠와 복구 규칙이 있는 범위에서는 연결이 끊겨도 쓸 수 있고, 대상 환경이 지원하고 사용자가 허락하면 푸시 알림도 받을 수 있습니다. 다만 설치만 한다고 이런 기능이 저절로 생기지는 않습니다.

웹 코드와 배포 흐름을 여러 환경에서 다시 쓸 수 있지만, 설치 화면이나 푸시, 백그라운드 기능의 지원 범위와 권한은 플랫폼마다 달라서 하나의 코드가 모든 기기에서 똑같이 동작한다고 보장하지는 않습니다. 공개 URL과 검색 엔진이 읽을 수 있는 콘텐츠를 유지하면 검색으로 발견될 수 있지만, PWA라는 이유만으로 검색 순위가 올라가지는 않습니다(Google의 페이지 경험 안내).

PWA 기능은 사용자에게 필요한 것부터 하나씩 선택합니다

PWA CAPABILITIES

PWA 기능은 사용자에게 필요한 것부터 하나씩 선택합니다

설치·오프라인·알림·백그라운드 동기화는 서로 다른 문제를 해결합니다. 대상 기기의 지원과 권한을 확인하고, 쓸 수 없는 경우의 일반 웹 경로를 함께 설계합니다.

필요한기능을기본웹에선택적으로더한다4노드3점선은순서가아닌선택적확장관계다. 설치는매니페스트와지원,오프라인은저장과복구,푸시·백그라운드는지원·권한·서버계약을각각확인한다. 기본웹의핵심작업은실패대체경로이며세기능이서로의성공을보증하지않는다. mobile동일관계.기본 웹의 핵심 작업문제별 기능 선택설치·매니페스트지원·실제 아이콘오프라인 캐시저장·복구 정책푸시·백그라운드지원·권한·서버
필요한기능을기본웹에선택적으로더한다4노드3점선은순서가아닌선택적확장관계다. 설치는매니페스트와지원,오프라인은저장과복구,푸시·백그라운드는지원·권한·서버계약을각각확인한다. 기본웹의핵심작업은실패대체경로이며세기능이서로의성공을보증하지않는다. mobile동일관계.기본 웹의핵심 작업문제별 기능 선택설치·매니페스트지원·실제 아이콘오프라인 캐시저장·복구 정책푸시·백그라운드지원·권한·서버

먼저: 사용자 문제와 지원 환경

반복 방문, 연결 불안정, 시간에 민감한 알림 중 무엇을 해결할지 정합니다. 서비스 워커는 HTTPS 등 secure context가 필요하며 localhost는 개발 예외입니다.

설치 · 반복 진입을 줄이기

매니페스트에 이름·아이콘·시작 URL·표시 방식을 정의합니다. 브라우저와 OS별 설치 조건·설치 UI를 확인합니다.

미지원·설치하지 않은 사용자도 URL로 계속 이용할 수 있게 합니다.

오프라인 · 끊겨도 핵심 작업 유지

서비스 워커가 캐시한 자원과 요청 처리 정책으로 가능한 기능을 제공합니다. 캐시에 없는 데이터까지 자동으로 생기지는 않습니다.

저장소 한도·만료·캐시 갱신을 정하고 연결 복구 후 서버 데이터와 맞춥니다.

푸시 알림 · 필요한 변화 전달

Push API·서비스 워커·서버의 발송 경로와 알림 권한이 필요합니다. 플랫폼별 지원 조건과 사용자 동의를 확인합니다.

권한을 거부하거나 해지해도 앱 안에서 알림을 확인할 수 있게 합니다.

백그라운드 동기화 · 나중에 전송

지원 환경에서 연결 복구 후 미뤄 둔 작업을 재시도합니다. 실행 시점·배터리·브라우저 정책의 제약을 확인합니다.

미지원 환경에서는 사용자가 앱을 다시 열 때 재시도하는 경로를 둡니다.

매니페스트와 서비스 워커를 추가했다는 사실만으로 모든 플랫폼의 설치·오프라인·푸시가 보장되지는 않습니다. 필요한 기능을 각각 검증합니다.

위 그림은 PWA의 기능을 사용자에게 필요한 것부터 하나씩 고르는 방법을 보여 줍니다. 가운데에는 일반 웹으로 핵심 작업을 할 수 있는 기본 화면이 있고, 그 위에 설치, 오프라인 캐시, 푸시와 백그라운드 기능을 필요한 만큼 더합니다. 세 기능은 차례로 거치는 단계가 아니라 서로 다른 문제를 푸는 선택지이며, 한 기능이 성공했다고 다른 기능이 보장되지 않습니다.

먼저 반복 방문, 불안정한 연결, 때를 놓치면 안 되는 알림 가운데 무엇을 해결할지 정합니다. 서비스 워커는 HTTPS 같은 안전한 컨텍스트가 필요하며, localhost나 127.0.0.1 같은 로컬 주소는 개발할 때의 예외입니다. 설치는 매니페스트에 이름, 아이콘, 시작 URL, 표시 방식을 정의해 다시 들어오는 수고를 줄이며, 브라우저와 운영체제마다 설치 조건과 화면이 다릅니다. 오프라인은 서비스 워커가 캐시한 자원과 요청 처리 규칙으로 가능한 기능을 제공하는 것이라, 캐시하지 않은 데이터까지 생기지는 않으며 저장 한도와 만료, 갱신, 연결이 돌아온 뒤 서버와 맞추는 방법을 정해야 합니다.

푸시 알림은 Push API와 서비스 워커, 서버의 발송 경로, 사용자의 알림 권한이 모두 있어야 하고, 백그라운드 동기화는 지원하는 환경에서 연결이 돌아오면 미뤄 둔 작업을 다시 시도하며 실행 시점과 배터리, 브라우저 정책의 제약을 받습니다. 각 기능에는 쓸 수 없을 때의 대체 경로를 둡니다. 설치하지 않은 사용자도 URL로 계속 쓸 수 있게 하고, 알림 권한을 거부해도 앱 안에서 알림을 확인할 수 있게 하며, 백그라운드 동기화가 없다면 앱을 다시 열 때 재시도합니다. 그림 아래의 설명처럼 매니페스트와 서비스 워커를 더했다는 것만으로 모든 플랫폼에서 설치, 오프라인, 푸시가 보장되지 않으므로 기능마다 검증해야 합니다.


PWA의 구성 요소

PWA는 매니페스트로 설치 정보를 선언하고, 서비스 워커로 필요한 오프라인과 백그라운드 기능을 구현하며, HTTPS로 전달합니다. 서비스 워커가 모든 브라우저에서 설치의 필수 조건은 아니므로(MDN의 설치 가능한 PWA 만들기), 브라우저가 제공하는 설치 기능과 직접 구현하는 오프라인 기능을 구별합니다. 또 파일을 서버에 올린 것과, 지금 사용자의 화면이 어느 버전의 응답을 쓰는지를 나눠 생각해야 합니다.

서비스 워커

서비스 워커는 페이지와 따로 실행되는 워커로 DOM을 직접 다루지 않고, 요청이나 푸시 같은 이벤트를 처리하며 캐시를 제어합니다. 항상 실행 중인 서버나 타이머가 아니라 브라우저가 쉬고 있는 워커를 멈출 수 있으므로, 중요한 상태를 메모리의 변수에만 두지 않습니다.

서비스 워커로 네트워크가 없거나 불안정할 때 캐시한 콘텐츠를 보여 주고, 캐시에서 바로 응답해 네트워크를 기다리는 시간을 줄이며, 앱을 닫은 상태에서도 서버가 보낸 푸시 메시지를 알림으로 보여 줄 수 있습니다. 다만 캐시에 없는 요청이나 오래된 데이터, 저장 한도도 고려해야 하므로 모든 페이지가 저절로 빨라지는 것은 아닙니다. 지원하는 환경에서는 미뤄 둔 작업을 저장했다가 다시 시도하는 백그라운드 동기화도 할 수 있는데, 다시 보낼 때 중복되지 않도록 하고 서버가 결과를 확인하도록 설계해야 합니다.

페이지가 서비스 워커를 등록하면 브라우저가 파일을 내려받아 설치하고, 이때 필요한 에셋을 캐시에 미리 저장할 수 있습니다. 활성화된 서비스 워커가 범위 안의 페이지를 제어하게 되면 그 페이지의 요청을 fetch 이벤트로 처리합니다. 처음 등록한 직후의 지금 페이지가 바로 제어되는 것은 아니며, 무엇을 어떻게 캐시할지는 직접 작성한 핸들러가 정합니다.

웹 앱 매니페스트

웹 앱 매니페스트(web app manifest)는 PWA의 이름, 아이콘, 시작 URL, 표시 방식 같은 정보를 정의하는 JSON 파일로, PWA의 신분증 같은 역할을 합니다. 홈 화면에 추가했을 때의 아이콘을 정하고, 이름과 배경색 같은 정보로 브라우저나 운영체제가 시작 화면을 만들게 합니다. 표시 방식(display)은 브라우저 화면 없이 독립된 앱처럼 실행할지(standalone), 최소한의 화면만 둘지(minimal-ui), 일반 웹 페이지처럼 보일지(browser)를 정하고, 시작 URL은 PWA를 실행했을 때 처음 열 주소입니다.

이름과 표시 방식을 선언했다고 모든 브라우저에서 설치 화면이나 시작 화면이 똑같아지는 것은 아닙니다. 매니페스트는 HTML에 <link rel="manifest" href="/manifest.json">으로 연결합니다.

HTTPS

서비스 워커는 요청을 가로챌 수 있는 강한 권한을 가지므로, PWA는 HTTPS로 제공해야 하며 개발할 때의 localhost나 127.0.0.1 같은 로컬 주소만 예외로 HTTP를 허락합니다. HTTPS는 전송 중에 엿보거나 바꾸는 위험을 줄여 주지만, 악성 코드나 잘못된 권한 검사, 개인 정보를 캐시에 저장하는 문제까지 해결하지는 않으므로 애플리케이션 자체의 보안도 필요합니다.

PWA와 네이티브 앱

구분PWA네이티브 앱 (iOS/Android)
설치지원하는 브라우저에서 웹으로 설치, 스토어 배포도 가능주로 스토어로 설치하며 플랫폼과 배포 정책을 따름
접근URL로 바로 접근해 즉시 사용검색하고 설치하는 과정이 필요
오프라인저장과 복구 규칙을 구성하면 지원앱의 로컬 데이터와 복구 규칙에 따라 지원
푸시 알림지원 가능지원 가능
업데이트새 버전이 적용되는 시점은 활성 서비스 워커와 캐시 규칙에 따름스토어를 통해 수동이나 자동으로 업데이트
개발 비용웹 코드를 다시 쓸 수 있지만 기기별 검증이 필요플랫폼별 구현이나 공유 코드 도구를 고르며 범위에 따라 다름
시스템 접근웹 API의 지원과 사용자 권한 범위 안에서 가능운영체제가 허락한 API와 사용자 권한 범위 안에서 가능
발견 경로공개 웹 콘텐츠의 검색과 URL 공유스토어 검색과 연결된 웹 콘텐츠 등

PWA 구현의 간단한 흐름

먼저 매니페스트 파일 manifest.json을 만듭니다. 이 예제는 해당 크기의 실제 /icons/icon-192.png, /icons/icon-512.png와 나머지 공개 파일을 준비했다고 가정합니다. sizes는 이미지 파일의 실제 해상도를 설명하는 값일 뿐 파일을 그 크기로 바꿔 주지 않습니다.

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"
    }
  ]
}

start_url의 .은 매니페스트 파일의 주소를 기준으로 한 상대 경로라서, /manifest.json에 둔 이 예에서는 사이트의 루트에서 앱이 시작됩니다. display의 standalone은 설치한 앱을 주소 표시줄 없는 독립된 창처럼 엽니다.

HTML의 <head>에 매니페스트를 연결합니다. 첫 줄의 rel="manifest" 링크가 브라우저에 매니페스트의 위치를 알려 주고, 나머지 줄은 화면 요소의 색과 iOS용 설정입니다.

<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>

theme-color는 브라우저의 주소 표시줄 같은 화면 요소의 색을 정합니다. apple-로 시작하는 줄은 오래된 iOS의 Safari를 위한 아이콘과 표시 설정이며, 지금의 Safari는 매니페스트도 읽습니다.

서비스 워커 파일 sw.js를 만들고 캐시 규칙을 정합니다.

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))
    ))
  );
});

이 예제는 사이트를 루트에 배포하고 공개된 정적 파일만 다룹니다. 같은 경로가 로그인한 사용자마다 다른 HTML을 돌려준다면 이 목록에 넣으면 안 되며, 주문이나 결제 API, 개인 정보도 캐시 대상에서 뺐습니다. fetch 핸들러는 같은 출처의 GET 요청 가운데 쿼리 문자열이 없고 허용 목록에 있는 경로만 처리하며, 나머지 요청은 return으로 넘겨 브라우저의 기본 네트워크 처리에 맡깁니다.

install의 waitUntil()은 설치 작업이 끝나기를 기다리게 하고, fetch의 respondWith()는 요청에 돌려줄 응답의 Promise를 정합니다. 이 예제는 그 Promise 안에서 cache.put()까지 기다리지만, 저장에 실패해도 받은 응답은 돌려주므로 응답을 받았다고 캐시에 저장되었다고 단정할 수는 없습니다. 캐시에 없고 네트워크도 실패하면, 페이지 이동(navigation) 요청에는 503 안내 응답을, CSS나 자바스크립트 요청에는 네트워크 오류를 돌려줍니다. 스크립트 자리에 HTML 안내문을 보내면 다른 오류를 낳기 때문입니다.

이 예제는 이름으로 연 캐시의 cache.match()로 지금 버전의 캐시만 찾습니다. 여러 캐시를 함께 찾는 caches.match()(MDN의 CacheStorage.match())와는 다른 API입니다. activate에서는 이 앱에만 붙인 접두사로 시작하는 예전 캐시만 지웁니다. 또 Cache API는 HTTP 캐시 헤더를 자동으로 따르지 않으므로, 서버가 Cache-Control: no-store를 보내도 코드의 cache.put()은 막히지 않습니다. 그래서 공개 파일의 허용 목록과 서버의 응답 규칙을 함께 관리해야 합니다.

마지막으로 메인 자바스크립트 파일에서 서비스 워커를 등록하고, HTTPS 환경에 배포합니다. 개발할 때는 localhost나 127.0.0.1 같은 로컬 주소를 써도 됩니다.

script.js
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);
      });
  });
}

빌드 도구나 프레임워크가 설치, 푸시, 오프라인을 모두 자동으로 만들어 주지는 않으므로, 도구가 매니페스트와 서비스 워커를 어떻게 통합하는지와 필요한 플러그인을 확인합니다. 예를 들어 Next.js의 PWA 안내는 매니페스트와 서비스 워커를 각각 구성하는 과정을 설명하며, 이 과정을 Next.js 앱에 적용하는 방법은 PWA 설정에서 이어집니다. PWA는 만든 뒤에도 캐시 갱신과 설치, 오프라인 동작을 실제 기기에서 점검해야 하며, 저장소를 열거나 읽을 때의 오류, 저장소 삭제, 버전 사이의 호환성은 따로 확인합니다.


새 버전은 언제 적용될까

서비스 워커 설치·갱신 생명주기 — 상태 추적

새 버전 설치 성공에도 기존 client가 활성 버전을 사용하면 waiting에 남는다. old/new worker와 client controller 상태의 변화를 따라 activate와 제어를 구분할 가치가 크다.

새 서비스 워커가 설치되어도 기존 탭은 이전 버전을 쓸 수 있습니다

SERVICE WORKER UPDATE

새 서비스 워커가 설치되어도 기존 탭은 이전 버전을 쓸 수 있습니다

아래는 기존 워커를 새 버전으로 갱신하는 기본 흐름입니다. 설치 성공, 대기 종료, 활성화와 페이지 제어를 구분해야 화면 코드와 캐시 자원의 버전이 어긋나는 일을 줄일 수 있습니다.

설치성공과현재요청제어를별도로확인한다6노드6관계는기본업데이트모델이다. 성공적으로설치한새버전은기존활성버전의client사용시waiting으로가고사용종료뒤activate한다. 기존사용이없으면waiting대기를건너활성화가능하다. 마지막노드는제어된client의요청만이며첫등록현재페이지의즉시제어를보증하지않는다. 설치실패/skipWaiting/clients.claim변경은원support에서구별하고mobile같다.등록·새 버전 확인설치 작업 완료기존 활성 버전의client 사용 중?waiting기존 사용 종료 대기activate활성 버전 전환제어된 clientfetch 핸들러있음없음
설치성공과현재요청제어를별도로확인한다6노드6관계는기본업데이트모델이다. 성공적으로설치한새버전은기존활성버전의client사용시waiting으로가고사용종료뒤activate한다. 기존사용이없으면waiting대기를건너활성화가능하다. 마지막노드는제어된client의요청만이며첫등록현재페이지의즉시제어를보증하지않는다. 설치실패/skipWaiting/clients.claim변경은원support에서구별하고mobile같다.등록·새 버전 확인설치 작업 완료기존 활성 버전의client 사용 중?waiting기존 사용 종료 대기activate활성 버전 전환제어된 clientfetch 핸들러있음없음
  1. 등록·새 버전 확인

    브라우저가 sw.js를 가져와 변경을 확인합니다. 기존 활성 워커는 자신이 제어하던 페이지를 계속 처리합니다.

  2. install · 새 자원 준비

    예제는 waitUntil과 cache.addAll로 자원을 준비합니다. 설치 작업이 실패하면 새 워커는 사용되지 않고 기존 버전은 유지될 수 있습니다.

  3. installed · waiting

    기존 워커가 제어하는 페이지가 남아 있으면 새 워커가 기다립니다. 사용자에게 갱신 안내를 보여주는 UI와 대기를 끝내는 API 호출은 별개입니다.

  4. activate · 갱신 적용

    기본적으로 기존 워커의 client가 없어지고 진행 중 작업이 끝나면 활성화합니다. skipWaiting()은 대기를 건너뛰는 별도 정책이므로 구버전 화면·새 자원의 호환성을 먼저 확인합니다.

  5. 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 제어를 위한 별도 호출입니다.
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
기존 clientcontroller: 구버전 / 새 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

새 서비스 워커가 설치되어도 기존 탭은 이전 버전을 쓸 수 있습니다

아래는 기존 워커를 새 버전으로 갱신하는 기본 흐름입니다. 설치 성공, 대기 종료, 활성화와 페이지 제어를 구분해야 화면 코드와 캐시 자원의 버전이 어긋나는 일을 줄일 수 있습니다.

설치성공과현재요청제어를별도로확인한다6노드6관계는기본업데이트모델이다. 성공적으로설치한새버전은기존활성버전의client사용시waiting으로가고사용종료뒤activate한다. 기존사용이없으면waiting대기를건너활성화가능하다. 마지막노드는제어된client의요청만이며첫등록현재페이지의즉시제어를보증하지않는다. 설치실패/skipWaiting/clients.claim변경은원support에서구별하고mobile같다.등록·새 버전 확인설치 작업 완료기존 활성 버전의client 사용 중?waiting기존 사용 종료 대기activate활성 버전 전환제어된 clientfetch 핸들러있음없음
설치성공과현재요청제어를별도로확인한다6노드6관계는기본업데이트모델이다. 성공적으로설치한새버전은기존활성버전의client사용시waiting으로가고사용종료뒤activate한다. 기존사용이없으면waiting대기를건너활성화가능하다. 마지막노드는제어된client의요청만이며첫등록현재페이지의즉시제어를보증하지않는다. 설치실패/skipWaiting/clients.claim변경은원support에서구별하고mobile같다.등록·새 버전 확인설치 작업 완료기존 활성 버전의client 사용 중?waiting기존 사용 종료 대기activate활성 버전 전환제어된 clientfetch 핸들러있음없음
  1. 등록·새 버전 확인

    브라우저가 sw.js를 가져와 변경을 확인합니다. 기존 활성 워커는 자신이 제어하던 페이지를 계속 처리합니다.

  2. install · 새 자원 준비

    예제는 waitUntil과 cache.addAll로 자원을 준비합니다. 설치 작업이 실패하면 새 워커는 사용되지 않고 기존 버전은 유지될 수 있습니다.

  3. installed · waiting

    기존 워커가 제어하는 페이지가 남아 있으면 새 워커가 기다립니다. 사용자에게 갱신 안내를 보여주는 UI와 대기를 끝내는 API 호출은 별개입니다.

  4. activate · 갱신 적용

    기본적으로 기존 워커의 client가 없어지고 진행 중 작업이 끝나면 활성화합니다. skipWaiting()은 대기를 건너뛰는 별도 정책이므로 구버전 화면·새 자원의 호환성을 먼저 확인합니다.

  5. 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 제어를 위한 별도 호출입니다.
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
기존 clientcontroller: 구버전 / 새 worker: 아직 없음기존 worker가 열린 client를 제어한다등록된 worker와 client controller는 다르다
새 worker 대기controller: 구버전 유지 / 새 worker: waiting새 설치가 끝나도 열린 client 때문에 waiting일 수 있다설치 완료가 즉시 takeover라는 뜻은 아니다
활성화와 제어새 worker: activated / client: 정책에 따라 새 제어활성화 조건과 claim/navigation 정책에 따라 새 controller를 얻는다활성화 상태와 현재 client 제어를 각각 확인한다

위 그림은 기존 서비스 워커를 새 버전으로 갱신하는 기본 흐름입니다. 브라우저가 sw.js를 가져와 바뀐 것을 확인하면 새 워커의 설치를 시작하고, 기존의 활성 워커는 자신이 제어하던 페이지를 계속 처리합니다. 예제처럼 waitUntil()과 cache.addAll()로 자원을 준비하는데, 이 작업이 실패하면 새 워커는 쓰이지 않고 기존 버전이 그대로 유지될 수 있습니다.

설치가 끝났을 때 기존 워커가 제어하는 페이지가 남아 있으면, 새 워커는 대기(waiting) 상태로 기다립니다. 남은 페이지가 없으면 바로 활성화로 넘어갑니다. 기존 워커의 페이지가 모두 닫히고 진행 중인 작업이 끝나면 새 워커가 활성화되어, 제어하는 페이지의 fetch 이벤트를 처리합니다. 사용자에게 새 버전을 안내하는 화면을 보여 주는 것과 대기를 끝내는 API를 호출하는 것은 별개의 일입니다.

그림의 설명 카드는 두 가지 예외를 짚습니다. skipWaiting()은 대기를 건너뛰는 별도의 정책이라, 옛 화면이 새 자원을 받아도 문제가 없는지 먼저 확인해야 합니다. 새 워커를 처음 설치한 경우에는 보통 지금 문서가 아니라 다음 탐색부터 제어하며, 이미 열린 페이지를 바로 제어하려면 clients.claim()을 따로 호출합니다. 운영에서는 이 예제에 더해 저장 실패와 저장소 삭제, 개인 API, 버전 사이의 호환성을 규칙으로 정하고 실제 기기에서 검증합니다.

더 깊이: 브라우저는 언제 새 sw.js를 확인할까

새 버전을 배포해도 브라우저가 sw.js를 다시 받아 보지 않으면 갱신은 시작되지 않습니다. web.dev의 서비스 워커 생명주기 설명에 따르면 브라우저가 갱신을 확인하는 때는 정해져 있습니다. 서비스 워커의 범위 안에 있는 페이지로 이동할 때, 그리고 push나 sync 같은 이벤트가 일어났는데 지난 24시간 안에 확인한 적이 없을 때입니다. 받은 스크립트가 지금 것과 한 바이트라도 다르면 새 버전으로 보고 설치를 시작합니다.

이 확인에서 HTTP 캐시는 기본으로 쓰이지 않습니다. Chrome 68부터 브라우저는 sw.js의 갱신을 확인할 때 캐시 헤더를 무시하고 서버에 직접 묻습니다. 그래서 sw.js에 실수로 긴 max-age를 주어도 갱신이 막히지는 않습니다. 다만 importScripts()로 불러오는 스크립트는 캐시 헤더를 따르므로, 이 파일들을 고쳤다면 캐시 설정을 함께 확인하고, 등록할 때 updateViaCache 옵션으로 이 동작을 바꿀 수도 있습니다.

등록하는 주소 자체를 sw-v2.js처럼 버전마다 바꾸는 방법은 권하지 않습니다. 같은 문서에 따르면 register()는 주소가 다를 때만 다른 워커로 다루는데, 옛 서비스 워커가 캐시에서 옛 index.html을 내주고 그 HTML이 옛 주소를 계속 등록하면, 새 주소의 워커는 한 번도 받지 못할 수 있기 때문입니다. 스크립트의 주소는 그대로 두고 내용만 바꿔 배포하는 것이 기본입니다.


정리

  • PWA는 웹 기술로 만든 앱에 설치, 오프라인, 푸시 알림 같은 기능을 필요한 만큼 더하는 방식이며, 기능마다 지원과 권한을 확인하고 쓸 수 없을 때의 경로를 둡니다.
  • 매니페스트는 이름, 아이콘, 시작 URL, 표시 방식을 정의하고, 서비스 워커는 요청과 푸시를 처리하며 캐시를 제어하고, HTTPS가 이 둘을 안전하게 전달합니다.
  • 서비스 워커는 브라우저가 멈출 수 있는 이벤트 처리기이며, 캐시할 대상은 공개된 자원의 허용 목록으로 정하고 개인 API와 거래 요청은 빼야 합니다.
  • Cache API는 HTTP 캐시 헤더를 따르지 않으므로, 무엇을 저장할지는 코드와 서버 규칙으로 함께 관리합니다.
  • 새 서비스 워커는 설치에 성공해도 기존 페이지가 남아 있으면 대기하며, 등록, 설치, 활성화, 지금 페이지의 제어를 각각 확인해야 합니다.
  • 오프라인에서 공개 안내는 보여 줄 수 있어도, 서버가 확인하지 않은 주문이나 결제를 캐시된 화면으로 성공 처리하지 않습니다.

연습 문제

1. 서비스 워커가 처리하는 요청

같은 출처에서 GET /styles.css, POST /api/orders, GET /api/me, GET /styles.css?v=2를 요청합니다. 예제의 허용 목록과 조건에서 서비스 워커가 응답을 정하는 요청은 무엇일까요? 주문 API가 실패했을 때 캐시해 둔 성공 화면으로 대신해도 될까요?

해설 보기

쿼리 문자열이 없는 GET /styles.css만 처리합니다. POST는 메서드 조건에서, /api/me는 공개 경로 목록에서, 쿼리가 있는 CSS는 쿼리 조건에서 빠져 기본 네트워크 처리로 갑니다.

실패한 주문에 성공 화면을 보여 주면 거래 상태를 거짓으로 알리게 되므로 안 됩니다. 오프라인에서는 공개된 안내를 보여 줄 수 있어도, 서버가 확인하지 않은 주문을 완료로 표시하지 않습니다.

2. 예전 캐시 정리

저장소에 my-pwa-public-v0, my-pwa-public-v1, other-app-cache-v3가 있고 v1이 활성화됩니다. 어떤 캐시가 지워질까요? /styles.css가 v0에만 있다면 v1 코드의 cache.match()가 그 파일을 찾을까요?

해설 보기

앱의 접두사와 맞고 지금 이름과 다른 v0만 지웁니다. v1과 다른 앱의 캐시는 그대로 둡니다.

v1으로 연 캐시의 match()는 v0까지 찾지 않으므로 그 파일을 찾지 못하고 네트워크로 요청합니다. 그래서 새 버전의 설치 단계에서 필요한 공개 자원을 모두 준비하고, 버전마다 조회와 삭제를 같은 규칙으로 맞춰야 합니다.

3. 설치 성공과 적용 시점

기존 v1이 탭 두 개를 제어하는 상태에서 v2의 설치가 성공했고, skipWaiting()과 clients.claim()은 호출하지 않았습니다. 배포 직후 두 탭이 자동으로 v2를 쓸까요? 설치 단계의 addAll()이 실패한 경우와는 어떻게 다를까요?

해설 보기

쓰지 않습니다. v2는 기존 페이지가 있는 동안 대기 상태로 남고, 두 탭은 v1을 계속 씁니다. 두 탭이 모두 닫히는 등 활성화 조건이 갖춰지면 v2로 바뀝니다.

반면 addAll()이 실패해 그 거부가 waitUntil()로 전달되면 v2의 설치 자체가 실패한 것이라, v2는 쓸 준비가 끝나지 않았고 기존 v1이 유지될 수 있습니다. 서비스 워커의 등록 결과만 보지 말고 installing, waiting, active 상태와 지금 페이지의 controller를 각각 확인합니다.

다음 글 WebAssembly 기초에서는 C, C++, Rust 같은 언어로 만든 코드를 브라우저에서 빠르게 실행하는 WebAssembly의 개념과 자바스크립트와 함께 쓰는 방법을 살펴봅니다.