본문 바로가기

개발 노트/개발 삽질

Python으로 푸쉬 알람 전송과 시행착오

728x90
반응형

 

개요

최근 푸시 알람 기능을 만들어야 하는 일이 생겼다.

 

이것저것 주워들은 게 있어서 푸시 알람 자체에 대해서 어렵게 생각하진 않고 있었지만 테스트를 위해 device token을 얻어내는 과정이 다소 까다로운 지점이었다. 아무래도 특정 기기에 푸쉬 알람을 보내려면 device token이 필요했는데 이를 얻기 위해 내가 사용중인 기기의 device toke을 얻어내는 과정에서 JavaScript를 다뤄야하는 부분이 그러했다.

 

우여곡절끝에 JavaScript 코드를 만들어 Web/Android 환경에서 device token을 얻어내는데 성공했지만 IOS에서  기기 예를 들어 아이폰이나 아이패드에서는 device token이 얻어지지 않는 현상을 겪었다. 여러 아이디어를 떠올려보고 결국 해결은 했다.

 

이 과정을 정리해 추후 다시 푸시 알람 기능을 만들 때 참고하려 한다.

 

FireBase 설정

여타 그렇듯 푸시 알람은 Google Fire Base를 이용했다. FireBase SDK를 사용하기 위해선 Fire Base의 프로젝트 설정을 통해서 얻어낸 Credential 파일과 VAPIKEY가 필요하다. Credential 파일은 다음과 같이 생겼다.

{
  "type"                       : "service_account",
  "project_id"                 : "",
  "private_key_id"             : "",
  "private_key"                : "",
  "client_email"               : "",
  "client_id"                  : "",
  "auth_uri"                   : "<https://accounts.google.com/o/oauth2/auth>",
  "token_uri"                  : "<https://oauth2.googleapis.com/token>",
  "auth_provider_x509_cert_url": "<https://www.googleapis.com/oauth2/v1/certs>",
  "client_x509_cert_url"       : "<https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-fbsvc%40poomasi-6a725.iam.gserviceaccount.com>",
  "universe_domain"            : "googleapis.com"
}

이어서 VAPIKEY가 필요한데 FireBase의 Cloud Messaging을 생성했으면 다음 위치에 해당 키를 확인할 수 있다.

 

 

Python으로 푸시 알람 보내기

푸시 알람 전송은 Python을 사용해 테스트했다. 즉, Python으로 FireBase의 기능을 사용할 수 있으면 되는 상황이다.

 

Python에서는 firebase_admin이라는 라이브러리를 통해 FireBase의 기능을 사용할 수 있었다.

$ pip install firebase_admin

이를 활용해 Python으로 푸시 알람을 보내는 예제 코드는 다음과 같다.

import firebase_admin
from firebase_admin import messaging, App

firebase_admin.initialize_app(credential_file_path)

message = messaging.Message(
    token=device_token,
    notification=messaging.Notification(
        title=title,
        body=body,
        image=image
    ),
    data={
	    "key1" : "value1"
    }
)

messaging.send(message)

credential_file_path의 값은 FireBase 프로젝트 설정에서 받은 credential 파일의 경로를 입력하면 된다. 추가로 message.send(message)를 사용하면 message id가 return 되는데 이 값은 추후 메시지 로깅에 활용해 볼 수도 있다.

 

이 예제 코드는 Python으로 푸시 알람 전송에 관해 검색을 하면 나오는 전형적인 코드였다. 이런 예제 코드를 따라 하고 나서 들었던 의문점은 “그래서 결국 나의 device_token은 어떻게 얻는가?”라는 부분이었다.

 

 

device_token을  어떻게 얻지?

firebase_admin이라는 라이브러리를 설치했기에 이를 통해 device token을 알아내는 어떤 방법이 있지 않을까 유추해보고 찾아봤지만 그런 방법은 발견하지 못했다.

 

생각해보니 결국 사용자의 기기로 푸시 알람을 보내는 것이기에 device_token을 얻어내려면 간단하게라도 “프런트” 페이지가 필요한 상황이었다.

 

그런데 “푸시 알람”이라는 키워드로 검색해서 그런지 대부분 Android 코드를 통해 device_token을 얻는 예제가 많았다. “웹 푸시 알람”이라고 생각을 확장하고 보니까 “JavaScript”에서 충분히 가능하지 않나”라는 아이디어를 떠올려서 조사한 끝에 후술 할 항목을 통해 device token을 얻어낼 수 있었다.

 

