sitelink1 https://www.omnibuscode.com/board/board_mobile/58290 
sitelink2  
sitelink3  

sitelink1 의 학습을 기반으로 프로젝트에 최종 반영한 샘플 코드이다.

학습하면서 참고한 코드들을 짬뽕했기 때문에 계속해서 수정 보완 중이다. (동작은 하는데 부족한 부분을 보완하고 있다)

게다가 FCM 의 API 가 계속해서 변경되고 있어서 그에 따른 degradation 도 대응하고 있다.

 

[service-worker.js]

self.oninstall = (e) => {

    console.log('[ServiceWorker] installed');

    self.skipWaiting();

    /**

    이벤트내에서 처리해야할 로직이 있고 작업이 끝날때까지 기다려야 한다면 다음과 같이 작성

    

    //'Ripple'이라는 이름으로 캐시를 열고 캐시에 필요한 리소스들을 추가할때까지 기다리게 한다

    e.waitUntil(

        caches.open('Ripple')

            .then(cache => {

                return cache.addAll([

                    '/',

                    '/index.html',

                    '/styles.css',

                    '/script.js',

                    '/images/logo.png'

                    //계속해서 필요한 리소스를 캐싱

                ]);

            })

            .then(() => {

                console.log('[ServiceWorker] Resources cached');

            })

    );    

    */

}

 

self.onactivate = (e) => {

    console.log('[ServiceWorker] activated');

    e.waitUntil(self.clients.claim()); // 제어권획득

    

    /**

    새로운 버전의 서비스 워커가 활성화된 경우 발생하는 이벤트이다.

    새로운 서비스 워커가 실행되었기 때문에 다음과 같이 캐시를 검사하여 초기화를 할 수 있다.

    

    e.waitUntil(caches.keys()

        .then(eacheNames => {

            return Promise.all(cacheNames.map

                (cacheName => {

                    if (cacheName !== 'Ripple') {

                        //다른 버전의 캐시를 정리

                        return caches.delete(cacheName);

                    }

                }));

        }).then(() => {

            console.log('[ServiceWorker] delete cache~');

        }));

    */

}

 

let reqDataCount = 0

self.onfetch = (e) => {

    console.log('[ServiceWorker] fetch request url', e.request.url);

    e.respondWith(fetch(e.request)); // 기존요청 그대로 보내기

 

    /**

    필요에 따라 아래와 같이 서버의 리소스 요청을 가로채어 작업을 수행하고 

    캐시를 활용하여 리소스를 반환하거나 네트워크 요청을 수정할 수 있다.

    

    // data.json 에 대한 요청을 가로채기

    if (e.request.url.endsWith('/data.json')) {

        reqDataCount++;

        e.respondWith(new Response(JSON.stringify({

                    reqDataCount

                }), {

                headers: {

                    'Content-Type': 'application/json'

                }

            }))

 

        return;

    }

    // 다음의 코드를 넣으면 무조건 캐시에서 리소스를 반환하도록 강제

    e.respondWith(

        caches.match(e.request)

            .then((response) => {

                if(response) { //캐시를 찾으면 캐시를 반환하고 종료

                    return response;

                } else { //캐시가 없으면 기존 요청 그대로 반환

                    return fetch(e.request);

                }

            })

            .then(error => {

                console.error('[ServiceWorker] error!!', error);

            })

    );    

    **/

 

}

 

//Push Message 수신 이벤트

self.onpush = (e) => {

    console.log('[ServiceWorker] 푸시알림 수신: ', e);

    console.log('[ServiceWorker] 푸시알림 내용: ', e.data.json().notification);

 

    //Push 정보 조회

    const noti = e.data.json().notification;

    var title = noti.title || '알림';

    var body = noti.body || 'no message';

    var icon = noti.icon || '/Images/icon.png'; //512x512

    var options = {

        body: body,

        icon: icon

    };

    

    //Notification 출력

    e.waitUntil(self.registration.showNotification(title, options));

}

 

