DEV Community

Cover image for FCM HTTP v1 푸시 알림, 서버와 Flutter 앱에서 처음부터 구현하기
바람의평온
바람의평온

Posted on

FCM HTTP v1 푸시 알림, 서버와 Flutter 앱에서 처음부터 구현하기

FCM HTTP v1 푸시 알림, 서버와 Flutter 앱에서 처음부터 구현하기

FCM HTTP v1 푸시 알림, 서버와 Flutter 앱에서 처음부터 구현하기

안녕하세요, 코딩아빠입니다. 오늘 제가 들려드릴 이야기는, 최근에 직접 부딪히고 해결했던 경험을 고스란히 담아낸 기술 노트입니다. 기존 푸시 알림 시스템이 오래된 FCM Legacy API를 사용하고 있었는데, 이 방식이 앞으로는 권장되지 않거나 언제든 지원이 중단될 수 있다는 소식에 마음이 편치 않았습니다. 그래서 고심 끝에, 최신 FCM HTTP v1 API를 이용해 서버부터 Flutter 앱까지 푸시 알림 시스템 전체를 새롭게 구축하기로 결정했습니다. 아무것도 없는 백지상태에서 시작해야 했기에, 그 과정에서 꽤 많은 시행착오를 겪었지만, 덕분에 많은 것을 배울 수 있었네요. 이 글이 저와 비슷한 고민을 하고 계신 분들께 작은 길잡이가 되었으면 합니다.

이런 분께 — PHP 서버와 Flutter 앱으로 FCM HTTP v1 푸시 알림 시스템을 처음부터 구축하려는 개발자. · 난이도는 중급 정도

글의 요점

  • FCM HTTP v1 API를 사용하는 이유와 이점
  • PHP 서버에서 Firebase 서비스 계정을 이용한 인증 및 메시지 발송 방법
  • Flutter 앱에서 FCM 토큰 관리 및 포그라운드/백그라운드 메시지 수신 처리
  • 푸시 알림 시스템 구축 시 필요한 서버, 앱, DB, 관리자 UI 연동 과정
  • 실제 시스템 구축 중 발생할 수 있는 주요 문제점과 해결 과정

오래된 방식 대신 새로운 길을 택하며 겪은 고민

새로운 푸시 알림 시스템을 구축해야 한다는 과제를 받았을 때, 가장 먼저 마주한 것은 어떤 API를 사용할 것인가 하는 문제였습니다. 기존에 사용하던 FCM Legacy API는 분명 익숙했지만, Firebase 공식 문서에서는 이미 HTTP v1 API로의 전환을 강력히 권장하고 있었죠. Legacy API가 언제든 지원이 중단될 수 있다는 경고 문구가 계속 마음에 걸렸습니다. 당장은 큰 문제가 없어도, 몇 년 후에는 시스템을 다시 뜯어고쳐야 할 수도 있다는 생각이 들더군요. 그래서 눈앞의 편의보다는 장기적인 안정성과 확장성을 택해, 다소 생소했지만 최신 HTTP v1 API를 처음부터 적용하기로 마음먹었습니다.

이 결정은 새로운 학습 곡선을 의미했습니다. Legacy API는 간단한 키 기반 인증으로 메시지를 보낼 수 있었지만, v1 API는 OAuth 2.0 기반의 인증 절차를 요구했으니까요. 단순히 메시지 페이로드만 바꾸는 문제가 아니라, 서버 측에서 인증 토큰을 관리하고 갱신하는 로직까지 새로 만들어야 한다는 부담이 있었습니다. 또한, Flutter 앱에서도 기존 FCM 연동 방식을 최신 firebase_messaging 패키지에 맞춰 재정비해야 했습니다. 말 그대로 바닥부터 시작하는 셈이었지만, 한 번 제대로 구축해두면 오랫동안 안정적으로 사용할 수 있을 것이라는 기대로 차근차근 준비를 시작했습니다.

