포스트

웹에서 FCM 보내기

node.js 환경에서 FCM 보내기를 하려고 합니다. FCM은 HTTPv1(버전)으로 기준으로 작성하였습니다.

웹 푸시는 운영하는 페이지에는 접속은 안해도 되지만 브라우저는 실행시켜야지 푸시메세지를 송신할 수 있습니다.

폴더 구조는 아래의 사진처럼 했습니다.

첫번째 사진

1. server 단 코드 app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
const express = require('express');
const path = require('path');
const { JWT } = require('google-auth-library');
const fs = require('fs').promises;
var bodyParser = require("body-parser");
const axios = require('axios');

const app = express();
const PORT = 3000;

app.use(bodyParser.json());
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(express.static(path.join(__dirname, 'public')));

// Firebase Access Token 가져오기 함수
const firebaseMessagingScope = 'https://www.googleapis.com/auth/firebase.messaging';

async function getAccessToken() {
  try {
    const json = await fs.readFile('public/<project>-firebase-adminsdk-uum70-4ab63d0963.json', 'utf8');
    // access token을 가져오기 위해 json 파일 읽는 부분
    const credentials = JSON.parse(json);

    const client = new JWT(
      credentials.client_email,
      null,
      credentials.private_key,
      [firebaseMessagingScope]
    );

    await client.authorize();
    const accessToken = client.credentials.access_token;
    return accessToken;
  } catch (error) {
    console.error('Error getting access token:', error);
    throw error;
  }
}

// 루트 경로
app.get('/', (req, res) => {
  res.sendFile(path.join(__dirname, 'public', 'index.html'));
});


// 푸시 알림 보내기 라우트
app.post('/send-notification', async (req, res) => {
  const token = req.body.token;
  const title = req.body.title;
  const content = req.body.content;

  const access_token = await getAccessToken(); // 위에서 선언한 access token 가져오는 함수 실행

  try {
    const response = await axios.post('https://fcm.googleapis.com/v1/projects/<your_porject_id>/messages:send', {
      //http v1 부터 위의 url처럼 사용합니다. 
      message: {
        token: token,
        notification: {
          title: title,
          body: content,
        },
      },
    }, {
      headers: {
        'Content-Type': 'application/json; charset=UTF-8',
        'Authorization': 'Bearer ' + access_token,
      }
    });
    if (response.status === 200) {
      res.send('푸시 메시지가 성공적으로 전송되었습니다.');
    } else {
      res.status(500).send('푸시 메시지 전송 실패: ' + response.statusText);
    }
  } catch (e) {
    console.error('sendPushNotification error:', e.toString());
  }
});


// 서버 시작
app.listen(PORT, () => {
  console.log(`서버실행 http://localhost:${PORT}`);
});

access token을 발급받기 위해 ‘google-auth-library’ 패키지를 설치해서 사용합니다.



2. 웹 페이지 index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Firebase Cloud Messaging Example</title>
     <!-- Firebase JavaScript SDK -->
     <script src="https://www.gstatic.com/firebasejs/8.6.1/firebase-app.js"></script>
     <script src="https://www.gstatic.com/firebasejs/8.6.1/firebase-messaging.js"></script>
     
     <!-- Firebase 초기화 및 서비스 워커 등록 -->
     <script src="firebase_init.js"></script>

     <!-- jquery -->
     <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
</head>
<body>
    <h1>Push Notification</h1>
    <textarea id="token-input" placeholder="Enter token"></textarea>
    <br><br>
    <input type="text" id="title-input" placeholder="Enter title">
    <br><br>
    <textarea id="content-input" placeholder="Enter content"></textarea>
    <br><br>
    <button id="send-button">Send Push Notification</button>
    <script>
        $(document).ready(function () {
            $('#send-button').click(function () {
                const token = $('#token-input').val(); 
                const title = $('#title-input').val();
                const content = $('#content-input').val();
                // input, textarea 에 입력된 value 를 /send-notification'
                $.ajax({
                    url: '/send-notification', 
                    type: 'POST',
                    data: {
                        token: token,
                        title: title,
                        content: content
                    },
                    success: function (response) {
                        console.log('Push notification sent successfully');
                    },
                    error: function (error) {
                        console.error('Failed to send push notification:', error);
                    }
                });
            });
        });
    </script>
</body>
</html>

이 html파일이 public 안에 있는 이유는 app.js 애서 res.sendFile로 파일을 전달하고 view 폴더를 app.js에서 따로 지정을 안했기 때문입니다.



3. 웹 페이지 index.html에 있는 firebase_init.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
// Firebase 설정
    const firebaseConfig = {
    apiKey: "-.....-9RtY",
    authDomain: "<your_project_id>.firebaseapp.com",
    projectId: "<your_project_id>",
    storageBucket: "<your_project_id>.appspot.com",
    messagingSenderId: "........",
    appId: ".................",
    measurementId: "G-....."
  };

// Firebase 초기화
firebase.initializeApp(firebaseConfig);

// VAPID 키 설정 (Firebase Cloud Messaging을 위해 필요)
const vapidKey = '.....-Wgu.....';

// Firebase Messaging 인스턴스 가져오기
const messaging = firebase.messaging();

