Push Notification In Web App

개요

JS로 브라우저의 token을 발급받고, 서버단에서 message를 전송

  • client(브라우저) : 알림 허용, push notification 수신 이벤트 작성
  • push service(FCM:Firebase cloud messaging)
  • application server(server-side) : message를 전송하기 위해 FCM과 통신

개요

  1. 사용자가 웹 앱에 접속하여 알림을 구독할 경우 클라우드 메시징 서비스로 등록을 요청합니다.
  2. 요청을 받은 클라우드 메시징 서비스는 해당 유저의 등록 정보를 제공합니다.
  3. 유저는 클라우드 메시징 서비스로부터 받은 등록 정보를 애플리케이션 서버로 전송하여 추후 애플리케이션 서버가 나에게 알림을 보낼 수 있도록 합니다.(알림을 받을 유저의 등록 정보가 필요하기 때문)

A. 특정 이벤트(팔로우, 새 댓글 등)로 인해 애플리케이션 서버가 특정 유저에게 푸시 알림을 보내야 하는 경우 알림을 보낼 대상 유저의 등록정보와 푸시 메시지 내용을 클라우드 메시징 서비스로 전달합니다.
B. 애플리케이션 서버로부터 수신한 정보를 확인하며 등록된 유저인 경우 푸시 알림을 보내고, 등록되지 않은 정보인 경우 애플리케이션 서버에게 이를 알립니다.

준비물

client

  1. index.html
  2. firebase-messaging-sw.js (Firebase 메시징 서비스 워커 정의 파일)
<!-- index.html -->
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>fcm</title>
    <link rel="apple-touch-icon" sizes="57x57" href="./img/apple-icon-57x57.png">
    <link rel="apple-touch-icon" sizes="60x60" href="./img/apple-icon-60x60.png">
    <link rel="apple-touch-icon" sizes="72x72" href="./img/apple-icon-72x72.png">
    <link rel="apple-touch-icon" sizes="76x76" href="./img/apple-icon-76x76.png">
    <link rel="apple-touch-icon" sizes="114x114" href="./img/apple-icon-114x114.png">
    <link rel="apple-touch-icon" sizes="120x120" href="./img/apple-icon-120x120.png">
    <link rel="apple-touch-icon" sizes="144x144" href="./img/apple-icon-144x144.png">
    <link rel="apple-touch-icon" sizes="152x152" href="./img/apple-icon-152x152.png">
    <link rel="apple-touch-icon" sizes="180x180" href="./img/apple-icon-180x180.png">
    <link rel="icon" type="image/png" sizes="192x192" href="./img/android-icon-192x192.png">
    <link rel="icon" type="image/png" sizes="32x32" href="./img/favicon-32x32.png">
    <link rel="icon" type="image/png" sizes="96x96" href="./img/favicon-96x96.png">
    <link rel="icon" type="image/png" sizes="16x16" href="./img/favicon-16x16.png">
    <link rel="manifest" href="./manifest.json">
    <meta name="msapplication-TileColor" content="#ffffff">
    <meta name="msapplication-TileImage" content="/ms-icon-144x144.png">
    <meta name="theme-color" content="#ffffff">
    <style>
        div{padding:1rem;}
    </style>
</head>

<body>
<h1>token : </h1>
    <div id="token"></div>
    <h1>msg : </h1>
    <div id="msg"></div>
    <h1>notis : </h1>
    <div id="notis"></div>
    <h1>err : </h1>
    <div id="err"></div>
    <script src="https://www.gstatic.com/firebasejs/8.0.1/firebase-app.js"></script>
    <script src="https://www.gstatic.com/firebasejs/8.0.1/firebase-messaging.js"></script>

    <script>
        MsgElem = document.getElementById("msg")
        TokenElem = document.getElementById("token")
        NotisElem = document.getElementById("notis")
        ErrElem = document.getElementById("err")
        // Initialize Firebase
        var config = {
            apiKey: "your apid key",
            authDomain: "webmanual-edfa4.firebaseapp.com",
            databaseURL: "https://webmanual-edfa4.firebaseio.com",
            projectId: "webmanual-edfa4",
            storageBucket: "webmanual-edfa4.appspot.com",
            messagingSenderId: "389960007523",
            appId: "your appId"
        };
        firebase.initializeApp(config);
        const messaging = firebase.messaging();
        messaging
            .requestPermission()
            .then(function () {
                MsgElem.innerHTML = "Notification permission granted."
                console.log("Notification permission granted.");

                // get the token in the form of promise
                return messaging.getToken()
            })
            .then(function (token) {
                TokenElem.innerHTML = "token is : " + token
            })
            .catch(function (err) {
                ErrElem.innerHTML = ErrElem.innerHTML + "; " + err
                console.log("Unable to get permission to notify.", err);
            });


            //포그라운드
        messaging.onMessage(function (payload) {
            console.log("Message received. : ", payload);
            console.log("Notification.permission : " + Notification.permission);
            NotisElem.innerHTML = NotisElem.innerHTML + JSON.stringify(payload);

            const notificationOption = {
                body: payload.notification.body,
                icon: payload.notification.icon
            };

            if (Notification.permission === 'granted') {
                var notification = new Notification(payload.notification.title, notificationOption);
                notification.onclick = function (ev) {
                    ev.preventDefault();
                    window.open(payload.notification.click_action, '_blank');
                    notification.close();
                }
            }
        });

        messaging.onTokenRefresh(function () {
            messaging.getToken().then(function (newtoken) {
                console.log("New Token : " + newtoken);
            }).catch(function (reason) {
                console.log(reason);
            })
        })


    </script>



