Next.js App Router + PWA + Firebase로 알림 구현하기
다른 개발자 블로그 보면 푸시 알림 금방 금방 구현했다고 하던데, 나는 계속 오류가 나고, 도대체 왜 알림이 두 번 뜨는지(아직 해결 못 함 ㅎ), 왜 권한 요청이 제대로 안 되는지 도통 알 수가 없었다. 구글링 해보면 다들 쉽게 잘 하는 것 같은데, 나는 뭐가 그렇게 꼬였는지 한 번 해결하면 또 다른 문제가 터지고... 정말 힘들었다.
Next.js App Router + PWA + Firebase를 조합해서 푸시 알림을 구현한 과정과 그동안 겪은 문제들, 해결 방법을 정리해보자..
❓PWA란
PWA(Progressive Web App)는 웹 애플리케이션을 네이티브 앱처럼 사용할 수 있게 해주는 기술이다. 기본적으로는 웹사이트지만, 오프라인에서도 작동하고 푸시 알림, 홈 화면 추가와 같은 네이티브 앱의 기능을 제공한다.
PWA의 가장 큰 장점은 빠른 로딩 속도와 앱처럼 동작하는 경험을 제공한다는 것!!! 난 PWA를 통해 내 사이트를 앱처럼 보고싶어서 도입하게 되었다 + 덤으로 알림 구현 하고 싶어서 ㅎㅎ.
0. PWA 설정
pnpm add next-pwa
PWA 기능을 활성화하기 위해 service worker와 manifest 파일을 설정해야 한다.
일단 next.config.ts파일에 PWA 설정하기
import withPWA from 'next-pwa'
/** @type {import('next').NextConfig} */
const nextConfig = {}
export default withPWA({
...nextConfig,
dest: 'public',
register: true,
skipWaiting: true,
disable: process.env.NODE_ENV === 'development',
})
https://progressier.com/pwa-manifest-generator
PWA Manifest Generator | Progressier
This free tool allows you create an app manifest
progressier.com
위 사이트에 접속하면 manifest파일을 쉽게 만들 수 있다.
import { MetadataRoute } from 'next'
export default function manifest(): MetadataRoute.Manifest {
return {
theme_color: '#355d39',
background_color: '#ffffff',
icons: [
{
purpose: 'maskable',
sizes: '512x512',
src: '/icons/icon_maskable.png',
type: 'image/png',
},
{
purpose: 'any',
sizes: '512x512',
src: '/icons/icon_rounded.png',
type: 'image/png',
},
],
orientation: 'any',
display: 'standalone',
dir: 'auto',
lang: 'en-KO',
name: 'GrowBit',
short_name: 'GrowBit',
start_url: '/',
id: '/',
description: '습관 기르기 애플리케이션',
}
}
1. firebase 콘솔로 이동해 Messaging 기능 활성화하기
https://console.firebase.google.com/
로그인 - Google 계정
이메일 또는 휴대전화
accounts.google.com
2. 프로젝트 설정에서 아래 웹 푸시 인증서에 있는 키 쌍을 복사하자.
이 키는 나중에 vapid key에 쓰임!
3. firebaseConfig 파일 설정하기
위에서 복사한 vapid key는 .env파일에 넣어주자!! (vercel 환경변수에도 추가해줘야함~~)
import { initializeApp } from 'firebase/app'
import { getAuth } from 'firebase/auth'
import { getFirestore } from 'firebase/firestore'
import { getMessaging, getToken } from 'firebase/messaging'
// Firebase 설정
export const firebaseConfig = {
apiKey: process.env.NEXT_PUBLIC_API_KEY,
authDomain: process.env.NEXT_PUBLIC_AUTH_DOMAIN,
projectId: process.env.NEXT_PUBLIC_PROJECT_ID,
storageBucket: process.env.NEXT_PUBLIC_STORAGE_BUCKET,
messagingSenderId: process.env.NEXT_PUBLIC_MESSAGING_SENDER_ID,
appId: process.env.NEXT_PUBLIC_APP_ID,
measurementId: process.env.NEXT_PUBLIC_MEASUREMENT_ID,
}
// Firebase 초기화
const app = initializeApp(firebaseConfig)
// Firebase 서비스 객체 생성
export const auth = getAuth(app)
export const db = getFirestore(app)
export const messaging =
typeof window !== 'undefined' ? getMessaging(app) : null
// 클라이언트에서 푸시 토큰 가져오기
export const requestForToken = async () => {
if (!messaging) return // 서버 환경에서는 실행하지 않음
try {
const token = await getToken(messaging, {
vapidKey: process.env.NEXT_PUBLIC_VAPID_KEY,
})
if (token) {
console.log('FCM Token:', token)
} else {
console.warn(
'등록된 Firebase 토큰이 없습니다. 권한을 요청해 토큰을 생성하세요.',
)
}
} catch (error) {
console.error('토큰을 가져오는 중 오류가 발생했습니다. ', error)
}
}
4. Firebase Cloud Messaging (FCM) 설정
public폴더안에 바로 firebase-messaging-sw.js 파일을 넣어준다. (자동으로 서비스워커가 등록됨!)
importScripts(
'https://www.gstatic.com/firebasejs/9.20.0/firebase-app-compat.js',
)
importScripts(
'https://www.gstatic.com/firebasejs/9.20.0/firebase-messaging-compat.js',
)
const firebaseConfig = {
apiKey: '',
authDomain: '',
projectId: '',
storageBucket: '',
messagingSenderId: '',
appId: '',
}
firebase.initializeApp(firebaseConfig)
const messaging = firebase.messaging()
messaging.onMessage((payload) => {
const title = payload.notification?.title || '알림'
const body = payload.notification?.body || '새로운 알림이 도착했습니다.'
new Notification(title, {
body: body,
icon: '/icons/icon_maskable.png',
})
})
개발자도구 > 애플리케이션 > 서비스워커에 가보면 등록되어있는 것을 볼 수 있다.
config 설정 값을 환경 변수로 관리하려고 시도했지만....
process.env를 사용하려고 했을 때, 오류가 발생해 결국 Firebase 설정을 코드에 직접 하드코딩했다.
⚠️ import script 오류
firebase-messaging-sw.js:1 Uncaught NetworkError: Failed to execute 'importScripts' on 'WorkerGlobalScope': The script at 'https://www.gstatic.com/firebasejs/9.20.0/firebase-app.js' failed to load. at firebase-messaging-sw.js:1:1
위와 같은 오류가 났었는데 원래 위 코드를 보면 -compat을 원래 안 붙였었는데 아래 사이트를 보니 JS 서비스워커 파일을 계속 사용하려면 뒤에 compat을 붙여야한다고 한다..!
Uncaught DOMException: Failed to execute 'importScripts' on 'WorkerGlobalScope'
i am trying to import scripts from importScripts("https://www.gstatic.com/firebasejs/9.1.0/firebase-app.js"); importScripts("https://www.gstatic.com/firebasejs/9.1.0/firebase-messagi...
stackoverflow.com
5. 클라이언트 측 알림 처리 - FCM 토큰 처리
클라이언트에서는 Firebase Messaging을 통해 알림 권한을 요청하고, 토큰을 받아온다. 이를 통해 실제 푸시 알림을 받을 수 있는 상태로 만든다.
/* eslint-disable @typescript-eslint/no-unused-vars */
'use client'
import './globals.css'
import Header from './_components/Header'
import Footer from './_components/Footer'
import { usePathname } from 'next/navigation'
import { useEffect, useState } from 'react'
import { getToken, onMessage } from 'firebase/messaging'
import { messaging } from '@/app/_libs/firebaseConfig'
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
const pathname = usePathname()
const hideHeaderPages = ['/', '/login', '/signup']
const hideHeader = hideHeaderPages.includes(pathname)
const [notificationPermission, setNotificationPermission] =
useState<string>('default')
const [fcmToken, setFcmToken] = useState<string | null>(null)
useEffect(() => {
if (typeof window === 'undefined' || !messaging) return
const requestPermissionAndGetToken = async () => {
const permission = await Notification.requestPermission()
setNotificationPermission(permission)
if (permission === 'granted' && messaging !== null) {
try {
const token = await getToken(messaging, {
vapidKey: process.env.NEXT_PUBLIC_VAPID_KEY,
})
if (token) {
console.log('FCM 토큰:', token)
setFcmToken(token)
} else {
console.error('등록된 Firebase 토큰이 없습니다.')
}
} catch (error) {
console.error('토큰 가져오기 중 오류 발생:', error)
}
} else {
console.error('알림 권한이 거부되었습니다.')
}
}
requestPermissionAndGetToken()
const requestPermissionAgain = async () => {
const permission = await Notification.requestPermission()
setNotificationPermission(permission)
if (permission === 'granted') {
console.log('알림 권한이 다시 승인되었습니다.')
if (messaging !== null) {
try {
const token = await getToken(messaging, {
vapidKey: process.env.NEXT_PUBLIC_VAPID_KEY,
})
if (token) {
console.log('Firebase 토큰:', token)
setFcmToken(token)
} else {
console.error('등록된 Firebase 토큰이 없습니다.')
}
} catch (error) {
console.error('토큰 가져오기 중 오류 발생:', error)
}
}
} else {
console.error('알림 권한이 여전히 거부되었습니다.')
}
}
return (
<html lang="ko">
<body>
{!hideHeader && <Header />}
{notificationPermission === 'denied' && (
<div>
<p>
알림 권한이 거부되었습니다. 권한을 다시 요청하려면 아래 버튼을
클릭해주세요.
</p>
<button onClick={requestPermissionAgain}>
알림 권한 다시 요청하기
</button>
</div>
)}
{children}
<Footer />
</body>
</html>
)
}
6. 알림 테스트하기
새 캠페인을 만들고 테스트를 해보면 아래와 같이 알림이 뜬다.
⚠️ 알림이 안 뜨는 오류
알고보니 나의 크롬 설정에서 localhost의 알림이 차단되어 있었다.. 허용을 해주면 뜬다. ㅠㅠ
7. PWA 확인하기
주소창 옆에 조그맣게 설치를 할 수 있다.
8. 아이폰에서 알림 확인하기
내 배포사이트를 사파리 or 크롬에 접속해 공유버튼을 누르고 홈 화면에 추가를 해준다.
알림 허용 후, firebase로 알림을 보내면 아래와 같이 알림이 뜬다!!!
그치만... 알림이 두 번 뜬다.. 🚬 이거 끙끙 해결하고있는 중...
'🔥Next 뽀개기' 카테고리의 다른 글
Chart.js 라이브러리로 달성률 시각화 구현하기(feat. Doughnut 차트) (0) | 2024.12.20 |
---|---|
리액트 캘린더와 Firebase로 CRD 구현하기 (2) | 2024.12.16 |
Next.js Layout과 Firebase로 AuthGuard 구현하기(feat. cookie) (0) | 2024.12.01 |
Next.js에 firebase 연동해서 회원가입 구현하기 (0) | 2024.11.29 |
Zod + React Hook Form = 회원가입 뚝-딱! (0) | 2024.11.27 |