소개
Firebase Cloud Messaging(FCM)은 Android, iOS 및 웹 애플리케이션에 무료로 메시지를 안정적으로 전송할 수 있는 크로스 플랫폼 메시징 솔루션입니다. 이 글에서는 Golang을 사용하여 FCM을 구현하는 방법에 대해 자세히 알아보겠습니다.
FCM이란
Firebase Cloud Messaging(FCM)은 다음과 같은 기능을 제공합니다:
- 단일 기기, 기기 그룹 또는 특정 주제를 구독한 기기로 메시지 전송
- 웹, Android 및 iOS 플랫폼 지원
- 알림 메시지와 데이터 메시지의 두 가지 유형 제공
- 안정적이고 배터리 효율적인 메시지 전달
필요한 준비물
- Firebase 프로젝트
- 서비스 계정 키 파일(JSON)
- Go 개발 환경
필요한 패키지
FCM을 Golang에서 사용하기 위해 공식 Firebase Admin SDK를 사용할 수 있습니다:
go get firebase.google.com/go/v4 go get google.golang.org/api/option |
Firebase 프로젝트 설정
FCM을 사용하기 전에 Firebase 프로젝트를 설정해야 합니다:
- Firebase 콘솔(https://console.firebase.google.com/)에 접속합니다.
- 새 프로젝트를 생성하거나 기존 프로젝트를 선택합니다.
- 프로젝트 설정 → 서비스 계정 탭으로 이동합니다.
- "새 비공개 키 생성" 버튼을 클릭하여 서비스 계정 키 파일(JSON)을 다운로드합니다.
- 이 키 파일은 안전한 곳에 보관하고 프로젝트에서 참조할 수 있도록 합니다.
Golang FCM 클라이언트 구현
다음은 Golang으로 FCM을 구현하는 기본적인 예제 코드입니다:
package main import ( "context" "fmt" "log" firebase "firebase.google.com/go/v4" "firebase.google.com/go/v4/messaging" "google.golang.org/api/option" ) func main() { // 서비스 계정 키 파일 경로 serviceAccountKeyPath := "./serviceAccountKey.json" // Firebase 앱 초기화 opt := option.WithCredentialsFile(serviceAccountKeyPath) app, err := firebase.NewApp(context.Background(), nil, opt) if err != nil { log.Fatalf("firebase.NewApp 오류: %v", err) } // FCM 클라이언트 가져오기 ctx := context.Background() client, err := app.Messaging(ctx) if err != nil { log.Fatalf("app.Messaging 오류: %v", err) } // 알림 메시지 생성 message := &messaging.Message{ Notification: &messaging.Notification{ Title: "새 메시지 알림", Body: "새로운 메시지가 도착했습니다!", }, Token: "대상_FCM_토큰", // 클라이언트 기기의 FCM 토큰 } // 메시지 보내기 response, err := client.Send(ctx, message) if err != nil { log.Fatalf("메시지 전송 오류: %v", err) } fmt.Printf("메시지가 성공적으로 전송되었습니다: %s\n", response) } |
고급 FCM 기능 구현
이제 몇 가지 고급 FCM 기능을 살펴보겠습니다.
주제 기반 메시징
특정 주제를 구독한 모든 기기에 메시지를 보낼 수 있습니다:
// 주제 기반 메시지 생성 message := &messaging.Message{ Notification: &messaging.Notification{ Title: "날씨 알림", Body: "오늘은 비가 올 예정입니다.", }, Topic: "weather", // "weather" 주제를 구독한 모든 기기에 전송 } |
조건부 주제 메시징
불리언 조건을 사용하여 여러 주제에 대한 메시지를 보낼 수 있습니다:
// 조건부 주제 메시지 생성 message := &messaging.Message{ Notification: &messaging.Notification{ Title: "스포츠 알림", Body: "축구 경기가 곧 시작됩니다!", }, // 'sports' 주제를 구독하고 'football'을 구독하거나 'soccer'를 구독한 기기 Condition: "'sports' in topics && ('football' in topics || 'soccer' in topics)", } |
멀티캐스트 메시징
여러 기기에 동일한 메시지를 한 번에 보낼 수 있습니다:
// 여러 기기에 보낼 메시지 생성 multicastMessage := &messaging.MulticastMessage{ Notification: &messaging.Notification{ Title: "그룹 알림", Body: "그룹 채팅방에 새 메시지가 있습니다.", }, Tokens: []string{ "첫번째_기기_토큰", "두번째_기기_토큰", "세번째_기기_토큰", }, } // 멀티캐스트 메시지 보내기 response, err := client.SendMulticast(ctx, multicastMessage) if err != nil { log.Fatalf("멀티캐스트 메시지 전송 오류: %v", err) } fmt.Printf("성공: %d, 실패: %d\n", response.SuccessCount, response.FailureCount) |
데이터 메시지
알림 없이 데이터만 전송할 수 있습니다:
// 데이터 메시지 생성 message := &messaging.Message{ Data: map[string]string{ "score": "3-1", "time": "15:10", "team": "토트넘", }, Token: "대상_FCM_토큰", } |
FCM 서버 구현 예제
다음은 Echo 웹 프레임워크를 사용한 FCM 서버 구현 예제입니다:
package main import ( "context" "net/http" firebase "firebase.google.com/go/v4" "firebase.google.com/go/v4/messaging" "github.com/labstack/echo/v4" "google.golang.org/api/option" ) type Server struct { FCMClient *messaging.Client } type PushRequest struct { Token string `json:"token"` Title string `json:"title"` Body string `json:"body"` ImageURL string `json:"image_url,omitempty"` Data map[string]string `json:"data,omitempty"` } func NewServer() (*Server, error) { // Firebase 초기화 opt := option.WithCredentialsFile("./serviceAccountKey.json") app, err := firebase.NewApp(context.Background(), nil, opt) if err != nil { return nil, err } // FCM 클라이언트 생성 client, err := app.Messaging(context.Background()) if err != nil { return nil, err } return &Server{ FCMClient: client, }, nil } func (s *Server) SendPushNotification(c echo.Context) error { var req PushRequest if err := c.Bind(&req); err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "잘못된 요청 형식"}) } // 요청 검증 if req.Token == "" { return c.JSON(http.StatusBadRequest, map[string]string{"error": "토큰이 필요합니다"}) } // 메시지 생성 message := &messaging.Message{ Notification: &messaging.Notification{ Title: req.Title, Body: req.Body, ImageURL: req.ImageURL, }, Data: req.Data, Token: req.Token, } // 메시지 전송 response, err := s.FCMClient.Send(context.Background(), message) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) } return c.JSON(http.StatusOK, map[string]string{"message_id": response}) } func main() { // 서버 초기화 server, err := NewServer() if err != nil { panic(err) } // Echo 인스턴스 생성 e := echo.New() // 라우트 설정 e.POST("/send-notification", server.SendPushNotification) // 서버 시작 e.Start(":8080") } |
오류 처리 및 모범 사례
- 토큰 관리: 각 기기 토큰은 언제든지 변경될 수 있으므로, 클라이언트 앱이 토큰 갱신을 서버에 알릴 수 있도록 해야 합니다.
- 일괄 처리: 대량의 메시지를 보낼 때는 멀티캐스트 API를 사용하거나 배치 작업으로 처리하세요.
- 속도 제한: FCM은 초당 요청 수를 제한하므로, 대규모 발송 시 속도 제한을 고려해야 합니다.
- 오류 코드 처리: FCM에서 반환하는 오류 코드를 적절히 처리합니다.
// 오류 처리 예제 response, err := client.Send(ctx, message) if err != nil { if messaging.IsUnregistered(err) { // 토큰이 더 이상 유효하지 않음 - 데이터베이스에서 제거 removeTokenFromDatabase(token) } else if messaging.IsInvalidArgument(err) { // 잘못된 인자 - 요청 수정 필요 log.Printf("잘못된 메시지 형식: %v", err) } else { // 기타 오류 처리 log.Printf("FCM 오류: %v", err) } return } |
보안 고려사항
- 서비스 계정 키 보호: 서비스 계정 키는 절대 공개 저장소에 커밋하거나 클라이언트 측 코드에 포함하지 마세요.
- 환경 변수 사용: 서비스 계정 키 파일 경로를 환경 변수로 관리하세요.
- 최소 권한 원칙: FCM API에 액세스하는 서비스 계정에 필요한 최소한의 권한만 부여하세요.
웹 JavaScript에서 FCM 메시지 수신하기
Go 서버에서 FCM 메시지를 보내는 방법을 살펴봤으니, 이제 웹 클라이언트에서 이러한 메시지를 수신하는 방법을 알아보겠습니다.
필요한 설정
웹 앱에서 FCM을 사용하기 위해 다음과 같은 준비가 필요합니다:
- Firebase 프로젝트에 웹 앱 추가
- Firebase SDK 설치
- 서비스 워커 설정
Firebase SDK 설치
NPM을 사용하는 경우:
npm install firebase |
또는 CDN을 사용하는 경우 HTML에 다음 스크립트를 추가합니다:
<!-- Firebase App (필수) --> <script src="https://www.gstatic.com/firebasejs/9.6.10/firebase-app.js"></script> <!-- FCM --> <script src="https://www.gstatic.com/firebasejs/9.6.10/firebase-messaging.js"></script> |
웹 앱에서 FCM 초기화하기
Firebase 콘솔에서 얻은 구성 정보로 Firebase를 초기화합니다:
// Firebase 9 버전 이상 import { initializeApp } from 'firebase/app'; import { getMessaging, getToken, onMessage } from 'firebase/messaging'; // Firebase 구성 정보 const firebaseConfig = { apiKey: "your-api-key", authDomain: "your-project.firebaseapp.com", projectId: "your-project", storageBucket: "your-project.appspot.com", messagingSenderId: "your-sender-id", appId: "your-app-id" }; // Firebase 초기화 const app = initializeApp(firebaseConfig); const messaging = getMessaging(app); |
서비스 워커 설정
FCM 메시지를 백그라운드에서 수신하려면 서비스 워커가 필요합니다. 프로젝트 루트 디렉토리에 firebase-messaging-sw.js
파일을 생성합니다:
// firebase-messaging-sw.js importScripts('https://www.gstatic.com/firebasejs/9.6.10/firebase-app-compat.js'); importScripts('https://www.gstatic.com/firebasejs/9.6.10/firebase-messaging-compat.js'); firebase.initializeApp({ apiKey: "your-api-key", authDomain: "your-project.firebaseapp.com", projectId: "your-project", storageBucket: "your-project.appspot.com", messagingSenderId: "your-sender-id", appId: "your-app-id" }); const messaging = firebase.messaging(); // 백그라운드 메시지 처리 messaging.onBackgroundMessage((payload) => { console.log('백그라운드 메시지 수신:', payload); const notificationTitle = payload.notification.title; const notificationOptions = { body: payload.notification.body, icon: '/firebase-logo.png' }; self.registration.showNotification(notificationTitle, notificationOptions); }); |
FCM 토큰 얻기
각 클라이언트 디바이스는 고유한 FCM 토큰을 가지며, 이 토큰을 사용하여 특정 기기에 메시지를 보낼 수 있습니다. 이 토큰을 서버에 등록해야 합니다:
// 알림 권한 요청 및 토큰 가져오기 async function requestPermissionAndGetToken() { try { // 알림 권한 요청 const permission = await Notification.requestPermission(); if (permission === 'granted') { // VAPID 키는 Firebase 콘솔의 "웹 구성" 섹션에서 찾을 수 있습니다 const token = await getToken(messaging, { vapidKey: 'your-vapid-key' }); console.log('FCM 토큰:', token); // 토큰을 서버에 등록 await registerTokenWithServer(token); return token; } else { console.log('알림 권한이 거부되었습니다.'); return null; } } catch (error) { console.error('토큰 얻기 실패:', error); return null; } } // 토큰을 서버에 등록하는 함수 async function registerTokenWithServer(token) { try { const response = await fetch('/api/register-fcm-token', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ token }), }); const data = await response.json(); console.log('토큰 등록 성공:', data); } catch (error) { console.error('토큰 등록 실패:', error); } } |
포그라운드 메시지 수신
앱이 활성화된 상태(포그라운드)에서 메시지를 수신하려면 onMessage
리스너를 설정합니다:
// 포그라운드 메시지 처리 onMessage(messaging, (payload) => { console.log('포그라운드 메시지 수신:', payload); // 사용자 인터페이스에 알림 표시 const title = payload.notification.title; const options = { body: payload.notification.body, icon: '/path/to/icon.png', }; // 브라우저 알림 표시 if ('Notification' in window && Notification.permission === 'granted') { new Notification(title, options); } // 또는 커스텀 UI 요소로 알림 표시 showCustomNotification(title, payload.notification.body); }); // 커스텀 알림 UI 표시 함수 예시 function showCustomNotification(title, body) { // 예: 페이지에 토스트 알림 표시 const toast = document.createElement('div'); toast.className = 'notification-toast'; toast.innerHTML = ` |
데이터 메시지 처리
데이터 메시지를 받아 처리하는 방법은 다음과 같습니다:
// 데이터 메시지 처리 예시 onMessage(messaging, (payload) => { // 데이터 메시지인 경우 if (payload.data) { console.log('데이터 메시지 수신:', payload.data); // 데이터에 따른 처리 if (payload.data.type === 'new_message') { // 새 메시지 처리 updateChatUI(payload.data); } else if (payload.data.type === 'update_score') { // 점수 업데이트 처리 updateScoreUI(payload.data); } } }); // 채팅 UI 업데이트 함수 예시 function updateChatUI(data) { const chatContainer = document.getElementById('chat-container'); const messageElement = document.createElement('div'); messageElement.className = 'chat-message'; messageElement.textContent = `${data.sender}: ${data.message}`; chatContainer.appendChild(messageElement); // 스크롤을 최신 메시지로 이동 chatContainer.scrollTop = chatContainer.scrollHeight; } |
전체 구현 예시
다음은 웹 앱에서 FCM을 구현하는 전체 예시입니다:
// app.js import { initializeApp } from 'firebase/app'; import { getMessaging, getToken, onMessage } from 'firebase/messaging'; // Firebase 설정 const firebaseConfig = { apiKey: "your-api-key", authDomain: "your-project.firebaseapp.com", projectId: "your-project", storageBucket: "your-project.appspot.com", messagingSenderId: "your-sender-id", appId: "your-app-id" }; // Firebase 초기화 const app = initializeApp(firebaseConfig); const messaging = getMessaging(app); // 앱 초기화 시 호출 async function initializeFirebaseMessaging() { try { // 알림 권한 확인 if (Notification.permission === 'default') { await Notification.requestPermission(); } if (Notification.permission === 'granted') { // FCM 토큰 얻기 const token = await getToken(messaging, { vapidKey: 'your-vapid-key' }); console.log('FCM 토큰:', token); // 토큰을 서버에 등록 await fetch('/api/register-token', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ token }), }); // 포그라운드 메시지 수신 리스너 설정 onMessage(messaging, handleForegroundMessage); } } catch (error) { console.error('FCM 초기화 오류:', error); } } // 포그라운드 메시지 처리 함수 function handleForegroundMessage(payload) { console.log('메시지 수신:', payload); // 알림이 있는 경우 if (payload.notification) { const { title, body } = payload.notification; // 커스텀 UI로 알림 표시 showNotification(title, body); } // 데이터가 있는 경우 if (payload.data) { // 데이터 유형에 따른 처리 handleDataMessage(payload.data); } } // 데이터 메시지 처리 함수 function handleDataMessage(data) { switch (data.type) { case 'new_message': updateChatUI(data); break; case 'update_profile': refreshUserProfile(); break; case 'refresh_data': reloadPageData(); break; default: console.log('알 수 없는 데이터 유형:', data.type); } } // 화면에 알림 표시 함수 function showNotification(title, body) { // 알림 요소 생성 const notificationEl = document.createElement('div'); notificationEl.className = 'notification'; notificationEl.innerHTML = ` |
FCM 토큰 관리 모범 사례
- 토큰 새로고침 처리: FCM 토큰은 여러 이유로 갱신될 수 있으므로, 토큰 갱신 이벤트를 처리해야 합니다.
- 사용자 인증과 연결: 토큰을 사용자 계정과 연결하여 로그인된 사용자에게 메시지를 보낼 수 있습니다.
- 주제 구독 관리: 사용자가 관심 있는 주제를 구독하고 관리할 수 있는 인터페이스를 제공합니다.
- 다중 기기 지원: 한 사용자가 여러 기기에서 로그인할 경우 모든 기기의 토큰을 관리합니다.
FCM 웹 푸시 스타일링
웹 푸시 알림의 모양을 개선하기 위한 CSS 예시:
/* 인앱 알림 스타일 */ .notification { position: fixed; top: 20px; right: 20px; width: 300px; background-color: #fff; border-radius: 8px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); overflow: hidden; z-index: 1000; animation: slide-in 0.3s ease-out forwards; } .notification.closing { animation: slide-out 0.3s ease-in forwards; } .notification-header { display: flex; justify-content: space-between; align-items: center; padding: 12px 15px; background-color: #4285f4; color: white; } .notification-header h3 { margin: 0; font-size: 16px; font-weight: 500; } .close-btn { background: none; border: none; color: white; font-size: 20px; cursor: pointer; padding: 0; line-height: 1; } .notification-body { padding: 15px; } .notification-body p { margin: 0; font-size: 14px; line-height: 1.4; } @keyframes slide-in { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } @keyframes slide-out { from { transform: translateX(0); opacity: 1; } to { transform: translateX(100%); opacity: 0; } } |
결론
이 글에서는 Golang을 사용하여 Firebase Cloud Messaging(FCM)을 구현하는 방법에 대해 알아보았습니다. FCM은 크로스 플랫폼 메시징 솔루션으로, Go 언어와 함께 사용하면 효율적이고 안정적인 푸시 알림 시스템을 구축할 수 있습니다.
Go의 강력한 동시성 모델과 FCM의 안정적인 메시징 인프라를 결합하면 수백만 명의 사용자에게 푸시 알림을 전송할 수 있는 확장 가능한 시스템을 구축할 수 있습니다.
이 글이 여러분의 FCM 구현에 도움이 되길 바랍니다. 질문이나 의견이 있으시면 댓글로 남겨주세요!