예약 포털 (Vue3 + Firebase) - 서비스 오픈까지

38. Vue3 + Firebase 프로젝트 '우리 동네' - PWA 설정 추가

그랜파 개발자 2025. 6. 23. 14:40

PWA 설정 추가

PWA(Progressive Web App)를 설정하는 과정은 웹 앱에 앱처럼 설치 가능한 기능과 오프라인 동작 기능을 추가하는 일입니다. 

 

https://github.com/inetsos/downtown

 

GitHub - inetsos/downtown: 동네 포털 - Vue 3 + Firebase

동네 포털 - Vue 3 + Firebase. Contribute to inetsos/downtown development by creating an account on GitHub.

github.com

 


✅ PWA 설정 방법 (전체 흐름)

  1. manifest.webmanifest 작성
  2. service-worker.js 구현
  3. HTML에 manifest와 meta 태그 추가
  4. 웹 서버 HTTPS 구성 (또는 localhost)
  5. (선택) vite-plugin-pwa 등 도구 사용
  6. 브라우저에서 서비스 워커 등록

🔷 1. manifest.json 설정

웹 앱의 설치 정보를 담는 JSON 파일로, 앱 이름, 아이콘, 시작 URL 등을 지정합니다.

 

📄 public/manifest.webmanifest

{
  "name": "우리 동네",
  "short_name": "우리동네",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#1976D2",
  "description": "우리 동네 지역 기반 상점 정보 서비스",
  "icons": [
    {
      "src": "/pwa-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/pwa-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}

 

매니페스트는 "우리 동네" 앱을 모바일이나 데스크탑에 설치했을 때, 앱처럼 보이도록 만드는 설정이에요.

 

📄 필드 설명

속성명 설명
"name" 앱의 전체 이름 (우리 동네)
홈 화면에 설치될 때 사용자에게 보여져요.
"short_name" 짧은 이름 (우리동네)
공간이 좁은 곳(홈 화면 아이콘 아래 등)에서 사용돼요.
"start_url" 앱 실행 시 시작될 경로. /는 루트 페이지를 의미해요.
"display" "standalone"은 앱을 브라우저 없이 네이티브 앱처럼 보이게 해줘요 (주소창 등 없음).
"background_color" 앱이 로딩될 때의 배경 색상. 로딩 화면 등에서 사용돼요.
"theme_color" 브라우저 UI의 테마 색상. 안드로이드에서는 상태바 색상으로 반영되기도 해요.
"description" 앱에 대한 간단한 설명 – 사용자가 설치 전에 볼 수도 있어요.
"icons" 앱 설치 시 사용될 아이콘 이미지 목록.
각기 해상도에 맞는 이미지들이 지정돼 있어요.

 


🔷 2. service-worker.js 설정

브라우저와 서버 사이에서 동작하는 백그라운드 스크립트입니다. 오프라인 지원, 캐싱, 푸시 알림 등을 담당합니다.

 

📄 public/service-worker.js

const CACHE_NAME = 'pwa-cache-v1';
const OFFLINE_URL = '/offline.html';

// 설치 시 캐시할 리소스
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME).then(cache =>
      cache.addAll([
        '/',
        '/index.html',
        '/offline.html',
        '/pwa-192x192.png',
        '/pwa-512x512.png'
      ])
    )
  );
  self.skipWaiting();
});

// 활성화 시 오래된 캐시 정리
self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys().then(keys =>
      Promise.all(
        keys.map(key => {
          if (key !== CACHE_NAME) {
            return caches.delete(key);
          }
        })
      )
    )
  );
  self.clients.claim();
});

// fetch 요청 처리
self.addEventListener('fetch', event => {
  event.respondWith(
    fetch(event.request).catch(() => caches.match(event.request).then(response =>
      response || caches.match(OFFLINE_URL)
    ))
  );
});

 

🔹 CACHE_NAME과 OFFLINE_URL

  • CACHE_NAME: 현재 캐시의 이름. 나중에 캐시 정리 시 이 이름을 기준으로 오래된 캐시를 제거해요.
  • OFFLINE_URL: 오프라인 상태일 때 보여줄 HTML 파일 경로.

🛠️ install 이벤트

self.addEventListener('install', event => { ... })

  • 서비스 워커가 처음 등록될 때 실행됩니다.
  • caches.open()으로 캐시를 열고 cache.addAll()로 리소스를 미리 저장해둬요.
  • 즉, 앱 로딩에 필요한 핵심 파일들을 오프라인에서도 쓸 수 있도록 미리 캐시하는 거예요.
  • self.skipWaiting()은 기존 서비스 워커가 있어도 기다리지 않고 즉시 새 워커로 교체되게 합니다.

♻️ activate 이벤트

self.addEventListener('activate', event => { ... })

  • 새 서비스 워커가 활성화될 때 실행됩니다.
  • caches.keys()로 저장된 모든 캐시 목록을 가져와서, 현재 CACHE_NAME과 다른 이전 캐시는 삭제해요.
  • 이렇게 하면 오래된 캐시가 쌓이는 걸 방지할 수 있어요.
  • self.clients.claim()은 해당 워커가 모든 클라이언트 탭에 즉시 적용되도록 합니다.

