Vite 프로젝트에 PWA와 FCM 푸시 알림 적용하기

2025. 1. 17. 01:03·Frontend

0. PWA와 FCM을 쓰려는 이유

친구들과 공모전을 나가기로 했었다. 공모전에 프로젝트를 제출하려면 몇 가지 조건을 충족해야했는데, iOS와 Android에서 모두 작동해야하며, 푸시 알림이 구현되어야 한다는 조건이었다. React와 문법이 비슷한 React Native를 빠르게 학습한 후 이 언어를 활용하여 개발을 진행하는 방법이 먼저 떠올랐다. 그 다음에 떠오른 것이 바로 PWA(Progressive Web App)였다.

 

PWA 기술을 활용하면 웹사이트지만 네이티브 앱처럼 설치하여 사용할 수 있고, 푸시 알림, 오프라인 작동 등 앱과 유사한 기능들도 구현할 수 있다. 푸시 알림은 FCM(Firebase Cloud Messaging)을 통하여 구현하기로 했다. Firebase을 독학하여 CRUD를 구현해본 적은 있지만 푸시 알림은 구현해본 적이 없었기 때문에 이번 기회에 FCM을 사용해보고 싶었다.


1. ‘vite-pwa’를 통하여 PWA를 구현하자

PWA를 어떻게 구현할 수 있을지 찾아보니, 어떤 스택을 활용하여 프로젝트를 빌드하느냐에 따라 PWA 구현 방법도 달라졌다. Next.js를 활용하면 ‘next-pwa’ 라이브러리를, Vite를 활용하면 ‘vite-pwa’ 라이브러리를 활용하면 되었다.

 

무엇을 사용할까 고민하다가, ‘지금 내가 구현하려는 프로젝트가 SSR(Server Side Rendering)이 꼭 필요한가?’ 하는 의문이 들었다. 사람들이 Next.js를 사용하는 이유가 SSR을 구현하기 위함이 전부가 아님을 알고 있다. 하지만 내가 구현하려는 프로젝트는 SEO가 중요하지 않고, 또 백엔드 팀원들이 존재한다. 그래서 Next.js를 사용하지 않고 가벼운 vite를 활용하기로 했다.

 

FCM을 구현하기 전 먼저 PWA부터 구현했었는데, vite-pwa는 service worker를 자동으로 생성해주기 때문에 별도의 service worker 파일을 만들어줄 필요가 없다고해서 따로 생성하지 않았다. (물론 뒤에 FCM을 구현할 때에는 service worker 파일을 생성했다.) 그 대신 vite.config.ts에서 VitePWA 설정을 추가해주었다.

 

 

vite.config.ts

plugins: [
    react({
      jsxImportSource: '@emotion/react',
    }),
    VitePWA({
      registerType: 'autoUpdate',
      devOptions: {
        enabled: true,
        type: 'module'
      },
      manifest: {
        name: '앱 이름',
        short_name: '앱 이름',
        description: '앱 설정',
        theme_color: '#ffffff',
        icons: [
          {
            src: '/favicon/favicon-16x16.png',
            sizes: '16x16',
            type: 'image/png'
          },
          {
            src: '/favicon/favicon-32x32.png',
            sizes: '32x32',
            type: 'image/png'
          },
          
          (...)         
          
          
        ]
      }
    })
  ],
})

확인 결과 sw.js를 따로 사용하지 않아도 PWA가 구현이 되었다! 어떤 환경에서는 앱 설치가 되지 않는 문제가 발생했지만, 계속 새로고침을 반복하다보니 다른 환경에서도 앱 설치가 가능했다. 이렇게 일단 PWA를 구현하는데는 성공했고, 이제 FCM을 구현해보기로 하였다.


2. FCM을 구현해보자

구글링을 해보면 어떻게 FCM을 구현할 수 있는지 상세히 작성되어 있는 글들을 확인할 수 있다. 또한 공식 문서에도 설명이 자세히 되어 있으니 이 글을 참고해서 FCM 설정을 시작했다.

 

공식 문서 링크: https://firebase.google.com/docs/cloud-messaging/js/client?hl=ko&source=post_page-----368d90974b7--------------------------------

 

자바스크립트 Firebase 클라우드 메시징 클라이언트 앱 설정  |  Firebase Cloud Messaging

2024년 데모 데이에서, Firebase를 사용하여 AI 기반 앱을 빌드하고 실행하는 방법에 관한 데모를 시청하세요. 지금 시청하기 의견 보내기 자바스크립트 Firebase 클라우드 메시징 클라이언트 앱 설정

