소개
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 구현에 도움이 되길 바랍니다. 질문이나 의견이 있으시면 댓글로 남겨주세요!