//사용자가 Notification을 클릭했을 때

self.onotificationclick = (e) => {

    console.log('[ServiceWorker] 푸시알림 클릭: ', e);

    e.notification.close();

    e.waitUntil(clients.matchAll({

            type: "window"

        }).then(function (clientList) {    

            //실행된 브라우저가 있으면 Focus

            for (var i = 0; i < clientList.length; i++) {

                var client = clientList[i];

                if (client.url == '/' && 'focus' in client)

                    return client.focus();

            }

            //실행된 브라우저가 없으면 Open

            if (clients.openWindow)

                return clients.openWindow('https://localhost:44337/');

        }));

};

 

[index.html]

<!DOCTYPE html>

<html lang="ko" data-bs-theme="auto">

<head>

    ...

 

    <link rel="manifest" href="manifest.json">

</head>

<body>

    <script type="module">

        // Import the functions you need from the SDKs you need 

        import { initializeApp } from "https://www.gstatic.com/firebasejs/10.6.0/firebase-app.js";

        import { getMessaging, getToken, onMessage } from "https://www.gstatic.com/firebasejs/10.6.0/firebase-messaging.js";

 

        // https://firebase.google.com/docs/web/setup#available-libraries 

        // Your web app's Firebase configuration 

        const firebaseConfig = {

            apiKey: "...",

            authDomain: "...",

            projectId: "...",

            storageBucket: "...",

            messagingSenderId: "...",

            appId: "...",

            measurementId: "..."

        };

 

        // Initialize Firebase

        const firebaseApp = initializeApp(firebaseConfig);

        const messaging = getMessaging(firebaseApp);

 

        var isServiceWorkerSupported = 'serviceWorker' in navigator;

        if (isServiceWorkerSupported)

        {

            navigator.serviceWorker.register('service-worker.js', { scope: './'})

                .then(reg => {

                    console.log('[ServiceWorker] 등록 성공: ', reg.scope);

                    Notification.requestPermission().then((permission) => {

                        if (permission === 'granted') {

                            console.log('Permission granted');

 

                            getToken(messaging, {

                                vapidKey:

                                    '...',

                                serviceWorkerRegistration: reg

                            }).then((token) => {

                                if (token) {

                                    onMessage(messaging, (payload) => {

                                        console.log('Messaging ', payload);

                                    });

                                    console.log('[FCM Token] token is : ', token);

                                    sendTokenToServer(token);

                                } else {

                                    console.log('[FCM Token] token is NULL', token);

                                }

                            }).catch((err) => {

                                console.log(err);

                            });

 

                        } else {

                            console.log('Permission denied');

                        }

                    });

                })

                .catch(function(err)

                {

                    console.log('[ServiceWorker] 등록 실패: ', err);

                });

        }

    </script>

 

    ...

</body>

</html>

 

[JAR]

아래의 jar들을 프로젝트에 수동 추가 (의존성 있는 라이브러리들이 다수 있음)

google-auth-library-oauth2-http-1.20.0.jar

firebase-admin-9.2.0.jar


  OR

 
build.gradle 에 다음을 추가

implementation 'com.google.firebase:firebase-admin:9.2.0'

implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '4.2.2'

 

[TestFcmNotification.java]

import java.io.FileInputStream;

import java.io.IOException;

 

import com.google.auth.oauth2.GoogleCredentials;

import com.google.firebase.FirebaseApp;

import com.google.firebase.FirebaseOptions;

import com.google.firebase.messaging.FirebaseMessaging;

import com.google.firebase.messaging.FirebaseMessagingException;

import com.google.firebase.messaging.Message;

import com.google.firebase.messaging.Notification;

 

public class TestFcmNotification {

 

    public static String json_file = "...\\res\\firebase-adminsdk.json";

    

    // 메인 메서드