가장 먼저 고려한 것은 서버 측에서 Firebase 서비스 계정을 어떻게 활용할 것인가였습니다. 서비스 계정 JSON 파일을 PHP 서버에서 안전하게 관리하고, 이를 이용해 Google OAuth 2.0 액세스 토큰을 발급받는 과정이 핵심이었습니다. 처음에는 PHP에서 직접 JWT를 구성하여 액세스 토큰을 요청하는 방법을 생각했는데, 이는 생각보다 복잡하고 오류 가능성이 높아 보였습니다. 다행히 Google API 클라이언트 라이브러리가 PHP용으로 잘 준비되어 있어, 이를 활용하기로 방향을 잡았습니다. 이 라이브러리를 사용하면 복잡한 인증 절차를 비교적 쉽게 처리할 수 있겠더군요.

서버에서 FCM HTTP v1 API와 첫 대면하기: 인증의 벽

PHP 서버에서 FCM HTTP v1 API를 사용하기 위한 첫 관문은 바로 인증이었습니다. FCM 메시지 발송은 Firebase 프로젝트에 대한 권한이 필요하며, 이를 위해 OAuth 2.0 액세스 토큰을 받아야 했습니다. Firebase 서비스 계정 JSON 파일을 서버에 안전하게 업로드하고, 이 파일을 통해 토큰을 발급받는 것이 핵심 과정이었죠. 처음에는 Google_Client 클래스를 어떻게 초기화하고 사용할지 감이 잘 오지 않았습니다. 문서들을 찾아보면서 setAuthConfig 메소드를 통해 서비스 계정 파일을 지정하고, setScopes로 필요한 권한 범위를 설정해야 한다는 것을 알게 되었습니다.

특히 scopes 설정이 중요했는데, FCM 메시징을 위한 정확한 스코프인 'https://www.googleapis.com/auth/firebase.messaging'를 지정해야 했습니다. 만약 이 스코프를 잘못 지정하거나 누락하면, 토큰 발급은 성공하더라도 메시지 발송 API 호출 시 권한 오류가 발생하더군요. 몇 번의 시도 끝에 올바른 스코프를 찾아 적용하니, 비로소 fetchAccessTokenWithAssertion() 메소드를 통해 유효한 액세스 토큰을 받아낼 수 있었습니다. 이 토큰은 유효 기간이 정해져 있으므로, 실제 시스템에서는 토큰 만료 시 자동으로 갱신하는 로직을 추가해야 했습니다. 저는 토큰을 캐싱해두고 만료 직전에 갱신하는 방식을 채택했습니다.

아래는 PHP에서 OAuth 토큰을 발급받는 기본적인 예시입니다. 서비스 계정 JSON 파일 경로를 정확히 지정하는 것이 중요합니다. 이 과정이 성공적으로 이루어져야만 FCM HTTP v1 API를 호출할 수 있는 자격을 얻게 됩니다. 처음에는 이 인증 과정에서 시간을 많이 할애했는데, 결국은 라이브러리의 도움을 받아 해결할 수 있었습니다.

$client = new Google_Client();
$client->setAuthConfig('path/to/your-service-account.json');
$client->setScopes(['https://www.googleapis.com/auth/firebase.messaging']);
$accessToken = $client->fetchAccessTokenWithAssertion()['access_token'];
Enter fullscreen mode Exit fullscreen mode

이렇게 발급받은 액세스 토큰은 FCM 발송 요청의 Authorization 헤더에 Bearer 토큰으로 포함되어야 합니다.

메시지 발송 로직 구현: 헛다리와 실제 동작

액세스 토큰을 발급받는 데 성공했으니, 이제 실제 FCM 메시지를 발송할 차례였습니다. FCM HTTP v1 API는 https://fcm.googleapis.com/v1/projects/YOUR_PROJECT_ID/messages:send 엔드포인트를 사용하며, POST 방식으로 JSON 형태의 메시지 페이로드를 전송해야 합니다. 처음에는 Legacy API와 유사하게 단순한 notification 필드만으로 메시지를 구성했는데, 생각보다 복잡한 구조를 요구하더군요. message 객체 안에 token, notification, data 등의 필드를 계층적으로 구성해야 했습니다. 공식 문서를 꼼꼼히 살펴보며 JSON 구조를 맞춰나가는 데 시간이 좀 걸렸습니다.