🌐 fetch 이벤트

self.addEventListener('fetch', event => { ... })

  • 사용자가 웹 페이지나 리소스를 요청할 때마다 실행돼요.
  • 우선 fetch()로 네트워크에서 리소스를 가져오려고 시도하고,
  • **만약 실패(예: 오프라인)**하면 → caches.match()로 캐시에서 찾고, → 그래도 없으면 OFFLINE_URL을 보여줘요.

🔷 3. HTML에 PWA 태그 삽입

📄 index.html (또는 Vue/React 앱의 public/index.html)

<!doctype html>
<html lang="ko">
  <head>
    <meta charset="UTF-8" />
    <link rel="manifest" href="/manifest.webmanifest" />
    <meta name="theme-color" content="#1976D2">

    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
     <!-- 카카오 주소 검색 위젯 -->
    <script src="https://t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js"></script>
    <!-- 카카오 로그인 SDK-->
    <script src="https://developers.kakao.com/sdk/js/kakao.js"></script>
    <!-- 네이버 지도 API -->
    <script src="https://oapi.map.naver.com/openapi/v3/maps.js?ncpKeyId=s781heidm9&submodules=geocoder"></script>
    <title>우리 동네 - Vue + Firebase 사이드 프로젝트</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>

🔷 4. HTTPS에서 서비스 워커 등록 (JS)

웹 앱이 로드될 때 브라우저에서 서비스 워커를 등록해야 합니다.

 

📄 main.js 또는 index.js

// src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import 'vuetify/styles'
import vuetify from './plugins/vuetify'
import { createPinia } from 'pinia'
import router from './router'
import '@mdi/font/css/materialdesignicons.css'
import { useAuthStore } from './stores/authStore'

const app = createApp(App)
app.use(vuetify)
app.use(createPinia())
app.use(router)

const authStore = useAuthStore()
authStore.initAuth() // 로그인 상태 복원

app.mount('#app')

// 서비스 워커 등록
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/service-worker.js')
      .then(registration => {
        console.log('Service Worker registered with scope:', registration.scope);
      })
      .catch(error => {
        console.error('Service Worker registration failed:', error);
      });
  });
}

🔷 5. (선택) vite-plugin-pwa 또는 workbox 등 사용

Vite를 사용 중이라면 vite-plugin-pwa를 사용하는 것이 편리합니다.

npm install vite-plugin-pwa

 

📄 vite.config.js

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vuetify from 'vite-plugin-vuetify'
import path from 'path'
import { VitePWA } from 'vite-plugin-pwa' 

export default defineConfig({
  plugins: [
    vue(),
    vuetify({ autoImport: true }),
    VitePWA({ // ✅ PWA 설정 추가
      registerType: 'autoUpdate',
      includeAssets: [
        'favicon.ico',
        'robots.txt',
        'apple-touch-icon.png'
      ],
      manifest: {
        name: 'Downtown Life',
        short_name: 'Downtown',
        description: '우리 동네',
        theme_color: '#1867C0', // Vuetify 기본 테마 색상
        background_color: '#ffffff',
        display: 'standalone',
        start_url: '/',
        icons: [
          {
            src: '/pwa-192x192.png',
            sizes: '192x192',
            type: 'image/png'
          },
          {
            src: '/pwa-512x512.png',
            sizes: '512x512',
            type: 'image/png'
          }
        ]
      }
    })
  ],
  server: {
    proxy: {
      '/naver-api': {
        target: 'https://openapi.naver.com/v1',
        changeOrigin: true,
        rewrite: path => path.replace(/^\/naver-api/, '')
      }
    }
  },
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src'),
    },
  },
  build: {
    chunkSizeWarningLimit: 1500,
    rollupOptions: {
      output: {
        manualChunks(id) {
          if (id.includes('node_modules')) {
            if (id.includes('vuetify')) return 'vuetify'
            if (id.includes('firebase')) return 'firebase'
            if (id.includes('vue-router')) return 'vue-router'
            if (id.includes('vue')) return 'vue'
            return 'vendor'
          }
        }
      }
    }
  }
})

🔷 6. 테스트 및 설치 확인

  1. 브라우저에서 앱을 열고 DevTools > Application > Manifest 탭 확인
  2. Service Worker가 활성화되었는지 확인
  3. 네트워크 탭에서 오프라인으로 설정 후 동작 확인
  4. 주소창에 + 설치 아이콘이 뜨는지 확인

홈 화면 설치
홈 화면 설치 후 바탕화면에 아이콘

 


✅ 결과

PWA 설정이 완료되면 사용자는:

  • 홈 화면에 설치할 수 있고
  • 오프라인에서도 앱을 실행할 수 있으며
  • 나중에는 푸시 알림이나 백그라운드 동기화 등 기능도 확장할 수 있습니다.