</body>

</html>
<!-- index.html -->
<!--
    모바일에선,
웹페이지 자체에서 api를 보내면 푸시알람이 오지 않고,
외부 application server에서(postman 이나 파이어베이스콘솔)에서 api를 보내야 푸시 알람이 온다.<--- 좀 더 연구해봐야 할 부분
-->
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>fcm</title>
    <link rel="apple-touch-icon" sizes="57x57" href="./img/apple-icon-57x57.png">
    <link rel="apple-touch-icon" sizes="60x60" href="./img/apple-icon-60x60.png">
    <link rel="apple-touch-icon" sizes="72x72" href="./img/apple-icon-72x72.png">
    <link rel="apple-touch-icon" sizes="76x76" href="./img/apple-icon-76x76.png">
    <link rel="apple-touch-icon" sizes="114x114" href="./img/apple-icon-114x114.png">
    <link rel="apple-touch-icon" sizes="120x120" href="./img/apple-icon-120x120.png">
    <link rel="apple-touch-icon" sizes="144x144" href="./img/apple-icon-144x144.png">
    <link rel="apple-touch-icon" sizes="152x152" href="./img/apple-icon-152x152.png">
    <link rel="apple-touch-icon" sizes="180x180" href="./img/apple-icon-180x180.png">
    <link rel="icon" type="image/png" sizes="192x192" href="./img/android-icon-192x192.png">
    <link rel="icon" type="image/png" sizes="32x32" href="./img/favicon-32x32.png">
    <link rel="icon" type="image/png" sizes="96x96" href="./img/favicon-96x96.png">
    <link rel="icon" type="image/png" sizes="16x16" href="./img/favicon-16x16.png">
    <link rel="manifest" href="./manifest.json">
    <meta name="msapplication-TileColor" content="#ffffff">
    <meta name="msapplication-TileImage" content="/ms-icon-144x144.png">
    <meta name="theme-color" content="#ffffff">
    <style>
        html,body{height:100%;}
        body{display:flex;justify-content: center;align-items:center;}
        .wrap{display:flex;flex-direction: column;}
        #token{position:absolute;left:0;bottom:0;}
    </style>
</head>

<body>
    <div class="wrap">
        <img src="./logo.png" alt="logo">
        <button type="button">좋아요</button>
    </div>
    <div id="token"></div>
    <!-- <h1>token : </h1>
    <div id="token"></div>
    <h1>msg : </h1>
    <div id="msg"></div>
    <h1>notis : </h1>
    <div id="notis"></div>
    <h1>err : </h1>
    <div id="err"></div> -->
    <script src="https://www.gstatic.com/firebasejs/8.0.1/firebase-app.js"></script>
    <script src="https://www.gstatic.com/firebasejs/8.0.1/firebase-messaging.js"></script>

    <script>
        var mytoken;
        var TokenElem = document.getElementById("token");
        /* test element
        var MsgElem = document.getElementById("msg");
        var TokenElem = document.getElementById("token");
        var NotisElem = document.getElementById("notis");
        var ErrElem = document.getElementById("err"); */
        // Initialize Firebase
        var config = {
            apiKey: "your api key",
            authDomain: "webmanual-edfa4.firebaseapp.com",
            databaseURL: "https://webmanual-edfa4.firebaseio.com",
            projectId: "webmanual-edfa4",
            storageBucket: "webmanual-edfa4.appspot.com",
            messagingSenderId: "389960007523",
            appId: "your appid"
        };
        firebase.initializeApp(config);
        const messaging = firebase.messaging();
        messaging
            .requestPermission()
            .then(function () {
                //MsgElem.innerHTML = "Notification permission granted."
                //console.log("Notification permission granted.");

                // get the token in the form of promise
                return messaging.getToken()
            })
            .then(function (token) {
                TokenElem.innerHTML = "token is : " + token;
                request_pushnotification(token,"welcome","Welcome to your first visit."); // 푸시 알람 요청
                mytoken = token;
            })
            .catch(function (err) {
                //ErrElem.innerHTML = ErrElem.innerHTML + "; " + err
                //console.log("Unable to get permission to notify.", err);
            });


        //포그라운드
        messaging.onMessage(function (payload) {
            //NotisElem.innerHTML = NotisElem.innerHTML + JSON.stringify(payload);

            const notificationOption = {
                body: payload.notification.body,
                icon: payload.notification.icon
            };

            if (Notification.permission === 'granted') {
                var notification = new Notification(payload.notification.title, notificationOption);
                notification.onclick = function (ev) {
                    ev.preventDefault();
                    window.open(payload.notification.click_action, '_blank');
                    notification.close();
                }
            }
        });

        messaging.onTokenRefresh(function () {
            messaging.getToken().then(function (newtoken) {
                console.log("New Token : " + newtoken);
            }).catch(function (reason) {
                console.log(reason);
            })
        });

        var httpRequest;
        function request_pushnotification(token,title,msg) {
            httpRequest = new XMLHttpRequest();

            httpRequest.onreadystatechange = alertContents;
            httpRequest.open('POST', 'https://fcm.googleapis.com/fcm/send');
            httpRequest.setRequestHeader('Content-Type', 'application/json');
            httpRequest.setRequestHeader('Authorization', 'key=서버키');
            
            var data = {
                        notification: {
                            title: title,
                            body: msg,
                            icon: "logo.png",
                            click_action: "http://www.despresso.co.kr/"
                        },
                        to: token,
                    };            
            httpRequest.send(JSON.stringify(data));
        }

        function alertContents() {
            if (httpRequest.readyState === XMLHttpRequest.DONE) {                
                if (httpRequest.status === 200) {
                    //alert(httpRequest.responseText);                    
                    console.log(httpRequest.responseText);
                } else {
                    //alert('request에 뭔가 문제가 있어요.');
                    console.log('request에 뭔가 문제가 있어요.');
                }
            }
        }

        var btn = document.querySelector('.wrap button');
        btn.addEventListener('click',function(){
            request_pushnotification(mytoken,"Notice","You clicked 'favorite'.");
        });

    </script>
