PWA (Porgressive Web APP) 개발 가이드

vite로 구성된 바닐라 프로젝트가 기준이다. 먼저 index.html 파일에 manifest.json에 대한 연결이 필요하다.

<head>
  <link rel="manifest" href="/manifest.json" />
  <link rel="icon" href="/icon_192x192.png" />
</head>

위의 코드를 보면 선택사항이지만 아이콘도 지정했다. 이 아이콘 역시 manifest 내부에서 언급되는데, index.html은 url을 통해 접속했을때 웹브라우저에 표시될 아이콘이고 manifest에서는 바탕화면 등에 App으로 등록될때 표시되는 아이콘이다. mainfest.json 파일은 다음과 같으며 위치는 public 폴더이다.

{
  "name": "Vite PWA 예제",
  "short_name": "VitePWA",
  "description": "간단한 Vite 기반 PWA 예제",
  "start_url": "/",
  "display": "standalone",
  "theme_color": "#ff0000",
  "background_color": "#ff0000",
  "icons": [
    {
      "src": "/icon_192x192.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "any maskable"
    },
    {
      "src": "/icon_512x512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "any maskable"
    }
  ]
}

여기까지만 해도 PWA로써 내가 원하는 아이콘과 제목 등으로 설정된 앱으로 설치할 수 있게 된다. 여기에 더해…. main.js 파일에 대한 내용이다. 크게 3가지 내용이다. DOM 구성, 서비스워커 구성, 앱 설치 UI이다. 먼저 DOM 구성은 다음처럼 했다.

import './style.css'

document.querySelector('#app').innerHTML = /*html*/ `
  

Vite PWA 예제

이것은 사용자 정의 Service Worker를 사용한 PWA입니다.

오프라인에서도 동작하며, 설치 가능합니다!

`;

서비스 워크 구성은 다음처럼 했다.

window.addEventListener('load', () => {
  navigator.serviceWorker.register('/sw.js')
    .then(registration => {
      console.log('Service Worker 등록 성공:', registration.scope);
    })
    .catch(error => {
      console.error('Service Worker 등록 실패:', error);
    });
});

앱 설치 UI에 대한 코드는 다음처럼 했다.

let deferredPrompt;
//const installButton = document.getElementById('installButton');
//const installMessage = document.getElementById('installMessage');

window.addEventListener('beforeinstallprompt', (e) => {
  // e.preventDefault();
  deferredPrompt = e;
  installButton.style.display = 'block';
  installMessage.textContent = '앱을 바탕화면에 설치할 수 있습니다!';
});

installButton.addEventListener('click', async () => {
  if (deferredPrompt) {
    deferredPrompt.prompt();
    const { outcome } = await deferredPrompt.userChoice;
    installMessage.textContent = outcome === 'accepted'
      ? '앱 설치가 시작되었습니다!'
      : '앱 설치가 취소되었습니다.';
    deferredPrompt = null;
    installButton.style.display = 'none';
  }
});

window.addEventListener('appinstalled', () => {
  installMessage.textContent = '앱이 성공적으로 설치되었습니다!';
  installButton.style.display = 'none';
});

서비스워커에 대한 sw.js 파일의 코드는 다음과 같으며 파일 위치는 public이다. 이 서비스워커는 캐쉬에 대한 기능이다.

const CACHE_NAME = 'vite-pwa-cache-v1';
const urlsToCache = [
  '/',
  '/index.html',
  '/style.css',
  '/icon_192x192.png',
  '/icon_512x512.png'
];

// 설치 시 캐싱
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => {
        console.log('캐시 열기 성공');
        return cache.addAll(urlsToCache);
      })
  );
});

// 요청 처리 (CacheFirst 전략)
self.addEventListener('fetch', event => {
  const dest = event.request.destination;
  // if (event.request.destination === 'document') {
    event.respondWith(
      caches.match(event.request)
        .then(cachedResponse => {
          // 캐시에 있으면 캐시된 응답 반환
          if (cachedResponse) {
            console.log(dest + ' (' + event.request.url + ') from cache');
            return cachedResponse;
          }
          // 캐시에 없으면 네트워크 요청
          return fetch(event.request)
            .then(networkResponse => {
              // 네트워크 응답을 캐시에 저장
              console.log(dest + ' (' + event.request.url + ') from fetch');
              return caches.open(CACHE_NAME).then(cache => {
                cache.put(event.request, networkResponse.clone());
                return networkResponse;
              });
            })
            .catch(() => {
              onsole.log(dest + ' (' + event.request.url + ') failed');
              // 네트워크 요청 실패 시 (오프라인 등)
              return new Response('오프라인 상태입니다. 캐시도 없습니다.', {
                status: 503,
                statusText: 'Service Unavailable'
              });
            });
        })
    );
  // }
});

// 요청 처리 (NetworkFirst 전략)
self.addEventListener('_____fetch', event => {
  console.log(event.request.destination);
  // if (event.request.destination === 'document') {
    event.respondWith(
      fetch(event.request)
        .then(response => {
          // 네트워크 응답을 캐시에 저장
          return caches.open(CACHE_NAME).then(cache => {
            console.log(event.request.url + ' from fetch');
            cache.put(event.request, response.clone());
            return response;
          });
        })
        .catch(() => {
          // 네트워크 실패 시 캐시에서 가져오기
          console.log(event.request.url + ' from cache');
          return caches.match(event.request);
        })
    );
  // }
});

// 캐시 정리
self.addEventListener('activate', event => {
  const cacheWhitelist = [CACHE_NAME];
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames.map(cacheName => {
          if (!cacheWhitelist.includes(cacheName)) {
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
  console.log('캐시 정리 성공');
});

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다