Golang으로 Firebase Cloud Messaging(FCM) 구현하기

소개

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 프로젝트를 설정해야 합니다:

  1. Firebase 콘솔(https://console.firebase.google.com/)에 접속합니다.
  2. 새 프로젝트를 생성하거나 기존 프로젝트를 선택합니다.
  3. 프로젝트 설정 → 서비스 계정 탭으로 이동합니다.
  4. "새 비공개 키 생성" 버튼을 클릭하여 서비스 계정 키 파일(JSON)을 다운로드합니다.
  5. 이 키 파일은 안전한 곳에 보관하고 프로젝트에서 참조할 수 있도록 합니다.

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")
}

오류 처리 및 모범 사례

  1. 토큰 관리: 각 기기 토큰은 언제든지 변경될 수 있으므로, 클라이언트 앱이 토큰 갱신을 서버에 알릴 수 있도록 해야 합니다.
  2. 일괄 처리: 대량의 메시지를 보낼 때는 멀티캐스트 API를 사용하거나 배치 작업으로 처리하세요.
  3. 속도 제한: FCM은 초당 요청 수를 제한하므로, 대규모 발송 시 속도 제한을 고려해야 합니다.
  4. 오류 코드 처리: 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을 사용하기 위해 다음과 같은 준비가 필요합니다:

  1. Firebase 프로젝트에 웹 앱 추가
  2. Firebase SDK 설치
  3. 서비스 워커 설정

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 = `
    

${title}

${body}

`; document.body.appendChild(toast); // 3초 후 삭제 setTimeout(() => { toast.classList.add('fade-out'); setTimeout(() => { document.body.removeChild(toast); }, 300); }, 3000); }

데이터 메시지 처리

데이터 메시지를 받아 처리하는 방법은 다음과 같습니다:

// 데이터 메시지 처리 예시
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 = `
    

${title}

${body}

`; // 닫기 버튼 이벤트 리스너 const closeBtn = notificationEl.querySelector('.close-btn'); closeBtn.addEventListener('click', () => { notificationEl.classList.add('closing'); setTimeout(() => { document.body.removeChild(notificationEl); }, 300); }); // 알림 추가 document.body.appendChild(notificationEl); // 5초 후 자동 제거 setTimeout(() => { if (document.body.contains(notificationEl)) { notificationEl.classList.add('closing'); setTimeout(() => { if (document.body.contains(notificationEl)) { document.body.removeChild(notificationEl); } }, 300); } }, 5000); } // 앱 초기화 시 FCM 설정 document.addEventListener('DOMContentLoaded', initializeFirebaseMessaging);

FCM 토큰 관리 모범 사례

  1. 토큰 새로고침 처리: FCM 토큰은 여러 이유로 갱신될 수 있으므로, 토큰 갱신 이벤트를 처리해야 합니다.
  2. 사용자 인증과 연결: 토큰을 사용자 계정과 연결하여 로그인된 사용자에게 메시지를 보낼 수 있습니다.
  3. 주제 구독 관리: 사용자가 관심 있는 주제를 구독하고 관리할 수 있는 인터페이스를 제공합니다.
  4. 다중 기기 지원: 한 사용자가 여러 기기에서 로그인할 경우 모든 기기의 토큰을 관리합니다.

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