</body>
</html>
// firebase-messaging-sw.js
// root 경로에 있어야 한다.
importScripts("https://www.gstatic.com/firebasejs/8.0.1/firebase-app.js");
importScripts("https://www.gstatic.com/firebasejs/8.0.1/firebase-messaging.js");


// Your web app's Firebase configuration
var firebaseConfig = {
  apiKey: "your apikey",
  authDomain: "webmanual-edfa4.firebaseapp.com",
  databaseURL: "https://webmanual-edfa4.firebaseio.com",
  projectId: "webmanual-edfa4",
  storageBucket: "webmanual-edfa4.appspot.com",
  messagingSenderId: "389960007523",
  appId: "your appid"
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);

const messaging = firebase.messaging();

messaging.setBackgroundMessageHandler(function (payload) { // data 메시지를 백그라운드 상태에서 수신하도록 처리
  //console.log("[firebase-messaging-sw.js] Received background message ", payload,);
  
  const { title, ...options } = payload.data;

  return self.registration.showNotification(
    title,
    options,
  );
});

messaging.onBackgroundMessage(function(payload) {
  //console.log('[firebase-messaging-sw.js] Received background message ', payload);
  // Customize notification here
  const notificationTitle = payload.notification.title;
  const notificationOptions = {
    body: payload.notification.body,
    icon: payload.notification.icon,
  };

  self.registration.showNotification(notificationTitle,
    notificationOptions);
});

push service (fcm:Firebase cloud messaging)

firebase 계정을 만들고, 앱을 추가한다. (과정 생략)

application server

message를 전송하기 위해 FCM과 통신할때 Firebase 서버키와 토큰 값을 FCM에 보낸다. Firebase 클라우드 메시징 서버키

postman

curl -X POST -H "Authorization: key=<Server Key>" \
   -H "Content-Type: application/json" \
   -d '{  
    "notification": {
        "title": "FCM Message",
        "body": "This is an FCM Message",
        "icon": "logo.png",
    },
    "to": "<DEVICE_REGISTRATION_TOKEN>",
    "webpush": {
      "fcm_options": {
        "link": "https://dummypage.com"
      }
    }
}' https://fcm.googleapis.com/fcm/send

api tester (크롬 확장 앱)

api test

php

<?php
//send_notification.php
function sendNotification(){
    $url="https://fcm.googleapis.com/fcm/send";

    $fields = array(
        "to"=>$_REQUEST['token'],
        "notification"=>array(
            "body"=>$_REQUEST['message'],
            "title"=>$_REQUEST['title'],
            "icon"=>$_REQUEST['icon'],
            "click_action"=>"https://google.com"
        )
    );

    $headers = array(
        'Authorization: key=서버키',
        'Content-Type:application/json'
    );

    $ch=curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($fields));
    $result=curl_exec($ch);
    print_r($result);
    curl_close($ch);
}
sendNotification();

?>

push test

  1. 아래 링크에서 알림 허용한다.
    https://uxmatrix.org/project/webmanual/fcm/

2.1 php로 메시지 보내는 방법

위 링크에서 받은 토큰 값을 아래 링크의 token쿼리 값에 넣는다. (title 이나 message 쿼리 값은 원하는 값으로 수정한다.)
https://uxmatrix.org/project/webmanual/fcm/send_notification.php?title=Hello&message=haveaniceday!&icon=./logo.png&token=

2.2 Firebase console 에서 메시지 보내는 방법

https://console.firebase.google.com/project/webmanual-edfa4/notification firebase cloud messaging send message input token

참고