특히, token 필드에 기기별 FCM 토큰을 정확히 넣어주는 것이 중요했습니다. 이 토큰이 없으면 어떤 기기로도 메시지가 전달되지 않으니까요. notification 필드는 알림창에 표시될 제목과 본문을 정의하고, data 필드는 앱에서 처리할 추가 데이터를 key-value 형태로 담는 데 사용됩니다. data 메시지는 앱이 백그라운드나 종료 상태일 때 notification 메시지와 함께 전달되거나, 포그라운드 상태일 때 앱 내에서만 처리되는 용도로 유용하게 쓰일 수 있습니다.

아래는 PHP에서 cURL을 이용해 FCM HTTP v1 메시지를 발송하는 예시입니다. Authorization 헤더에 앞서 발급받은 액세스 토큰을 포함하고, Content-Typeapplication/json으로 설정해야 합니다. 또한, YOUR_PROJECT_ID 부분은 실제 Firebase 프로젝트 ID로 교체해야 한다는 점을 잊지 말아야 합니다. 이 부분을 처음에는 프로젝트 이름으로 잘못 넣었다가 오류를 겪기도 했습니다.

$headers = ['Authorization: Bearer ' . $accessToken, 'Content-Type: application/json'];
$data = ['message' => ['token' => 'FCM_DEVICE_TOKEN', 'notification' => ['title' => '제목', 'body' => '내용']]];
$ch = curl_init('https://fcm.googleapis.com/v1/projects/YOUR_PROJECT_ID/messages:send');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_exec($ch);
Enter fullscreen mode Exit fullscreen mode

이렇게 코드를 작성하고 테스트 발송을 해보니, 드디어 첫 FCM 알림이 기기에 도착했습니다. 성공적인 발송을 확인한 후에는, 발송 결과를 데이터베이스에 로그로 남기는 로직을 추가하여 추후 문제 발생 시 추적할 수 있도록 대비했습니다.

Flutter 앱 연동 과정: 토큰 등록과 메시지 수신 확인

서버 측에서 메시지를 발송할 준비가 되었으니, 이제 Flutter 앱에서 이 메시지를 수신하고 처리할 차례였습니다. Flutter 앱에 FCM을 연동하기 위해서는 firebase_messaging 패키지를 사용해야 합니다. 먼저 main 함수에서 Firebase.initializeApp()를 호출하여 Firebase를 초기화하는 것이 필수적입니다. 이 과정이 누락되면 FCM 기능이 제대로 동작하지 않더군요. 또한, 앱이 실행될 때 기기의 고유한 FCM 토큰을 받아 서버에 등록하는 로직을 구현해야 했습니다.

FCM 토큰은 기기가 변경되거나 앱이 재설치될 때 등 여러 상황에서 갱신될 수 있기 때문에, FirebaseMessaging.instance.onTokenRefresh.listen() 메소드를 사용하여 토큰 갱신 이벤트를 감지하고, 갱신된 토큰을 즉시 서버에 업데이트하는 것이 중요했습니다. 만약 이 과정을 놓치면, 서버가 구형 토큰으로 메시지를 보내게 되어 알림이 도달하지 않는 문제가 발생할 수 있습니다. 저는 앱 실행 시 현재 토큰을 한 번 서버에 전송하고, 토큰 갱신 이벤트가 발생할 때마다 다시 전송하도록 구현했습니다.

