본문으로 건너뛰기
안동민 개발노트 아이콘

안동민 개발노트

본문 시작
16장 : 고급 주제

PWA 설정

웹 앱 매니페스트와 서비스 워커를 설정하고 캐시·오프라인·설치 가능 여부를 운영 빌드에서 검증합니다.

현대 웹 애플리케이션은 단순 정보 제공을 넘어 네이티브 앱에 가까운 경험을 제공하는 방향으로 진화하고 있습니다.

프로그레시브 웹 앱(Progressive Web App, PWA)은 이 목표를 달성하기 위한 웹 기술 집합입니다.

앱 스토어 설치 없이도 모바일 앱처럼 동작하도록 웹의 접근성과 기능을 함께 끌어올립니다.

Next.js는 PWA 구현에 필요한 기능을 지원하고, 라이브러리와 설정을 통해 비교적 쉽게 PWA로 전환할 수 있습니다.

이 절에서는 App Router의 매니페스트 규칙과 직접 작성한 서비스 워커를 바탕으로 설치, 오프라인 지원, 업데이트, 푸시 알림의 경계를 익힙니다.

먼저 PWA 설정을 manifest, service worker, 캐시 전략, 운영 검증이라는 네 축으로 나눠 큰 그림을 잡습니다.


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, 표시 모드를 타입 안전하게 정의하는 특별 파일입니다.

src/app/manifest.ts
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에 두고 캐시할 범위를 최소한으로 시작합니다.

다음 예제는 오프라인 안내 화면만 미리 저장하고, 문서 이동이 네트워크에서 실패했을 때 그 화면으로 대체합니다.

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을 캐시할 수 있습니다.

public/offline.html
<!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, 결제 결과는 이 예제처럼 무조건 캐싱하면 안 됩니다.

브라우저에 등록

등록 로직은 작은 클라이언트 컴포넌트로 분리하고 운영 환경에서만 실행합니다.

src/app/[locale]/pwa-register.tsx
'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를 새로 만들지 않습니다.

src/app/[locale]/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 기능 구현 및 테스트

구현 단계로 들어가기 전에 서비스 워커가 설치되고 활성화된 뒤 요청을 어떻게 제어하는지 먼저 확인합니다.

오프라인 지원 (Caching Strategy)

서비스 워커는 작성한 fetch 처리 범위만 제어합니다.

캐시 대상과 전략은 요청의 성격에 따라 명시적으로 정해야 합니다.

  • 정적 자산: 파일명이 해시로 바뀌는 자산은 cache-first 전략을 검토할 수 있습니다.
  • 공개 문서: 최신성과 오프라인 접근을 함께 원하면 stale-while-revalidate를 검토합니다.
  • 사용자별 데이터: 인증·권한·개인정보 응답은 공유 캐시에 넣지 않고 네트워크 실패 UI를 따로 설계합니다.

캐시 전략은 요청 종류마다 달라야 합니다.

정적 파일, 문서 페이지, 사용자별 API를 같은 방식으로 캐싱하면 빠른 화면 대신 오래된 화면이 남을 수 있습니다.

오프라인 테스트

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 키 설정이 필요합니다.

푸시 알림 구현은 백엔드 로직이 필요하므로, 이 가이드의 범위를 넘어섭니다. 하지만 PWA 설정의 중요한 부분으로 알아두는 것이 좋습니다.

홈 화면 설치 (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을 순서대로 확인해야 합니다.

PWA는 웹 애플리케이션에 오프라인 캐싱, 홈 화면 설치, 푸시 알림 같은 기능을 추가하는 방식입니다.

Next.js PWA를 운영할 때는 캐시 전략, 업데이트 처리, 알림 권한, 브라우저 지원 범위를 함께 확인해야 합니다.

출시 전에는 기능이 켜졌는지보다 실패 상황에서 사용자가 다음 행동을 이해할 수 있는지까지 점검합니다.

마지막으로 manifest, service worker, runtime caching, 설치 UX의 책임을 분리해 전체 PWA 구성을 다시 정리합니다.