다음은 device_token을 얻어내기 위해 사용했던 파일들이다.

├── firebase-config.js
├── firebase-messaging-sw.js
├── index.html
├── main.js

각각의 코드 내용은 하기 내용을 참조하자.

firebase-messaging-sw.js

importScripts('<https://www.gstatic.com/firebasejs/11.7.1/firebase-app-compat.js>');
importScripts('<https://www.gstatic.com/firebasejs/11.7.1/firebase-messaging-compat.js>');

firebase.initializeApp({
    apiKey: "",
    authDomain: "",
    projectId: "",
    storageBucket: "",
    messagingSenderId: "",
    appId: ""
});

const messaging = firebase.messaging();

messaging.onBackgroundMessage(function(payload) {
  console.log('[firebase-messaging-sw.js] 백그라운드 메시지 수신:', payload);
  const { title, ...options } = payload.notification;
  console.log(title)
});

firebase-config.js

// firebase-config.js
import { initializeApp } from "<https://www.gstatic.com/firebasejs/11.7.1/firebase-app.js>";
import {
  getMessaging,
  getToken,
  onMessage,
  isSupported
} from "<https://www.gstatic.com/firebasejs/11.7.1/firebase-messaging.js>";

// Firebase 설정
const firebaseConfig = {
    apiKey: "",
    authDomain: "",
    projectId: "",
    storageBucket: "",
    messagingSenderId: "",
    appId: ""
};

const app = initializeApp(firebaseConfig);

export async function initMessaging() {
  const supported = await isSupported();
  if (!supported) {
    alert("이 브라우저는 FCM을 지원하지 않습니다.");
    console.warn("이 브라우저는 FCM을 지원하지 않습니다.");
    return;
  }

  // 알림 권한 요청
  const messaging = getMessaging(app);

  try {
    // Service Worker 등록
    const registration = await navigator.serviceWorker.register("/firebase-messaging-sw.js");
    console.log("Service Worker 등록 성공:", registration);

    // FCM 토큰 요청
    const token = await getToken(messaging, {
      vapidKey: "",
      serviceWorkerRegistration: registration
    });

    console.log("FCM 토큰:", token);
    document.getElementById("token-container").textContent = token;

  } catch (err) {
    console.error("FCM 토큰 가져오기 실패:", err);
  }

  // 포그라운드 수신 처리
  onMessage(messaging, (payload) => {
    console.log("포그라운드 메시지 수신:", payload);
    const { title, body, icon } = payload.notification;
    // TODO: 알림 표시 구현 가능
  });
}

initMessaging();

index.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8" />
  <title>FCM 테스트</title>
    <link rel="manifest" href="/manifest.webmanifest" />
</head>

<body>
  <h1>FCM 테스트 페이지</h1>

  <button onclick="requestNotificationPermission()">알림 권한 요청</button>
  <button onclick="initMessaging()">토큰 요청</button>

 <div id="token-container"></div>
 <div id="token-container2"></div>
 <div id="token-container3"></div>
  <script type="module" src="./main.js"></script>
</body>

    <script>
        async function requestNotificationPermission() {
          if (!("Notification" in window)) {
            alert("이 브라우저는 알림을 지원하지 않습니다.");
            return;
          }

          const permission = await Notification.requestPermission();
          console.log("알림 권한 상태:", permission);

          if (permission === "granted") {
            alert("알림 권한이 허용되었습니다!");
            // FCM 토큰 요청 등 후속 작업 가능
          } else if (permission === "denied") {
            alert("알림 권한이 거부되었습니다.\\n설정에서 다시 허용해주세요.");
          } else {
            alert("알림 권한이 보류 중입니다.");
          }
        }

    </script>
</html>

main.js

import { initMessaging } from './firebase-config.js';

if ('serviceWorker' in navigator) {
   window.initMessaging = initMessaging;
  initMessaging();
}

 

 

 

device_token에 대한 시행착오

앞선 프런트 코드까지 세팅 후 localhost에서 동작했을 때는 무리 없이 device_token을 얻어냈지만 이 과정에서 다수의 시행착오가 있었다.

 

알림 권한이 필요하다.