firebase.google.com

 

2-1. Service Worker 설정

firebase-messaging-sw.js를 생성해서 아래 코드와 같이 service worker를 설정했다.

 

 

firebase-messaging-sw.js

// Service Worker에서 사용할 Workbox 매니페스트
self.__WB_MANIFEST

importScripts('<https://www.gstatic.com/firebasejs/9.0.0/firebase-app-compat.js>');
importScripts('<https://www.gstatic.com/firebasejs/9.0.0/firebase-messaging-compat.js>');

// Firebase 초기화
firebase.initializeApp({
  apiKey: "",
  authDomain: "",
  projectId: "",
  storageBucket: "",
  messagingSenderId: "",
  appId: "",
  measurementId: ""
});

const messaging = firebase.messaging();

// Background 메시지 처리
messaging.onBackgroundMessage((payload) => {
  console.log('Background 메시지 수신:', payload);

  const notificationTitle = payload.notification.title;
  const notificationOptions = {
    body: payload.notification.body,
    icon: '/logo.png'
  };

  self.registration.showNotification(notificationTitle, notificationOptions);
});

service worker 파일을 public 디렉토리에 추가한 이유

AI가 service worker 파일은 public 디렉토리에 추가하라고 해서 public 디렉토리에 추가했다.

 

이유를 물어보니,

  1. 루트 경로 접근성: 서비스 워커는 보안상의 이유로 반드시 웹사이트의 루트 경로에서 접근 가능해야 합니다. public 폴더의 내용은 빌드 시 프로젝트의 루트 디렉토리로 복사되어 서비스됩니다.
  2. 스코프 제한: 서비스 워커는 자신이 위치한 디렉토리와 그 하위 디렉토리에만 영향을 미칠 수 있습니다. 루트에 위치함으로써 전체 애플리케이션에 대한 푸시 알림을 처리할 수 있습니다.
  3. 직접 서빙: public 폴더의 파일들은 번들링이나 트랜스파일링 없이 있는 그대로 서빙됩니다. 서비스 워커는 클라이언트 사이드 JavaScript와는 별도로 동작해야 하므로, 번들러의 처리 없이 그대로 서빙되어야 합니다.

라고 한다.

 

 

public 폴더에 있으면 문제점이 하나 생기는데, 바로 .env 파일의 환경변수를 사용하지 못한다는 점이다.

빌드 스크립트를 작성해서 환경변수를 주입하는 방법도 사용할 수 있겠으나, 검색해보니 firebase의 키 값들은 노출되도 별 문제가 없다는 답변이 많아 위 코드와 같이 초기화를 진행했다.

 

Workbox 매니페스트

먼저 Workbox 매니페스트를 선언했다. Workbox 매니페스트는 생소한 개념이라 어떤 용도로 사용되는지 잘 몰랐다. 찾아본 결과, 웹사이트를 빌드(build)할 때, self.__WB_MANIFEST는 실제 파일 목록으로 바뀐다고 한다.이 매니페스트가 캐시할 파일 목록들을 저장해놓기 때문에, 인터넷이 끊겨도 웹사이트가 작동할 수 있게 해주고, 파일들을 미리 저장해두어 웹사이트 로딩 속도를 빠르게 해준다고 한다.

 

그리고 밑에는 백그라운드 메시지 처리 코드들이 있다. FCM 알림은 백그라운드(Background) 알림과 포그라운드(Foreground) 알림으로 나뉘는데, 포그라운드 알림은 앱이 켜져 있을 때 오는 알림이고, 백그라운드 알림은 앱이 꺼져 있을 때 오는 알림이다. service worker는 앱이 꺼져 있을 때에도 실행될 수 있는 유일한 컨텍스트이므로, 백그라운드 알림 관련 코드는 service worker 파일에 추가하는 것이 맞아보인다.


2-2. firebase.ts 생성

firebase.ts를 생성해줘서 firebase 초기화 및 설정을 진행했다.

잠깐만, 위의 service worker 파일에서 firebase 초기화를 이미 진행해준거 아닌가, 왜 또 진행하지 하는 의문을 가질 수 있는데 찾아본 결과,

  1. Service Worker는 완전히 독립된 JavaScript 실행 환경
  2. 메인 앱의 JavaScript 컨텍스트와 변수나 상태를 공유할 수 없음

