회사 서비스에 채팅 기능이 생기면서 PWA로 웹 푸시 알림을 구현해야 하는 과제가 있었습니다.
제가 세웠던 목표는 아래 두 가지 였습니다.
CNA로 빈 프로젝트를 하나 생성했습니다.
PWA가 사용자의 데스크톱 또는 휴대기기에 설치되었을 때 어떻게 동작해야 하는지 브라우저에 알려주는 JSON 파일인 manifest.json
파일이 필요합니다.
public 폴더 하위에 manifest.json 파일을 생성해도 되지만, Next.js의 파일 컨벤션에 따라 app 디렉토리 하위에 manifest.ts
를 생성했습니다.
// app/manifest.ts
import { MetadataRoute } from "next";
export default function manifest(): MetadataRoute.Manifest {
return {
name: "Push",
description: "Push web app with Next.js",
display: "standalone",
start_url: "/",
theme_color: "#FFFFFF",
background_color: "#FFFFFF",
icons: [
{
src: "icons/icon-96.png",
type: "image/png",
sizes: "96x96",
},
{
src: "icons/icon.svg",
type: "image/svg+xml",
sizes: "any",
},
{
src: "icons/icon-maskable-640.png",
type: "image/png",
sizes: "640x640",
purpose: "maskable",
},
],
screenshots: [
{
src: "
bg.png",
sizes: "1280x720",
type: "image/png",
},
],
};
}
short_name
name
description
icons
start_url
theme_color
background_color
display
아이콘 파일들은 다음과 같이 public/icons/
폴더에 넣어주었습니다.
서비스 워커를 사용하면 네트워크로 이동하지 않고도
- 에셋을 제공하고,
- 사용자에게 알림을 보내고,
- PWA 아이콘에 배지를 추가하고,
- 백그라운드에서 콘텐츠를 업데이트하고,
- 전체 PWA가 오프라인으로 작동하도록 할 수 있습니다.
// pubic/service-worker.js
function registerServiceWorker() {
if (typeof window !== "undefined") {
if ("serviceWorker" in navigator) {
navigator.serviceWorker
.register("/firebase-messaging-sw.js")
.then((registration) => {
console.log("Service Worker Registered");
console.dir(registration);
});
}
}
}
registerServiceWorker();
firebase-messaging-sw.js
를 serviceWorker에 register 해줍니다.
firebase console로 가서 프로젝트를 생성해봅시다
//pubic/firebase-messaging-sw.js
importScripts("https://www.gstatic.com/firebasejs/8.10.1/firebase-app.js");
importScripts(
"https://www.gstatic.com/firebasejs/8.10.1/firebase-messaging.js"
);
const firebaseConfig = {
apiKey: "xxxxxxxxxx",
authDomain: "xxxxxxxxxx.firebaseapp.com",
databaseURL: "https://xxxxxxxxxx.firebaseio.com",
projectId: "xxxxxxxxxx",
storageBucket: "xxxxxxxxxx.appspot.com",
messagingSenderId: "xxxxxxxxxx",
appId: "xxxxxxxxxx"
};
firebase.initializeApp(firebaseConfig);
const messaging = firebase.messaging();
messaging.onBackgroundMessage((payload) => {
const title = payload.notification.title + " (onBackgroundMessage)";
const notificationOptions = {
body: payload.notification.body,
icon: "https://avatars.githubusercontent.com/sasha1107",
};
self.registration.showNotification(title, notificationOptions);
});
firebase api key는 일반적인 api key와 다르게 env에 넣지 않고 관리해도 안전하기 때문에 그냥 파일에 넣어도 무방합니다. 다만, 배포 환경에 따라 다른 config를 사용하고자 할 때는 register하는 url에 queryString으로 전달하여 config를 환경별로 설정 할 수 있습니다. 이 프로젝트에서는 env를 사용하지 않고 했지만, 회사에서 환경 별로 설정하면서 엄청난 삽질을 했습니다. 😅
// app/layout.tsx
import type { Metadata } from "next";
import Script from "next/script";
import { Inter } from "next/font/google";
import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>
{children}
<Script src="/service-worker.js" />
</body>
</html>
);
}
layout.tsx
파일에서 <Script>
태그를 사용하여 public/service-worker.js
파일을 포함시켜줍니다.
PWA installale 상태가 되었습니다. 개발자 도구에서 확인해봅시당.
모바일에서 브라우저로 해당 페이지에 접속하여 아래 공유 버튼을 선택 | 홈 화면에 추가 선택 |
---|---|
![]() | ![]() |
홈화면에 아름답게 추가 되었습니다.
firebase console에서 그대로 가져왔습니다.
// src/firebase/index.ts
// Import the functions you need from the SDKs you need
import { initializeApp } from "firebase/app";
// TODO: Add SDKs for Firebase products that you want to use
// https://firebase.google.com/docs/web/setup#available-libraries
// Your web app's Firebase configuration
// For Firebase JS SDK v7.20.0 and later, measurementId is optional
const firebaseConfig = {
apiKey: "xxxxxxxxxx",
authDomain: "xxxxxxxxxx.firebaseapp.com",
databaseURL: "https://xxxxxxxxxx.firebaseio.com",
projectId: "xxxxxxxxxx",
storageBucket: "xxxxxxxxxx.appspot.com",
messagingSenderId: "xxxxxxxxxx",
appId: "xxxxxxxxxx"
};
// Initialize Firebase
export const firebaseApp = initializeApp(firebaseConfig);
Notification API를 사용하여 브라우저의 알림 권한을 받아야 합니다.
granted
상태가 되어야 푸시 알림을 받을 수 있습니다.
const requestPermission = async () => {
if (!("Notification" in window)) {
console.warn("This browser does not support notifications.");
return;
}
const permission = Notification.permission;
if (permission === "granted") {
return;
} else {
Notification.requestPermission().then((permission) => {
console.log("permission", permission);
});
return;
}
};
알림 권한 요청을 하면 브라우저의 알림 허용 팝업이 뜹니다.
// firebase-messaging-sw.js
messaging.onBackgroundMessage((payload) => {
const title = payload.notification.title + " (onBackgroundMessage)";
const notificationOptions = {
body: payload.notification.body,
icon: "https://avatars.githubusercontent.com/sasha1107",
};
self.registration.showNotification(title, notificationOptions);
});
아까 1.3.2에서 작성했던 firebase-messaging-sw.js
파일의 일부인데, messaging.onBackgroundMessage
메서드를 사용하여 웹 앱이 백그라운드 상태일 때 알림을 수신할 수 있도록 했습니다.
// app/page.tsx
"use client";
import { useEffect } from "react";
import {
getMessaging,
onMessage,
getToken,
isSupported,
} from "firebase/messaging";
import { firebaseApp } from "@/firebase";
const messaging = async () => {
try {
const isSupportedBrowser = await isSupported();
if (isSupportedBrowser) {
return getMessaging(firebaseApp);
}
return null;
} catch (err) {
console.error(err);
return null;
}
};
export default function Home() {
const permission = Notification.permission;
useEffect(() => {
const onMessageListener = async () => {
const messagingResolve = await messaging();
if (messagingResolve) {
onMessage(messagingResolve, (payload) => {
if (!("Notification" in window)) {
return;
}
const permission = Notification.permission;
const title = payload.notification?.title + " foreground";
const redirectUrl = "/";
const body = payload.notification?.body;
if (permission === "granted") {
console.log("payload", payload);
if (payload.data) {
const notification = new Notification(title, {
body,
icon: "/icons/icon-96.png",
});
notification.onclick = () => {
window.open(redirectUrl, "_blank")?.focus();
};
}
}
});
}
};
onMessageListener();
}, []);
return (
<main className="flex min-h-screen flex-col items-center justify-center p-24">
<div className="flex flex-col gap-10">
<div className="text-4xl">🔔{permission}🔔</div>
<button className="border rounded py-2" onClick={requestPermission}>
푸시 알림 켜기
</button>
</div>
</main>
);
}
회사에서는 Sendbird(채팅 서비스)에 User Device Token을 등록하여 채팅이 올 때 fcm을 통해 알림이 오도록 했지만, 여기에서는 Sendbird와 같은 외부 라이브러리를 거치지 않기에 firebase console에서 테스트 push를 보내보았습니다..
PC
모바일
샘플 코드를 공유합니다.
오 잘보고갑니다.