blog

Expo + FCM으로 Android 푸시 알림 설정하기

Front-end / Mobile#Expo#React Native#FCM#Firebase#EAS#푸시 알림

Expo SDK 54 기준으로, WebView 기반 앱에서 Android 푸시 알림을 붙인 과정이다.

  • 전송 경로: 서버 → Expo Push Service → FCM HTTP v1 → Android 기기
  • 패키지: expo-notifications, expo-device, expo-constants
웹(WebView)
  └─ bridge.requestPushPermission()
       └─ requestPushNotificationPermission()
            └─ Expo Push Token 발급 → 서버에 저장
                 └─ 서버가 Expo Push API 호출 → FCM v1 → 기기

초기 설정

1. Firebase 프로젝트에 Android 앱 추가

  1. Firebase Console에서 프로젝트를 만들거나 선택한다.
  2. 프로젝트 설정 → 일반 → Android 앱 추가. 패키지 이름은 app.config.js의 android.package와 같아야 한다(예: com.example.app).
  3. google-services.json을 내려받는다.

2. google-services.json을 EAS 환경 변수로 관리

이 파일은 .gitignore에 넣고, EAS에 파일 타입 환경 변수로 등록한다.

eas env:create
# name: GOOGLE_SERVICES_JSON
# type: file
# visibility: secret
# environment: development (필요한 환경 선택)

EAS Build는 Git에 커밋된 파일만 업로드하므로, 커밋하지 않은 google-services.json을 경로로 지정하면 빌드에서 찾지 못한다. 파일 타입 환경 변수는 빌드 서버에 파일을 만들고 그 경로를 변수에 넣어 준다.

// app.config.js
export default {
  expo: {
    android: {
      package: 'com.example.app',
      googleServicesFile: process.env.GOOGLE_SERVICES_JSON ?? './google-services.json',
    },
  },
};

app.json은 정적 JSON이라 process.env를 읽을 수 없다. 환경 변수를 쓰려면 app.config.js(또는 .ts)로 바꿔야 한다.

3. FCM v1 서비스 계정 키 등록

Expo 서버가 FCM으로 알림을 보내려면 Firebase 서비스 계정 키가 필요하다.

  1. Firebase Console → 프로젝트 설정 → 서비스 계정 → 새 비공개 키 생성
  2. 키를 EAS에 등록한다.
eas credentials
# Android → Google Service Account
# → Manage your Google Service Account Key for Push Notifications (FCM V1)
# → Set up ... → 내려받은 JSON 경로 입력

이 키는 프로젝트에 대한 관리자 권한을 가지므로 저장소에 커밋하지 않는다.

4. 개발 빌드

푸시 알림은 Expo Go가 아니라 개발 빌드(development build) 에서 테스트한다.

eas build --profile development --platform android

권한 요청과 토큰 발급

import Constants from 'expo-constants';
import * as Device from 'expo-device';
import * as Notifications from 'expo-notifications';
import { Platform } from 'react-native';

export async function requestPushNotificationPermission() {
  if (!Device.isDevice) throw new Error('실제 기기에서만 푸시를 사용할 수 있습니다.');

  // Android 13+는 채널이 있어야 권한 요청 팝업이 뜬다
  if (Platform.OS === 'android') {
    await Notifications.setNotificationChannelAsync('default', {
      name: 'default',
      importance: Notifications.AndroidImportance.MAX,
    });
  }

  const { status: existing } = await Notifications.getPermissionsAsync();
  const { status } =
    existing === 'granted' ? { status: existing } : await Notifications.requestPermissionsAsync();
  if (status !== 'granted') throw new Error('알림 권한이 거부되었습니다.');

  const projectId = Constants.expoConfig?.extra?.eas?.projectId;
  const { data: token } = await Notifications.getExpoPushTokenAsync({ projectId });
  return token; // ExponentPushToken[...]
}

웹뷰 브릿지에서는 성공 시 { success: true, token }, 실패 시 { success: false, error } 형태로 웹에 결과를 돌려주었다.

전송 테스트

curl -X POST https://exp.host/--/api/v2/push/send \
  -H "Content-Type: application/json" \
  -d '{
    "to": "ExponentPushToken[YOUR_TOKEN_HERE]",
    "title": "테스트 알림",
    "body": "알림이 잘 오나요?"
  }'

응답의 {"data": {"status": "ok", "id": "..."}}는 Expo가 요청을 접수했다는 뜻이다. 실제 전달 결과는 이 id로 push receipt API를 조회해서 확인한다(FCM 자격 증명 오류도 receipt에서 드러난다).

Firebase 프로젝트를 바꿀 때

  1. 새 google-services.json으로 EAS 환경 변수 GOOGLE_SERVICES_JSON 값을 갱신한다(eas env:update).
  2. eas credentials에서 FCM v1 서비스 계정 키를 새 프로젝트의 키로 교체한다.
  3. 다시 빌드한다. google-services.json은 앱 바이너리에 포함되므로 재빌드 없이는 바뀌지 않는다.

트러블슈팅

에러원인해결
Default FirebaseApp is not initialized빌드에 google-services.json이 포함되지 않음환경 변수 등록 후 재빌드
open '$GOOGLE_SERVICES_JSON'app.json에서 환경 변수 치환 시도app.config.js로 전환
google-services.json is missing (EAS Build)Git에 없는 파일을 경로로 지정파일 타입 EAS 환경 변수 사용
Unable to retrieve the FCM server key (receipt)FCM v1 서비스 계정 키 미등록eas credentials에서 등록