// 서비스 워커 등록 및 처리
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/firebase-messaging-sw.js', {
    scope: '/firebase-cloud-messaging-push-scope'
  }).then((registration) => {

    // FCM 메시지 수신 핸들러 등록
    messaging.onMessage((payload) => {
      
      // 알림 커스터마이징 가능
      const notificationTitle = payload.notification.title;
      const notificationOptions = {
        body: payload.notification.body,
      };
  
      registration.showNotification(notificationTitle, notificationOptions);
    });
  }).catch((err) => {
    console.error('Service Worker registration failed:', err);
  });
}

// 사용자에게 알림 허용 여부 묻기
function requestPermission() {
  Notification.requestPermission().then((permission) => {
    if (permission === "granted") {
      // 알림 권한 허용 시 추가 작업 수행 가능
      getFCMToken();
    }
  });
}

// FCM 토큰 받기
function getFCMToken() {
  messaging.getToken({ vapidKey: vapidKey }).then((currentToken) => {
    if (currentToken) {
      console.log("FCM Token:", currentToken);
      // 토큰을 서버로 전송하고 UI 업데이트 등 추가 작업 수행 가능

      // 직접 #token-input textarea에 값 넣는 부분
      document.getElementById('token-input').value = currentToken
    } else {
      console.log("No registration token available. Request permission to generate one.");
    }
  }).catch((err) => {
    console.log("An error occurred while retrieving token. ", err);
  });
}

// 초기화 후 알림 권한 요청
requestPermission();

getFCMToken() 함수에 있는 document.getElementById(‘token-input’).value = currentToken 부분이 index.html에 있는 id=”token-input”인 textarea에 값을 넣는 부분입니다.

그리고 service worker라는 것이 있는데 해당 웹 페이지에 접속을 안해도 푸시메시지를 송신하기 위해 꼭 필요한 작업입니다. 웹 페이지에 등록이 되어야 가능합니다.

firebase에서 fcm 보낼 때는 firebase-messaging-sw.js라는 파일 명의 service worker 파일이 있어야 합니다. 해당 파일은 node.js 환경에서 작업을 하기 때문에 public 폴더 안에 위치시켜야 합니다.



4. firebase-messaging-sw.js 파일 내용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
  importScripts('https://www.gstatic.com/firebasejs/9.15.0/firebase-app-compat.js');
  importScripts('https://www.gstatic.com/firebasejs/9.15.0/firebase-messaging-compat.js');

  // Firebase 설정
    const firebaseConfig = {
      apiKey: "-.....-9RtY",
      authDomain: "<your_project_id>.firebaseapp.com",
      projectId: "<your_project_id>",
      storageBucket: "<your_project_id>.appspot.com",
      messagingSenderId: "........",
      appId: ".................",
      measurementId: "G-....."
    };
    
  // Firebase 초기화
  firebase.initializeApp(firebaseConfig);

  // Firebase Messaging 인스턴스 가져오기
  const messaging = firebase.messaging();

  // 백그라운드 메시지 핸들러 설정
  messaging.onBackgroundMessage(function(payload) {
    console.log('[firebase-messaging-sw.js] Received background message ', payload);

    // 알림 커스터마이징 가능
    const notificationTitle = payload.notification.title;
    const notificationOptions = {
      body: payload.notification.body,
    };

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



5. firebase-adminsdk-uum70-4ab63d0963.json 대략적인 파일 내용

1
2
3
4
5
6
7
8
9
10
11
12
13
{
  "type": ".....",
  "project_id": "<your_project_id>",
  "private_key_id": ".............",
  "private_key": "-----BEGIN PRIVATE KEY----lUiRok/.....",
  "client_email": "firebase-admin......0@<your_project_id>.iam.....com",
  "client_id": ".......",
  "auth_uri": "https://a..................",
  "token_uri": "https://...............n",
  "auth_provider_x509_cert_url": "........",
  "client_x509_cert_url": "https://www......%<your_project_id>.iam......com",
  "universe_domain": "googleapis.com"
}

firebase-adminsdk-uum70-4ab63d0963.json은 firebase에서 프로젝트 설정에서 확인할 수 있습니다.

6. 패키지 정보

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
  "name": "node_fcm",
  "version": "1.0.0",
  "description": "",
  "main": "app.js",
  "scripts": {
    "start": "node app.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@onesignal/node-onesignal": "^2.0.1-beta1",
    "axios": "^1.7.2",
    "body-parser": "^1.20.2",
    "ejs": "^3.1.10",
    "express": "^4.18.2",
    "google-auth-library": "^9.11.0",
    "path": "^0.12.7"
  }
}




🧐 오늘의 소감은?

처음으로 웹 푸시를 해보는 데 너무 구글이나 깃허브 같으느 로그인을 해서 토큰을 얻어오는 것이 아닌 로그인 없이 해당 사이트를 접속했을때 토큰을 가져오는 방법이 생소했었습니다.

다른 예제들은 firebase-admin 패키지를 사용해서 토큰을 가져오더라고요 ㅠㅠㅠ 어떻게 접근해야하는 지 처음 감이 너무 안잡혔어서 3일 밤 꼬박 새서 겨우 해결했던 것 같습니다.. 이건 진짜 너무 어려웠어요 그래도 다음 프로젝트를 할 때 좀 더 쉽게 접근 할 수 있겠죠?? 휴휴 그나마 그 생각이 제가 일하면서 힘이 됩니다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.