    public static void main(String[] args) {

        

        String token = null;

        

        // Firebase 앱을 초기화

        initializeFirebaseApp();

        

        // notification을 발송

        //pc firefox

        token = "fdkIKreAEZ8i-XDD-WbAhq:APA91bG06N7F9gj1_...";

        sendNotification(token, "FCM Firefox Message", "This is an FCM Firefox Message");

        

        //pc edge

        token = "c0Cae9s0m-FVSotbzr9mnF:APA91bGPEgB1V4e...";

        sendNotification(token, "FCM Edge Message", "This is an FCM Edge Message");

        

        //pc whale

        token = "cBQhRiyHaKBoiOWhr9Wf8l:APA91bGmZOobkU...";

        sendNotification(token, "FCM Whale Message", "This is an FCM Whale Message");

        

        //pc chrome

        token = "dx8Mftb9Avpznp2IgDQfzC:APA91bHRjnrZQMG...";

        sendNotification(token, "FCM Chrome Message", "This is an FCM Chrome Message");

        

        //mobile chrome

        token = "dKXYcP8pcnvRg7qLsR8lbG:APA91bEA1s-G70n...";

        sendNotification(token, "FCM Mobile Chrome Message", "This is an FCM Mobile Chrome Message");

        

        //mobile default

        token = "esR-hyLHS2cMKNRC9MJw68:APA91bEdJqUZQ...";

        sendNotification(token, "FCM Mobile Default Message", "This is an FCM Mobile Default Message");

        

        //mobile whale

        token = "dB4913luC307KcP8JNbqe2:APA91bH1jTrazYp...";

        sendNotification(token, "FCM Mobile Whale Message", "This is an FCM Mobile Whale Message");

        

        //mobile edge

        token = "d4aB0Xw4ZKVv3bdgMymnlf:APA91bFTsq7d...";

        sendNotification(token, "FCM Mobile Edge Message", "This is an FCM Mobile Edge Message");

    }

 

    // Firebase 앱을 초기화하는 메서드

    public static void initializeFirebaseApp() {

        try {

            // google-services.json 파일의 경로를 지정

            FileInputStream serviceAccount = new FileInputStream(json_file);

            // Firebase 옵션을 설정

            FirebaseOptions options = FirebaseOptions.builder()

                    .setCredentials(GoogleCredentials.fromStream(serviceAccount)).build();

            // Firebase 앱을 초기화

            FirebaseApp.initializeApp(options);

        } catch (IOException e) {

            // 예외 처리

            e.printStackTrace();

        }

    }

 

    // notification을 발송하는 메서드

    public static void sendNotification(String token, String title, String body) {

        try {

            // notification 객체를 생성

            Notification notification = Notification.builder().setTitle(title).setBody(body).build();

            // 메시지 객체를 생성

            Message message = Message.builder().setNotification(notification).setToken(token).build();

            // FirebaseMessaging 객체를 가져옴

            FirebaseMessaging firebaseMessaging = FirebaseMessaging.getInstance();

            // 메시지를 발송하고 결과를 받음

            String response = firebaseMessaging.send(message);

            // 결과를 콘솔에 출력

            System.out.println("Successfully sent message: " + response);

        } catch (FirebaseMessagingException e) {

            // 예외 처리

            e.printStackTrace();

        }

    }

 

}

 

 

로직 적용은 위와 같이 하면 되고 테스트 환경 설정은 sitelink1 의 내용을 참고하면 된다.

준비가 끝났다면 TestFcmNotification.java 를 실행해서 브라우저에 알림이 뜨는지 확인하면 된다.

 

참고로 위와 같은 셋팅으로도 edge(ver:119.0.2151.58)와 firefox(ver:120.0) 기준으로 최초 접속시에도 알림 권한 허용에 대한 프롬프트가 활성화 되지 않는다.

권한 허용 프롬프트 없이 사용자가 직접 브라우저의 알림 설정을 '허용'으로 변경해 줘야만 메세지 푸시가 정상 동작한다.

