Next.js에서의 Firebase Cloud Messaging 삽질 기록

cansweep·2022년 8월 16일
8
post-thumbnail

드디어 팀 프로젝트가 절반쯤 왔고 프로젝트의 주 기능인 푸시 알림을 구현하게되었다.
궁금해약 프로젝트 진행 중 reCAPTCHA, botd 등 수많은 삽질을 했지만 이번 삽질은 정말 힘들었다. 누군가 나랑 같은 문제를 겪고 있다면 도움이 되었으면 좋겠다.

PWA에서 Push Notification 구현하는 방법

PWA에서 Push Notification을 구현하는 것은 링크를 참고했다.

Push API와 Notification API를 사용하게 되는데 Notification API는 알림을 보내는 작업을 하고 Push API는 서버에서 푸시된 메세지를 웹 프로그램에 수신할 수 있는 기능을 제공한다.
즉, Push API를 통해 사용자가 앱을 사용하지 않아도 사용자의 기기에 알림을 보낼 수 있다.

Notification

Notification API는 정말 간단하다.

const Test: NextPage = () => {
 const sendMessage = () => {
    const title = "궁금해 약";
    const body = "약을 복용할 시간입니다!";
    const icon = "/images/logo.png";
    const options = { body, icon };

    const notif = new Notification(title, options);
  };

  const btnClickHandler = async () => {
    const result = await Notification.requestPermission();
    if (result === "granted") {
      sendMessage();
    }
  };

  return <button onClick={btnClickHandler}>알림 보내기</button>;
};

Notification.requestPermission()으로 알림을 표시하기 위한 권한을 요청한다.
이때 사용자가 요청을 승인하면 result는 granted라는 값을 가지고 이때 message를 보내는 것이다.

message는 title과 option으로 이루어진다.
option은 또 body와 icon으로 이루어지는데 message의 상세 내용과 이미지를 설정할 수 있다.

버튼을 클릭하면 위와 같은 알림을 받을 수 있다.

Push

mdn에서도 얘기하듯이 Push는 Notification보다 어렵다.

사용자가 앱을 사용하지 않을 때에도 알림을 보내고 앱으로 돌아오도록 하기 위해 Service Worker를 등록할 필요가 있다.

처음 mdn 문서를 봤을 때 Service Worker를 어떻게 만들어야 하는지 감도 안 오고 막막했는데 사실 Service Worker는 next-pwa에서 만들어준다. (이걸 몰라서 한참 헤맸었다...)

공식문서의 Features 첫번째 줄에 이런 내용이 있다.

0️⃣ Zero config for registering and generating service worker

service worker는 개발 환경에서 만들어지지 않고 빌드 시 만들어진다.

next-pwa는 pwa 세팅 시 필요하니 pwa 세팅이 끝났다면 service worker는 신경 쓸 필요가 없고 이제 남은 것은 푸시 메시징 서버이다.

푸시 메시징 서버를 구현하기 위해서는 firebase를 사용하거나 자체 커스텀 서버를 사용하는데 나는 firebase를 사용하기로 했다.

⚠️ 문제 1. Service messaging is not available

FCM을 사용하기 위해서는 firebase 프로젝트를 만들고 웹 앱을 추가한 뒤 firebase SDK를 추가하기 위한 config 값을 받아와야 한다.

문제 설명

먼저, firebase는 9.9.2 버전을 사용했다.
그리고 _app.tsx에서 firebase를 초기화하기 위해 config의 내용들을 환경변수에 저장한 뒤 사용했다.

import { initializeApp } from "firebase/app";
import { getMessaging } from "firebase/messaging";

const firebaseConfig = {
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
  storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
  measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID,
};

function MyApp({ Component, pageProps }: AppProps) {
	const app = initializeApp(firebaseConfig);
	const messaging = getMessaging(app);
  	// ...
}

그랬더니 fcm을 초기화하는 과정에서 에러가 발생했다.

Service messaging is not available

이 에러메세지를 구글링한 결과 해결 방법은 여러가지였다.

1. 앱을 껐다 켜기 => 안됨.
2. 컴퓨터를 껐다 켜기 => 안됨.
3. 인터넷 연결 상태 확인하기 => 연결 잘 되어있음.

모두 내 상황과는 관련이 없는 해결방법이었다.

혹시나 getMessaging 함수 호출 전 firebase 초기화가 덜 진행되어 app이 없는 상태로 호출하고 있는 건 아닐까 싶어 분기처리까지 해줬지만 이것도 해결 방법이 아니었다.

_app.tsx가 아닌 알림 기능을 직접 사용할 page 파일에서 초기화를 해도, page 파일이 아닌 firebase 초기화 관련 파일을 따로 만들어도 같은 문제가 발생했다.

해결

그러던 중 stack overflow에서 해결 방법을 찾았다.

혹시나 싶어 firebase의 버전을 8.2.1로 재설치한 뒤 v8의 문법을 따라 _app.tsx의 내용을 수정했다.

import firebase from "firebase/app";
import "firebase/messaging";

const firebaseConfig = {
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
  storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
  measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID,
};

function MyApp({ Component, pageProps }: AppProps) {
	firebase.initializeApp(firebaseConfig);
	const messaging = firebase.messaging();
  	// ...
}

이렇게 작성하면 또 에러가 나지만 이전과는 다른 에러다.

