React Native로 앱을 개발하는 중 푸시 알림 구현에서 예상치 못한 문제에 직면했습니다. 처음에는 Expo의 푸시 알림 시스템을 사용하려 했으나, 백엔드 API가 FCM(Firebase Cloud Messaging) 토큰만 지원한다는 것을 알게 되었습니다.
Expo와 FCM의 차이점을 이해한 후, FCM으로 전환하기로 결정했습니다.
푸시 알림이 앱의 핵심 기능이라면 처음부터 FCM을 구현하는 것이 장기적으로 더 효율적이기 때문입니다.
그런데 중요한 것은 Expo의 편리함을 완전히 포기할 필요는 없다는 점입니다.
Managed Workflow를 유지하면서도 FCM을 사용할 수 있는 방법이 있습니다.
이 글에서는 Expo Managed Workflow에서 FCM으로 전환하는 과정과 그 과정에서 발생할 수 있는 문제점, 그리고 해결 방법을 공유하려 합니다.
먼저 Expo의 두 가지 개발 방식에 대해 이해할 필요가 있습니다:
expo start
, expo build
등의 간단한 명령어로 개발android/
, ios/
폴더) 직접 수정 가능ExponentPushToken[xxxxxx]
형식의 토큰 사용Expo Go에서 Firebase 모듈을 설치하고 사용하려고 할 때 다음과 같은 오류가 발생합니다:
Native module RNFBAppModule not found
이 오류는 Expo Go가 사전 빌드된 앱이라 네이티브 모듈을 추가로 설치할 수 없기 때문에 발생합니다. Expo Go는 제한된 네이티브 모듈만 포함하고 있으며, Firebase와 같은 추가 네이티브 모듈을 사용하려면 개발용 클라이언트(Development Client)를 빌드해야 합니다.
Expo의 Managed Workflow를 포기하지 않고도 FCM을 구현할 수 있습니다. 다음 단계를 따라하세요:
npx expo install expo-dev-client
expo-dev-client
는 Managed Workflow를 유지하면서도 네이티브 모듈을 사용할 수 있게 해주는 패키지입니다.
npx expo install @react-native-firebase/app @react-native-firebase/messaging
com.yourcompany.yourapp
)com.yourcompany.yourapp
)google-services.json
GoogleService-Info.plist
{
"expo": {
"name": "YourAppName",
"slug": "your-app-slug",
// 다른 설정들...
"android": {
"package": "com.yourcompany.yourapp",
"googleServicesFile": "./google-services.json"
},
"ios": {
"bundleIdentifier": "com.yourcompany.yourapp",
"googleServicesFile": "./GoogleService-Info.plist"
},
"plugins": [
"@react-native-firebase/app",
"@react-native-firebase/messaging"
]
}
}
이 설정은 EAS Build 시 Firebase 설정 파일을 올바른 위치에 배치하고 필요한 설정을 자동으로 수행합니다.
EAS(Expo Application Services)를 사용하여 개발용 클라이언트를 빌드합니다:
# EAS CLI 설치 (아직 설치하지 않았다면)
npm install -g eas-cli
# EAS 로그인
eas login
# EAS 프로젝트 구성
eas build:configure
# 개발 빌드 프로필 설정 (eas.json 파일에 다음 내용 추가)
# {
# "build": {
# "development": {
# "developmentClient": true,
# "distribution": "internal"
# }
# }
# }
# 개발용 클라이언트 빌드
eas build --profile development --platform all
또는 로컬 환경에서 직접 빌드하려면:
# Android
npx expo run:android
# iOS (Mac 필요)
npx expo run:ios
앱 코드에 FCM을 연동하는 기본적인 구현 예시:
// App.js 또는 적절한 컴포넌트에 추가
import messaging from '@react-native-firebase/messaging';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useEffect } from 'react';
import axios from 'axios'; // 또는 여러분의 API 클라이언트
export default function App() {
useEffect(() => {
// 앱 시작 시 푸시 알림 권한 요청
requestUserPermission();
// 포그라운드 메시지 핸들러
const unsubscribe = messaging().onMessage(async remoteMessage => {
console.log('Foreground message received:', remoteMessage);
// 여기서 알림 표시 로직 구현
});
// 컴포넌트 언마운트 시 구독 해제
return unsubscribe;
}, []);
// 푸시 알림 권한 요청 함수
async function requestUserPermission() {
const authStatus = await messaging().requestPermission();
const enabled = authStatus === messaging.AuthorizationStatus.AUTHORIZED ||
authStatus === messaging.AuthorizationStatus.PROVISIONAL;
if (enabled) {
console.log('Push notification authorization status:', authStatus);
getFCMToken();
}
}
// FCM 토큰 가져오기 및 서버에 등록
async function getFCMToken() {
try {
// 저장된 토큰 확인
let fcmToken = await AsyncStorage.getItem('fcmToken');
if (!fcmToken) {
// 토큰 새로 받기
fcmToken = await messaging().getToken();
if (fcmToken) {
await AsyncStorage.setItem('fcmToken', fcmToken);
}
}
console.log('FCM Token:', fcmToken);
// 서버에 토큰 등록
await registerTokenWithServer(fcmToken);
// 토큰 갱신 이벤트 처리
messaging().onTokenRefresh(async newToken => {
console.log('FCM token refreshed:', newToken);
await AsyncStorage.setItem('fcmToken', newToken);
await registerTokenWithServer(newToken);
});
} catch (error) {
console.error('Failed to get FCM token:', error);
}
}
// 서버에 토큰 등록하는 함수
async function registerTokenWithServer(token) {
try {
await axios.post('/device', {
fcmToken: token,
deviceType: Platform.OS.toUpperCase()
});
// 푸시 알림 설정 상태 저장
await AsyncStorage.setItem('notificationSettings', 'true');
console.log('FCM token registered with server');
} catch (error) {
console.error('Failed to register FCM token with server:', error);
}
}
// 나머지 앱 컴포넌트 코드...
}
백그라운드와 종료 상태의 알림을 처리하기 위해 index.js
파일을 수정합니다:
// index.js (또는 app.config.js, app.json)
import { registerRootComponent } from 'expo';
import App from './App';
import messaging from '@react-native-firebase/messaging';
// 백그라운드 메시지 핸들러 등록
messaging().setBackgroundMessageHandler(async remoteMessage => {
console.log('Background message received:', remoteMessage);
// 백그라운드 처리 로직
});
registerRootComponent(App);
문제: Expo Go에서 Firebase 모듈을 사용하려고 할 때 발생하는 오류
해결:
eas build --profile development --platform all
명령으로 개발용 클라이언트 빌드문제: messaging().getToken()
이 실패하거나 토큰을 반환하지 않는 경우
해결:
app.json
의 plugins 섹션에 Firebase 플러그인이 추가되었는지 확인문제: 서버에 FCM 토큰을 등록하려고 할 때 오류가 발생하는 경우
해결:
문제: 서버에서 알림을 보내도 앱에서 수신되지 않는 경우
해결:
Expo의 편리함 유지: expo
명령어, OTA 업데이트 등 Expo의 주요 기능을 계속 사용할 수 있습니다.
네이티브 모듈 사용 가능: Firebase, Bluetooth 등 네이티브 코드가 필요한 패키지를 사용할 수 있습니다.
간소화된 네이티브 설정: Expo의 config 플러그인 시스템을 통해 네이티브 설정을 자동화합니다.
EAS Build 활용: 로컬 환경 설정 없이도 클라우드에서 빌드할 수 있습니다.
점진적 전환 가능: 필요에 따라 Bare Workflow로 완전히 전환할 수도 있습니다.
Expo Managed Workflow를 사용하면서도 FCM 같은 네이티브 모듈을 활용하는 것은 충분히 가능합니다.expo-dev-client
를 통해 개발용 클라이언트를 빌드하면 Expo의 편리함을 유지하면서도 FCM을 사용할 수 있습니다.
앱의 핵심 기능으로 푸시 알림을 사용하는 경우, 이 방식은 Expo Go 환경에서의 제한을 극복하면서도 네이티브 코드를 직접 관리해야 하는 부담을 줄여줍니다. 처음에는 개발용 클라이언트 빌드 과정이 추가되어 약간의 시간이 소요되지만, 장기적으로는 더 안정적이고 확장 가능한 푸시 알림 시스템을 구축할 수 있습니다.
Expo Managed Workflow에 expo-dev-client
를 추가하는 것은 네이티브 기능이 필요한 앱을 개발할 때 가장 균형 잡힌 접근 방식이라고 할 수 있습니다. 이 방식을 통해 React Native 개발의 편리함과 네이티브 기능의 강력함을 모두 활용할 수 있습니다.