edge는 권한을 수동으로라도 변경하지 않으면 브라우저가 아래와 같이 오류 메세지를 출력하면서 권한을 아예 차단 시켜버린다.

"permission has been blocked as the user has ignored the permission prompt several times"

stackoverflow 에서 4개월전에 동일한 증상을 호소한 개발자도 아직까지 답변을 못받고 있는 상태이므로 해결을 포기했다. -> stackoverflow

그리고 아이폰의 모든 브라우저는 지원 불가다. (push event 가 발생하지 않음 -> mozilla, fcm이 아닌 web-push는 우회로 가능)

 

다음은 pc 와 mobile 의 각 브라우저들을 테스트한 결과이다.

1. PC

  - Edge(알림 수동 설정함) : 브라우저를 닫은 상태에서 알림 수신 성공

  - Firefox(알림 수동 설정함) : 브라우저를 활성화해야만 알림 수신 성공

  - Chrome : 브라우저를 활성화해야만 알림 수신 성공

  - Whale : 브라우저를 활성화해야만 알림 수신 성공

2. Mobile

  - Default : 브라우저가 비활성화 상태에서도 수신 성공

  - Edge : 토큰은 받았지만 알림을 전송해도 반응이 없음

  - Firefox : 알림 수동 설정 불가

  - Chrome : 브라우저가 비활성화 상태에서도 수신 성공

  - Whale : 알림 허용창은 뜨지만 토큰을 받아오지 못함

 

브라우저별 분기 처리해야만 하나보다 (pwa 에서 크로스브라우징 작업을 해야할 줄이야...)

방어코드가 꽤 길어질 것 같다.

 

번호 제목 글쓴이 날짜 조회 수
23 FirebaseMessagingException: Requested entity was not found. 황제낙엽 2024.01.12 0
22 책 2권에 대한 목차와 후기 황제낙엽 2023.11.29 7
21 (Copilot) Admin SDK Reference의 java 라이브러리를 이용하여 notification을 fcm에 전송하는 java 예제 황제낙엽 2023.11.28 0
20 firebase.messaging().getToken() 함수와 pushManager.subscribe() 함수의 관계 황제낙엽 2023.11.26 1
19 service worker 재작성시 수동 업데이트 file 황제낙엽 2023.11.25 1
18 [POST/2023.09.13] PWA (Progressive Web Apps) 관련 황제낙엽 2023.11.24 1
17 [POST/2019.11.25] 브라우저 알림(Notification) 팝업에 버튼 추가 with ServiceWorker file 황제낙엽 2023.11.24 0
16 [FCM] FCM 으로 알림 전송 테스트 (spring boot + android + fcm rest) 황제낙엽 2023.11.24 0
15 [FCM] Firebase Console 에서 메세지 보내기 file 황제낙엽 2023.11.24 0
» (OMNIBUSCODE/FCM/WEB/JAVA) web push notification (web browser) 샘플 file 황제낙엽 2023.11.23 3
13 (Copilot) Notification Server 의 종류 황제낙엽 2023.11.23 0
12 (OMNIBUSCODE/FCM/WEB/JAVA) web push notification (web browser) 구현 절차 [1] 황제낙엽 2023.11.20 0
11 서비스 워커(service worker) 등록에 대한 LLM 챗봇의 답변 황제낙엽 2023.11.20 0
10 service worker 개발 참고용 링크 모음 황제낙엽 2023.11.10 0
9 service worker 개발을 위한 mozilla 공식 문서 file 황제낙엽 2023.11.10 0
8 service worker 개발을 위한 chrome 공식 문서 file 황제낙엽 2023.11.10 0
7 푸쉬 알림 개발 관련 레퍼런스 황제낙엽 2023.11.09 2
6 [bard] web-push와 fcm 의 차이 황제낙엽 2023.11.08 1
5 Web Push Notification 에 대한 web.dev(크롬) 의 문서 링크 황제낙엽 2023.11.07 0
4 PWA 관련 링크 모음 황제낙엽 2023.11.06 5