푸시알림은 아래와 같은 과정을 거쳐 일어난다.
이렇게 구독한 클라이언트들에게 필요할 때마다 푸시 알림을 보낸다.
보통 앱을 실행할 때, "알림을 허용하시겠습니까?"하는 창을 본 적이 많을 것이다.
앱을 처음 실행할 때 그런 창을 띄워주면 좋겠지만, 네이티브 앱이 아닌 PWA에서는 ios 환경에서 페이지를 방문할 때, 알림 허용 요청을 자동으로 실행할 수 없기 때문에 버튼을 클릭하는 등의 "사용자 상호작용이 있을 때에만" 알림 허용 요청을 할 수 있다.
아래의 코드는 특정 버튼을 클릭할 때 사용자에게 알림 허용을 선택하는 창을 띄우고, 알림이 허용된 상태라면 푸시 알림을 구독하는 함수이다.
// 버튼 클릭 시 알림 허용 요청
const handleBtnClick = async () => {
const permission = await Notification.requestPermission();
// 알림이 허용되어있지 않은 경우
if (permission !== 'granted') {
if (permission === 'denied') {
alert('알림이 차단되어 있습니다. 설정에서 직접 변경해야 합니다.');
}
return;
}
// 푸시 알림을 구독 (아래쪽에 코드 있음)
subscribePush(userId);
};
service-worker.js에 push 이벤트에 대한 event listener를 설정해준다.
// service-worker.js
// push 이벤트가 발생할 때, 푸시알림을 보여줌
self.addEventListener('push', (event) => {
const data = event.data.json();
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.body,
icon: '/icon512_maskable.png',
data: { link: data.link }, // 클릭 이벤트에서 사용할 데이터 저장
})
);
});
// 푸시알림을 클릭하면 알림에 설정된 페이지로 이동
self.addEventListener('notificationclick', (event) => {
const url = event.notification.data?.link;
if (url) {
clients.openWindow(url);
}
event.notification.close();
});
// 기존에 설정된 service worker가 있다면 그 service worker들이 제거될 때까지 기다려야 하기 때문에
// 그 과정을 생략하기 위한 코드임
self.addEventListener('install', () => {
self.skipWaiting();
});
self.addEventListener('activate', (event) => {
event.waitUntil(
(async () => {
await clients.claim(); // 기존 열린 탭에서도 새로운 서비스 워커를 즉시 적용
console.log('새로운 서비스 워커가 활성화되었습니다!');
})()
);
});
openssl rand -base64 32 | tr '+/' '-_' | tr -d '='
우선, 위의 코드를 실행해서 Base64 URL Safe 형식의 문자열을 만들어 .env 파일에 NEXT_PUBLIC_VAPID_PUBLIC_KEY라는 이름으로 저장해준다.
// subscribePush.ts
export const subscribePush = async (userId: string) => {
// 푸시알림을 지원하지 않는 브라우저일 경우 구독하지 않고 종료
if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
console.warn('푸시 알림을 지원하지 않는 브라우저입니다.');
alert('푸시 알림을 지원하지 않는 브라우저입니다.');
return;
}
// 미리 설정해 둔 service worker를 브라우저에 등록
const registerServiceWorker = async () => {
return await navigator.serviceWorker.register('/service-worker.js');
};
const getPushSubscription = async () => {
const serviceWorker = await registerServiceWorker();
// 브라우저에 등록한 service worker를 통해 푸시알림에 대한 구독 정보를 설정
const subscription = await serviceWorker.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY,
});
return subscription;
};
try {
const subscription = await getPushSubscription();
// 위에서 생성된 구독 정보를 서버로 전송
const response = await fetch(`/api/user/subscription`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(subscription),
});
if (!response.ok) {
throw new Error(`서버 오류: ${response.status}`);
}
} catch (err) {
console.error('구독 정보 전송 중 오류 발생:', err);
}
};
간단하게 Supabase를 사용해서 구독 정보를 저장하자.
// supabseClient.ts
import { createClient } from '@supabase/supabase-js';
const supabaseUrl = process.env.SUPABASE_URL!;
const supabaseKey = process.env.SUPABASE_KEY!;
export const supabase = createClient(supabaseUrl, supabaseKey);
여기서는 Supabase를 사용해 구독 정보를 저장하고 있지만, prisma같은 DB ORM, firebase 등을 사용할 수 있다.
// /app/api/user/subscription/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { supabase } from '@/utils/supabase/supabaseClient';
export async function POST(req: NextRequest) {
const subscription = await req.json();
const { data, error } = await supabase
.from('subscription')
.insert(subscription);
if (error) {
return NextResponse.json({ message: '구독 실패', error: error.message }, { status: 400 });
}
return NextResponse.json({ message: '구독 완료' }, { status: 201 });
}
별도의 서버나 vercel cron기능 등을 사용해서 아래의 함수를 실행하면 알림을 허용한 모든 사용자에게 푸시알림이 발송된다.
// sendNotifications.ts
import webpush from 'web-push';
import { supabase } from './supabaseClient';
webpush.setVapidDetails(
'mailto:메일주소', // 앱 관리자의 메일 주소
process.env.VAPID_PUBLIC_KEY!, // 위에서 설정한 NEXT_PUBLIC_VAPID_PUBLIC_KEY와 동일
// (클라이언트에서 보내는 VAPID_PUBLIC_KEY와 서버에 있는 VAPID_PUBLIC_KEY가 동일한지 확인)
process.env.VAPID_PRIVATE_KEY! // VAPID_PUBLIC_KEY처럼 Base64 URL Safe 형태로 새로 생성
);
export async function sendNotifications() {
const { data: subscriptions, error } = await supabase
.from('subscription')
.select('*');
if (error) {
console.error('Error fetching subscriptions:', error.message);
return;
}
const notification = {
title: '테스트 푸시 알림',
body: '테스트 성공!',
link: '/'
};
for (const subscription of subscriptions) {
webpush.sendNotification(subscription, JSON.stringify(notification));
}
}
클라이언트에서 이 함수를 호출하거나 또는 별도의 서버, vercel cron기능 등에서 위의 함수를 실행하면 알림을 허용한 모든 사용자에게 푸시알림이 발송된다.