푸시 알람을 보내기 위해서는 알림 권한이 허용되어야 한다. JavaScript에서는 다음 코드를 통해 알림 여부를 물어 권한을 허용할 수 있었다.

<script>
    async function requestNotificationPermission() {
      const permission = await Notification.requestPermission();
      console.log("알림 권한 상태:", permission);

      if (permission === "granted") {
        alert("알림 권한이 허용되었습니다!");
        // FCM 토큰 요청 등 후속 작업 가능
      } else if (permission === "denied") {
        alert("알림 권한이 거부되었습니다.\n설정에서 다시 허용해주세요.");
      } else {
        alert("알림 권한이 보류 중입니다.");
      }
    }

</script>

 

device_token은 https에서만 가능하다.

다른 환경에서도 제대로 동작이 가능한지를 테스트하기 위해 조사를 이어나가던 중 device_token 요청은 https 환경에서만 가능하단 문서를 읽게 되었다. 외부 공개용 도메인이 없었기에 조금 난감한 상황이었지만 locahost에 띄워놓은 http 서버를 ngrok와 연계하여 해결할 수 있었다.

 

IOS는 아이패드에서 테스트했다.

IOS 환경에서의 테스트는 소유 중인 아이패드를 통해서 진행했다. 이때 아이패드는 IOS 17.4 버전이었는데 웹 푸시가 제대로 동작하지 않았다. IOS는 16.4부터 푸시 알람을 지원하다고 되어있는 것과는 반대의 상황이었다. 결국 아이패드를 업데이트하니 이 문제는 해결됐다.

 

IOS에서는 버튼을 클릭하게끔 만들어야..

Android에서도 무리 없이 동작하는 걸 체크하고 IOS로 넘어가니 문제가 생겼다. Android 환경에서는 Chrome 브라우저로 접속만 해도 device_token이 얻어졌는데 IOS에서는 그대로 동작하지 않았다. 몇 가지 시행착오 끝에 이를 해결하는 방법은 다음과 같았다.

브라우저 접속 > 홈 화면에 추가 > html 버튼 (알림허용신청 기능, 토큰 획득 기능) 추가 > html 버튼 클릭

Android에서는 브라우저에 접속하면 Javascript가 알아서 실행이 됐는데 IOS에서는 실행할 함수를 직접 클릭한다는 점이다. 문제는 해결했지만 이렇게 하는 정책이나 근거는 찾아보기 어려웠다.

 

IOS에서 방해금지모드를 걸었는지 체크하자.

알림 설정도 잘 되어있는데 푸시 알람이 발생하지 않는 경우였다. 이는 “방해금지모드”에 의한 것이었고 이를 해제하자 푸시 알람이 잘 발생되었다.

 

그 후 이러한 사례에 대해서 모아놓은 링크를 발견할 수 있었다. 필자가 참고한 링크는 다음과 같다.

https://www.tenorshare.kr/ios-16/ios-16-notifications-are-no-longer-showing-up.html

 

iOS 17/16: 아이폰 푸시 알림 안옴 및 알림 오류 해결법

17년의 신뢰, 20% 할인은 오직 여러분을 위해! 카운트다운 00일: 00시간: 00분: 00초

www.tenorshare.kr

 

 

FCM 관련 데이터 참고하기

푸시 알람을 보낼 때 메시지의 내용의 “어디에 뭐가 바뀌었으면 좋겠는데”라고 생각한다면 다음 문서를 참고하자.

https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification

 

마치며

지금까지 푸시 알람 기능을 만들면서 겪었던 시행착오에 대해서 간단히 작성했다. device_token을 알아내야 하는 과정에서 JavaScript 코드를 다뤄야 했기에 이색적인 경험이었다. (필자는 JavaScript를 잘 모른다)

 

그러나 백엔드 관점으로 푸시 알람은 기능을 만들게 된다면 생각해 볼 게 많다는 게 까다로운 점인 듯하다. Message FanOut이라던가 관련 법령이라던가 device_token의 갱신 타이밍 같은 부분이다.

 

본 포스팅의 내용은 단순히 세팅할 때 겪었던 시행착오만을 기록했기에 관련 내용은 따로 다루지 않았다. 다음에 기회가 되면 다뤄보기로 하고 이만 글을 마친다.

728x90
반응형