때문에 firebase 초기화를 각각 해줘야 한다고 한다.

 

 

firebase.ts

import { initializeApp } from 'firebase/app';
import { getMessaging, getToken, onMessage } from 'firebase/messaging';

const firebaseConfig = {
  apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
  authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
  projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
  storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
  appId: import.meta.env.VITE_FIREBASE_APP_ID,
  measurementId: import.meta.env.VITE_FIREBASE_MEASUREMENT_ID
};

// Firebase 초기화
const app = initializeApp(firebaseConfig);
console.log('Firebase initialized:', app);

const messaging = getMessaging(app);
console.log('Messaging initialized:', messaging);

// FCM 토큰 가져오기
export const requestForToken = async () => {
  try {
    const currentToken = await getToken(messaging, {
      vapidKey: import.meta.env.VITE_FIREBASE_VAPID_KEY,
    });
    
    if (currentToken) {
      console.log('FCM 토큰 가져오기 성공!')

      // 테스트 시 사용
      // console.log('FCM 토큰 값: ', currentToken);

      // 나중에 토큰을 백엔드 서버로 전송하는 로직 추가
      return currentToken;
    } else {
      console.log('토큰을 가져올 수 없습니다.');
    }
  } catch (err) {
    console.log('토큰 발급 중 에러 발생:', err);
  }
};

// foreground 메시지 수신
export const onMessageListener = () =>
  new Promise((resolve) => {
    onMessage(messaging, (payload) => {
      console.log('Foreground message received:', payload);
      resolve(payload);
    });
  });

FCM 토큰을 가져오는 requestForToken 메서드와 포그라운드 메시지를 수신하는 메서드도 firebase.ts에서 생성해주었다.

주목할 점은 FCM 토큰을 가져올 때는 vapidKey를 담아서 getToken으로 보내줘야 한다는 것이다.

 

또한 firebase.ts는 src 파일에 있으므로 환경변수들을 활용해서 키들을 공개하지 않으려 했다. (물론 service worker에 다 공개되 있으므로 크게 의미는 없지만..)


2-3. App.tsx에서 코드 추가

최상단 파일인 App.tsx에서 useEffect를 활용하여 앱이 실행될 때마다 알림 권한을 요청하고, FCM 토큰을 요청해서 FCM 토큰을 받아오게 했다. 코드에서 확인할 수 있듯이 permission이 ‘granted’여야 FCM 토큰을 받을 수 있다.

그리고 firebase.ts의 onMessageListener 메서드를 활용하여 포그라운드 메시지가 작동하게 코드를 작성하였다.

 

App.tsx

import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { useEffect } from 'react';
import Onboarding from './pages/Onboarding';
import MyPage from './pages/MyPage';
import Register from './pages/Register';
import { requestForToken, onMessageListener } from './firebase/firebase';

function App() {
  useEffect(() => {
    // 알림 권한 요청
    if ('Notification' in window) {
      console.log('Current notification permission:', Notification.permission);
      Notification.requestPermission().then((permission) => {
        console.log('알림 허용이 되어있나요 :', permission);
        if (permission === 'granted') {
          
          // FCM 토큰 요청
          requestForToken().then((token) => {
            if (token) {
              console.log('requestForToken 성공!');
            }
          });
        }
        if (permission === 'denied') {
          console.log('알림이 거부되었어요');
        }
      });
    } else {
      console.log('알림이 되지 않아요!');
    }

    // Foreground 메시지 수신 처리
    onMessageListener()
      .then((payload : any) => {
        // Foreground에서 알림 표시
        if (Notification.permission === 'granted') {
          new Notification(payload.notification.title, {
            body: payload.notification.body,
            icon: '/logo.png'
          });
        }
        if (Notification.permission === 'denied') {
          console.log('알림이 거부되었어요!'); 
                }
      })
      .catch((err) => console.log('메시지 수신 에러:', err));
  }, []);

  return (
    <BrowserRouter>
      <Routes>
        <Route path='/onboarding' element={<Onboarding />} />
        <Route path='/myPage' element={<MyPage />} />
        <Route path='/register' element={<Register />} />
        <Route path='/' element={<Navigate to='/onboarding' replace />} />
      </Routes>
    </BrowserRouter>
  );
}

export default App;


3. FCM이 잘 작동하는지 테스트해보자

firebase 홈페이지에서 알림을 보내서 FCM이 잘 작동하는지 테스트할 수 있다.

 