메시지 수신 처리는 앱의 상태에 따라 다르게 접근해야 합니다. 앱이 포그라운드에 있을 때는 FirebaseMessaging.onMessage.listen()을 통해 메시지를 실시간으로 수신하고, 사용자에게 직접 알림을 보여주거나 앱 내에서 특정 동작을 수행할 수 있습니다. 앱이 백그라운드나 종료 상태일 때는 FirebaseMessaging.onBackgroundMessage() 핸들러를 등록하여 메시지를 처리하도록 했습니다. 이 백그라운드 핸들러는 앱이 완전히 종료된 상태에서도 메시지를 받아 처리할 수 있게 해주므로, 매우 중요한 부분입니다. 이 핸들러는 반드시 최상위 함수로 선언되어야 한다는 제약도 있었습니다.

Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  // 백그라운드 메시지 처리: 예를 들어, 로컬 알림을 띄우거나 데이터 업데이트 등
  await Firebase.initializeApp(); // 백그라운드에서도 Firebase 초기화 필요
  print('Handling a background message: ${message.messageId}');
}

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);

  // FCM 토큰 갱신 리스너
  FirebaseMessaging.instance.onTokenRefresh.listen((fcmToken) {
    print('FCM Token refreshed: $fcmToken');
    // 서버에 토큰 등록 API 호출: 이 부분을 구현해야 합니다.
    // 예를 들어, ApiService.registerFcmToken(fcmToken); 와 같이 호출합니다.
  });

  // 앱 시작 시 현재 FCM 토큰 가져와서 서버에 등록 (선택 사항이지만 권장)
  String? currentToken = await FirebaseMessaging.instance.getToken();
  if (currentToken != null) {
    print('Current FCM Token: $currentToken');
    // 서버에 토큰 등록 API 호출
  }

  // 포그라운드 메시지 처리
  FirebaseMessaging.onMessage.listen((RemoteMessage message) {
    print('Got a message whilst in the foreground!');
    print('Message data: ${message.data}');
    if (message.notification != null) {
      print('Message also contained a notification: ${message.notification!.title} / ${message.notification!.body}');
      // 로컬 알림을 띄우는 등의 처리
    }
  });

  runApp(const MyApp());
}
Enter fullscreen mode Exit fullscreen mode

이렇게 앱 코드까지 완성하고 나니, 이제 서버와 앱 간의 연동 준비가 거의 끝난 것 같았습니다. 마지막으로 Firebase 콘솔에서 APNs 인증 키를 등록하는 등, iOS 환경에서 푸시 알림이 제대로 동작하도록 몇 가지 수동 설정을 해주는 것도 잊지 않았습니다. 이 설정이 없으면 iOS 기기에서는 알림이 오지 않더군요.

견고한 시스템을 위한 뒷단 작업: DB와 관리 도구

서버와 앱 간의 기본적인 푸시 알림 연동은 마쳤지만, 실질적인 시스템으로 운영하기 위해서는 몇 가지 뒷단 작업이 더 필요했습니다. 가장 중요한 것은 바로 사용자 기기별 FCM 토큰을 저장하고 관리하는 데이터베이스 스키마와 API를 구축하는 것이었습니다. 토큰은 사용자별로 고유하며, 앱 설치나 재설치, 기기 변경 등 다양한 상황에서 갱신될 수 있으므로, 항상 최신 상태를 유지해야 했습니다. 저는 MySQL 데이터베이스에 사용자 ID와 FCM 토큰을 매핑하는 테이블을 만들고, 토큰이 갱신될 때마다 UPDATE 하거나, 새로운 기기에서 로그인 시 INSERT 하는 로직을 구현했습니다.

또한, 푸시 알림 발송 기록을 남기는 것도 중요했습니다. 어떤 알림을 언제, 누구에게 보냈는지, 그리고 그 결과는 어떠했는지 추적할 수 있어야 했기 때문입니다. 이를 위해 발송 로그 테이블을 별도로 구축하고, 메시지 제목, 내용, 대상 사용자, 발송 시간, 그리고 Firebase로부터 받은 발송 응답까지 기록하도록 했습니다. 이 로그는 나중에 알림 전달 문제를 디버깅하거나, 알림 효과를 분석하는 데 귀중한 자료가 됩니다.

