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('캐시 정리 성공'); });