테스트 알림 메세지 문서 링크:

https://firebase.google.com/docs/cloud-messaging/js/first-message?hl=ko

위 링크로 접속해서 테스트 알림 메시지를 보내는 방법이 나오는데, 아래와 같이 따라하면 된다.

 

 

FCM 등록 토큰을 넣어야되는데, firebase.ts에서 토큰을 가져오는 메서드를 만들 때 임시로 토큰을 알려줄 수 있도록 코드를 수정하면 된다.

 

// FCM 토큰 가져오기
export const requestForToken = async () => {
  try {
    const currentToken = await getToken(messaging, {
      vapidKey: import.meta.env.VITE_FIREBASE_VAPID_KEY,
    });
    
    if (currentToken) {
      console.log('FCM 토큰 가져오기 성공!')

      // 테스트 시 사용
      // console.log('FCM 토큰 값: ', currentToken);

      // 나중에 토큰을 백엔드 서버로 전송하는 로직 추가
      return currentToken;
    } else {
      console.log('토큰을 가져올 수 없습니다.');
    }
  } catch (err) {
    console.log('토큰 발급 중 에러 발생:', err);
  }
};

 

주석 친 ‘테스트 시 사용’ 밑의 코드를 활성화 시키고. 나온 토큰 값을 복사해서 웹 페이지 작성란에 넣자.

그리고 알려준대로 차근차근 진행했다.

 

그리고 콘솔을 확인해보면 알림이 잘 왔음을 확인할 수 있다!

 

이번에는 포그라운드 메시지가 잘 도착하는지 테스트 해봤는데, 다음에는 백그라운드 메시지가 잘 도착했는지 테스트해보려 한다.

 

아직 FCM과 관련해서 백엔드와 연동을 진행하지는 않았기에 그 때에도 FCM이 잘 작동할지는 확신할 수 없지만, 계속 디벨롭해가면서 푸시 알림이 잘 작동되는 PWA를 완성해보려 한다.

저작자표시 비영리 변경금지 (새창열림)

'Frontend' 카테고리의 다른 글

[Next.js 강의 정리] Next.js와 Page Router에 관하여  (0) 2025.05.25
CORS 에러는 프론트가 해결해야하는가? (어떤 트윗을 보고)  (6) 2025.02.19
[코딩테스트] Javascript로 코딩테스트 보기 전 봐야 할 핵심로직 정리  (0) 2025.01.11
'react-datepicker'에서 날짜 상태를 관리할 때 겪었던 시행착오  (0) 2024.08.03
프로젝트 팀원 코드를 보고 컴포넌트 만드는 법 배우기(typescript + clsx)  (1) 2024.07.21
'Frontend' 카테고리의 다른 글
  • [Next.js 강의 정리] Next.js와 Page Router에 관하여
  • CORS 에러는 프론트가 해결해야하는가? (어떤 트윗을 보고)
  • [코딩테스트] Javascript로 코딩테스트 보기 전 봐야 할 핵심로직 정리
  • 'react-datepicker'에서 날짜 상태를 관리할 때 겪었던 시행착오
퀵차분
퀵차분
Web Developer 🥐
  • 퀵차분
    QC's Devlog
    퀵차분
  • 전체
    오늘
    어제
    • 분류 전체보기 (178)
      • Frontend (31)
      • Fedify (4)
      • Study (42)
        • NestJS (2)
        • Node.js (3)
        • Modern JS Deep Dive (13)
        • SQL (1)
        • Network (1)
        • 프롬프트 엔지니어링 (4)
        • 인공지능 (9)
        • 시스템프로그래밍 (11)
        • 선형대수학 (1)
      • Intern (4)
      • KUIT (21)
      • Algorithm (48)
        • Baekjoon(C++) (26)
        • Programmers(JavaScript) (22)
      • 우아한테크코스(프리코스) (4)
      • Project (10)
        • crohasang_page (3)
        • PROlog (4)
        • Nomadcoder (2)
      • 생각 (4)
      • Event (7)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    프로그래머스
    react
    시스템프로그래밍
    음악추천
    프로그래머스 자바스크립트
    프론트엔드
    알고리즘
    인공지능
    타입스크립트
    리액트
    HTML
    티스토리챌린지
    자바스크립트
    오블완
    javascript
    KUIT
    백준
    typescript
    fedify
    next.js
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
퀵차분
Vite 프로젝트에 PWA와 FCM 푸시 알림 적용하기
상단으로

티스토리툴바