마지막으로, 관리자가 직접 푸시 알림을 작성하고 발송할 수 있는 UI를 구축했습니다. 이 UI는 알림 제목과 내용을 입력하고, 특정 사용자 그룹이나 전체 사용자에게 알림을 보낼 수 있는 기능을 포함했습니다. 이 관리자 UI를 통해 테스트 알림을 쉽게 발송하고, 실제 운영 환경에서도 효율적으로 알림을 관리할 수 있도록 했습니다. UI에서 입력된 데이터는 앞서 만든 서버 API를 통해 FCM 발송 로직으로 전달되고, 발송 후에는 DB에 로그가 기록되는 전체 워크플로우를 완성한 셈입니다. 이처럼 서버, 앱, DB, 관리자 UI까지 전체적인 흐름을 한 번에 고려해야만 견고하고 사용하기 편리한 푸시 알림 시스템을 만들 수 있다는 것을 다시 한번 깨달았네요.

마무리 검증: 예상치 못한 상황과 최종 확인

모든 시스템 구축이 끝나고 나면, 이제 철저한 검증 단계가 남아 있었습니다. 단순히 알림이 한두 번 오는 것을 확인하는 것을 넘어, 다양한 시나리오에서 시스템이 안정적으로 동작하는지 확인해야 했죠. 저는 관리자 UI를 통해 여러 종류의 테스트 푸시 알림을 발송하며 개발 및 실제 디바이스에서 수신 여부를 확인했습니다. 특히 중요한 것은 앱의 상태에 따른 알림 수신이었습니다. 포그라운드, 백그라운드, 그리고 앱이 완전히 종료된 상태에서도 알림이 정상적으로 도착하는지 여러 번 확인했습니다.

초기에는 백그라운드 메시지 처리가 제대로 되지 않는 문제가 있었습니다. Flutter의 onBackgroundMessage 핸들러가 최상위 함수로 선언되지 않아 발생한 문제였는데, 이 부분을 수정하니 잘 동작했습니다. 또한, iOS 환경에서는 APNs 인증 키 설정이 제대로 되지 않아 알림이 오지 않는 경우가 있었는데, Firebase 콘솔에서 Apple 개발자 계정에서 발급받은 APNs 인증 키를 올바르게 등록하고, Firebase 프로젝트 설정에서 Bundle ID와 팀 ID를 정확히 입력하니 해결되었습니다. 이처럼 플랫폼별 특성을 고려한 설정이 중요하더군요.

데이터베이스에 발송 로그가 올바르게 기록되는지도 꼼꼼히 검증했습니다. 메시지 발송 후 DB를 확인하여, 발송 시간, 내용, 대상 등 모든 정보가 정확히 저장되어 있는지 확인했죠. 만약 발송은 되었는데 DB에 기록이 없다면 나중에 문제 추적이 어려워질 수 있습니다. 마지막으로, FCM 토큰 갱신 시 서버에 제대로 반영되는지도 확인했습니다. 앱을 삭제 후 재설치하거나, 다른 기기에서 로그인하는 등의 상황을 시뮬레이션하여 토큰이 변경되었을 때 서버의 DB에 최신 토큰이 정확히 업데이트되는지 확인하는 것이 중요했습니다. 이 모든 검증 과정을 거치고 나서야 비로소 FCM HTTP v1 푸시 알림 시스템이 안정적으로 작동할 준비가 되었다고 판단했습니다.

남는 이야기

FCM HTTP v1 API를 이용한 푸시 알림 시스템을 처음부터 구축하는 과정은 결코 쉽지 않았습니다. 특히 구형 API와는 다른 인증 방식과 메시지 페이로드 구조 때문에 초반에 헤매기도 했습니다. 하지만 서버의 인증 및 발송 로직, Flutter 앱에서의 토큰 관리와 메시지 수신 처리, 그리고 이를 뒷받침하는 데이터베이스 스키마와 관리자 UI까지 전체적인 그림을 완성하면서 많은 것을 배울 수 있었습니다. 이 과정에서 겪었던 시행착오와 해결 과정이, 혹시라도 저와 같은 길을 걷고 계실 다른 개발자분들에게 조금이나마 도움이 되었으면 하는 바람입니다. 최신 기술 스택을 적용하는 것은 때론 번거롭지만, 그만큼 더 안정적이고 확장성 있는 시스템을 만들 수 있다는 보람이 큰 작업이었습니다.

Top comments (0)