FirebaseError: Firebase App named '[DEFAULT]' already exists (app/duplicate-app).

app이 이미 있는데 동일한 이름으로 초기화를 하려고 해서 일어나는 에러다.
따라서 없을 때만 초기화할 수 있도록 분기처리를 해준다.

if (!firebase.apps.length) {
    firebase.initializeApp(firebaseConfig);
}

그럼 드디어 정상적으로 FCM SDK가 초기화된다.

⚠️ 문제 2. Messaging: We are unable to register the default service worker

FCM SDK 초기화 후 메세지를 수신하기 위해서는 firebase-messaging-sw.js를 구현해야 한다.
firebase-messaging-sw.js와 관련된 내용은 링크를 참조하면 된다.

// Give the service worker access to Firebase Messaging.
// Note that you can only use Firebase Messaging here. Other Firebase libraries
// are not available in the service worker.
importScripts('https://www.gstatic.com/firebasejs/8.10.0/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/8.10.0/firebase-messaging.js');

// Initialize the Firebase app in the service worker by passing in
// your app's Firebase config object.
// https://firebase.google.com/docs/web/setup#config-object
firebase.initializeApp({
  apiKey: 'api-key',
  authDomain: 'project-id.firebaseapp.com',
  databaseURL: 'https://project-id.firebaseio.com',
  projectId: 'project-id',
  storageBucket: 'project-id.appspot.com',
  messagingSenderId: 'sender-id',
  appId: 'app-id',
  measurementId: 'G-measurement-id',
});

// Retrieve an instance of Firebase Messaging so that it can handle background
// messages.
const messaging = firebase.messaging();

아까 SDK 초기화 시 은근슬쩍 messaging을 선언하는 로직이 빠진 이유가 여기있다.
_app.tsx에서는 firebase만 초기화해주고 messaging에 대한 내용은 여기서 선언해도 된다.

firebase-messaging-sw.js는 프로젝트의 루트에 있어야하기 때문에 public 폴더에 파일을 생성해 주었다.

그리고 메세지를 보낼 때 필요한 token을 발급받기 위해 utils 폴더에 firebase.ts를 만들고 token을 발급받기 위한 함수를 구현했다.

import firebase from "firebase/app";

export async function getToken() {
  const messaging = firebase.messaging();
  const token = await messaging.getToken({
    vapidKey: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_VAPID_KEY,
  });

  return token;
}

그리고 이 함수는 컴포넌트가 마운트되었을 때 호출한다.

 useEffect(() => {
    async function getMessageToken() {
      const token = await getToken();
      console.log(token);
    }
    getMessageToken();
  }, []);

여기까지 하면 로컬에서는 잘 돌아간다. 하지만 실제로 동작하는지 확인을 하기 위해서 배포를 진행했고 역시나 또 에러를 만났다.

Messaging: We are unable to register the default service worker

2-1. .env의 내용이 적용되지 않는다.

firebase-messaging-sw.js를 작성하며 firebase를 초기화할 때 환경변수로 설정한 값들을 사용했다.
그리고 배포 전에는 아무 문제가 없으나 배포 후에는 환경 변수의 값이 적용되지 않았다.

따라서 firebase-messaging-sw.js에서 환경변수를 사용하지 않고 필요한 값들만 넣어주었다.

firebase.initializeApp({
  apiKey: 'api-key',
  projectId: 'project-id',
  messagingSenderId: 'sender-id',
  appId: 'app-id',
});

2-2. onMessage 함수는 firebase-messaging-sw.js에서 호출되지 않는다.

웹이 백그라운드 상태일 때 메세지를 수신하기 위해서는 firebase-messaging-sw.js에 관련 처리가 필요하다.

messaging.onBackgroundMessage((payload) => {
  console.log('[firebase-messaging-sw.js] Received background message ', payload);
  // Customize notification here
  const notificationTitle = 'Background Message Title';
  const notificationOptions = {
    body: 'Background Message body.',
    icon: '/firebase-logo.png'
  };

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

이와 비슷하게 포그라운드 상태일 때도 onMessage()라는 함수를 호출하게 되는데 나는 이 둘을 모두 firebase-messaging-sw.js에 선언해 사용하고자 했다.

하지만 onMessage()는 페이지에서 직접 알림을 수신하기 위한 함수라 service worker에서 사용하는 것이 아니다.
따라서 onMessage() 함수가 firebase-messaging-sw.js 내에 있으면 에러가 발생한다.

드디어 성공...


FCM을 사용해 이 푸시 알림을 보기까지 3-4일의 시간이 걸렸다.
이 중 3일이라는 시간은 거의 문제 1.을 해결하느라 보냈다.
이전에 firebase v9을 사용한 적이 있던 터라 버전 문제라고는 생각을 못 했는데 구글은 역시 친절한 듯 불친절하다.

관련 링크

Firebase Cloud Messaging 문서

profile
하고 싶은 건 다 해보자! 를 달고 사는 프론트엔드 개발자입니다.

1개의 댓글

comment-user-thumbnail
2023년 6월 8일

PWA로 만든 후 백그라운드 상태에서 service worker로 수신한 메세지를 Next.js 페이지로 전달하는 방법이 있나요?
redux store에 넣으면 좋겠는데.. 방법을 못 찾겠네요.

답글 달기