본문 바로가기
🔥Next 뽀개기

Next.js App Router + PWA + Firebase로 알림 구현하기

by 짱돌보리 2025. 1. 26.
728x90

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 workermanifest 파일을 설정해야 한다.

일단 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을 붙여야한다고 한다..!

https://stackoverflow.com/questions/70868923/uncaught-domexception-failed-to-execute-importscripts-on-workerglobalscope

 

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로 알림을 보내면 아래와 같이 알림이 뜬다!!!

그치만... 알림이 두 번 뜬다.. 🚬 이거 끙끙 해결하고있는 중...