앱이 잘 돌아가고 사용자 로그인까지 끝난 시점이라면, 이제 제품 팀은 네이티브처럼 느껴지는 재참여 플로우를 원할 겁니다. 장바구니 리마인더, 리뷰 요청 prompt, 새 메시지 알림, 릴리스 공지 같은 것들이죠. 보통은 “그냥 push만 연결하면 되겠지”라고 생각하지만, 일주일 뒤에는 왜 어떤 기기에는 알림이 오고 어떤 기기에는 안 오는지, 시뮬레이터에서는 등록이 잘 되는 것처럼 보이는데 왜 탭해도 올바른 화면이 열리지 않는지 아무도 설명하지 못한 채 디버깅하고 있을 겁니다.
Expo push notifications는 바로 이 지점에서 반가울 만큼 간단하거나, 놀랄 만큼 취약합니다.
Expo는 React Native 팀에게 APNs와 FCM 위에 실용적인 레이어를 제공하며, 많은 팀이 Expo를 쓰는 이유도 바로 여기에 있습니다. 하지만 데모와 프로덕션 수준 구현 사이의 간극은 분명히 존재합니다. Token lifecycle, 권한 요청 타이밍, listener 설정, payload 설계, 백엔드 정리까지 모두 중요합니다. 앱 로직 변경을 자주 배포한다면 운영 관리의 중요성은 더욱 커지며, 특히 리텐션 작업이 안정적인 메시징과 릴리스 속도에 달려 있다면 더 그렇습니다. 메시지 전달은 그 주변 사용자 경험이 예측 가능할 때만 유용합니다.
목차
- Expo Push Notifications로 사용자 참여를 이끄는 기반
- 초기 프로젝트 설정 및 구성
- 권한 요청 및 푸시 토큰 캡처
- 서버에서 알림 보내기
- 앱에서 수신 알림 처리하기
- 프로덕션 베스트 프랙티스와 흔한 함정
Expo Push Notifications로 사용자 참여를 이끄는 기반
한 Expo 푸시 알림 Expo 푸시 알림 설정이 무엇보다 매력적인 이유는 팀이 첫날부터 떠안고 싶어 하지 않는 네이티브 메시징 복잡성을 크게 덜어주기 때문입니다. APNs와 FCM 연동을 직접 구축하는 대신 Expo 게이트웨이를 사용해 제품 동작, 라우팅, 권한 UX, 백엔드 메시지 로직에 집중할 수 있습니다.
이러한 추상화가 푸시 알림의 실효성을 떨어뜨리는 것은 아닙니다. 단지 엔지니어링 리소스를 어디에 투입할지를 바꿔줄 뿐입니다.
또한 서비스가 충분히 빨라 성능은 보통 가장 먼저 걱정할 부분이 아닙니다. 2023년 3월 14일부터 6월 12일까지 Expo 푸시 알림 API은 다음을 보여줬습니다: 중앙값 응답 시간 42ms, p99 지연 시간 273ms, 그리고 일일 평균 에러율 0.17% 전체 일일 수천만 건의 메시지이는 Knock의 Expo 푸시 API 벤치마크 분석. Expo가 프로토타입 전용인지 의문을 품는 팀이라면 안심할 수 있습니다.
Expo가 실제로 추상화하는 것들
팀에서 “Expo push”라고 말할 때는 보통 여러 가지 별개의 이슈가 함께 묶여 있습니다:
- Provider 라우팅: Expo가 메시지를 전달하는 대상: APNs 은 iOS용이며, FCM Android용.
- Token 형식: 플랫폼별 토큰 처리를 직접 관리하는 대신, 서버에서 Expo Push Token을 저장하고 전송합니다.
- Request 규약: 네이티브 프로바이더 API를 직접 통합하는 대신, Expo의 푸시 API로 페이로드를 POST합니다.
이 방식은 편리하지만 흔한 오해를 불러일으키기도 합니다. 팀들은 종종 모든 전송 문제를 Expo의 책임으로 돌리곤 합니다. 하지만 실제 실패 사례의 상당수는 앱 code, 오래된 토큰, 잘못된 형식의 페이로드 또는 미흡한 권한 요청 흐름에서 발생합니다.
실무 원칙: Expo를 탄탄한 클라이언트·백엔드 설계의 대체재가 아닌, 신뢰할 수 있는 전송 계층으로 보세요.
프로덕션 준비가 실제로 의미하는 것
작동하는 데모는 한 기기가 페이로드 하나를 한 번 받았다는 것만 증명합니다. 프로덕션 준비성은 다른 이야기입니다:
| 고려 사항 | 데모 중심 사고방식 | 프로덕션 중심 사고방식 |
|---|---|---|
| 권한 | 지금 바로 질문하기 | 사용자 가치가 분명해진 뒤, 맥락에 맞춰 질문하기 |
| 토큰 | 한 번만 저장 | 새로고침, 중복 제거, 만료, 재조정 |
| 페이로드 | 전부 넣기 data |
페이로드는 작게, 액션 중심으로 유지 |
| 앱 동작 | 알림 표시 | 올바른 네비게이션과 포그라운드 상태 처리 |
| 운영 | 수동 테스트 | 수신 확인, 정리, logs, 장애 대응 |
단순히 “알림을 보내는 것”과 “알림이 실제 제품 워크플로우를 지원하는 것”의 차이입니다.
초기 프로젝트 설정 및 구성
Expo 푸시 알림의 많은 문제는 첫 번째 권한 prompt가 뜨기도 전에 시작됩니다. 프로젝트 설정이 허술하면, 클라이언트 code는 정상처럼 보여도 빌드마다 앱 동작이 달라질 수 있습니다.

올바른 라이브러리를 설치하고 빌드 경로와 일치하는 개발 환경을 갖추는 것부터 시작하세요. Expo Go 이상의 환경에서 작업 중이라면 릴리스 워크플로우를 맞추는 것이 도움이 됩니다. Capgo의 Expo EAS 대안 가이드, 알림 동작은 간단한 샌드박스 실행보다 프로덕션에 더 가까운 빌드에서 검증해야 하는 경우가 많기 때문입니다.
알림 패키지 설치
최소한 다음과 같은 것들이 필요합니다:
expo-notifications권한 요청, token 가져오기, listener, 알림 표시를 위해 필요합니다.expo-devicetoken 가져오기는 반드시 다음으로 보호해야 하기 때문입니다.Device.isDevice.
일반적인 설치 명령어는 사용하는 package manager에 따라 다르지만, 핵심은 Expo SDK와의 버전 일치입니다. 임의의 패키지 버전을 섞어 쓰지 마세요. Expo가 호환되는 버전을 찾도록 하세요.
프로젝트 레벨 설정 추가
설정은 명시적으로 유지하세요. 최소한의 app.json 또는 app.config.js 알림이 나중에 덧붙이는 것이 아니라 앱 계약의 일부임을 반영해야 합니다.
{
"expo": {
"name": "MyApp",
"slug": "my-app",
"plugins": ["expo-notifications"],
"ios": {
"bundleIdentifier": "com.example.myapp"
},
"android": {
"package": "com.example.myapp"
},
"extra": {
"eas": {
"projectId": "your-project-id"
}
}
}
}
여기서 몇 가지 세부 사항이 중요합니다:
- Bundle identifier와 package name은 실제 배포하는 앱과 일치해야 합니다.
- 알림 플러그인은 빌드 시 네이티브 프로젝트에 필요한 설정이 적용되도록 합니다.
- EAS project ID는 토큰을 가져올 때 앱이 올바른 Expo 프로젝트와 연결되어 있어야 한다면 중요합니다.
알림 핸들러를 미리 설정하세요
많은 기본 튜토리얼은 알림 동작을 너무 늦게 정의합니다. 그렇게 하지 마세요. 앱 시작 시점 즈음에 설정해서 포그라운드 동작을 예측 가능하게 만드세요.
import * as Notifications from 'expo-notifications';
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: false,
shouldSetBadge: true,
}),
});
포그라운드 알림이 알림을 표시할지, 소리를 재생할지, 배지에 영향을 줄지는 팀이 결정합니다. 정확한 동작은 제품에 따라 다릅니다. 채팅 앱과 결제 앱이 같은 선택을 하지는 않습니다.
포그라운드 동작을 의도적으로 정의하지 않으면, 실제로는 수신되었지만 제품이 기대한 방식으로 표시되지 않은 '누락된' 알림을 결국 디버깅하게 됩니다.
Android 채널 설정이 필요합니다
실제로 Android 알림 채널은 선택 사항이 아닙니다. 설정을 건너뛰면 알림이 일관성 없어 보이거나 사용자 기대에 맞지 않을 수 있습니다.
import { Platform } from 'react-native';
import * as Notifications from 'expo-notifications';
export async function configureAndroidNotifications() {
if (Platform.OS !== 'android') return;
await Notifications.setNotificationChannelAsync('default', {
name: 'Default',
importance: Notifications.AndroidImportance.MAX,
});
}
앱 초기화 시점에 이 설정을 하세요. 그다음 채널 ID를 안정적으로 유지하세요. 함부로 변경하면 나중에 알림 동작을 파악하기 어려워집니다.
권한 요청 및 푸시 토큰 캡처
많은 팀이 스니펫을 그대로 복사하다가 나중에 후회하는 부분이 바로 여기입니다.
권한 요청은 타이밍, 플랫폼 인식, 그리고 체계적인 비동기 처리가 필요합니다. 토큰 캡처는 반드시 물리적 기기에서, 권한이 해결된 후에, 그리고 백엔드에 결과를 즉시 저장할 준비가 되었을 때만 수행해야 합니다.

기준으로 삼아야 할 클라이언트 함수
다음과 같은 함수를 시작점으로 사용하세요:
import * as Device from 'expo-device';
import * as Notifications from 'expo-notifications';
import Constants from 'expo-constants';
import { Platform } from 'react-native';
type RegisterResult =
| { ok: true; token: string }
| { ok: false; reason: string };
export async function registerForExpoPushNotificationsAsync(): Promise<RegisterResult> {
if (!Device.isDevice) {
return { ok: false, reason: 'Push notifications require a physical device.' };
}
if (Platform.OS === 'android') {
await Notifications.setNotificationChannelAsync('default', {
name: 'Default',
importance: Notifications.AndroidImportance.MAX,
});
}
const permissions = await Notifications.getPermissionsAsync();
let finalStatus = permissions.status;
if (finalStatus !== 'granted') {
const request = await Notifications.requestPermissionsAsync();
finalStatus = request.status;
}
if (finalStatus !== 'granted') {
return { ok: false, reason: 'Notification permission was not granted.' };
}
const projectId =
Constants.expoConfig?.extra?.eas?.projectId ??
Constants.easConfig?.projectId;
if (!projectId) {
return { ok: false, reason: 'Missing EAS project ID configuration.' };
}
const tokenResponse = await Notifications.getExpoPushTokenAsync({ projectId });
return { ok: true, token: tokenResponse.data };
}
순서가 중요합니다. 디바이스 타입을 먼저 확인하고, Android 채널 동작을 설정한 뒤, 권한을 처리하고 프로젝트 구성을 검증한 다음 Expo 토큰을 요청합니다.
이유 Device.isDevice 선택 사항이 아닙니다
이는 겉보기에는 무해해 보이지만 많은 혼란을 야기하는 몇 안 되는 실수 중 하나입니다. 숙련된 팀은 다음과 같은 경우에만 조건부로 권한을 요청합니다. Device.isDevice true인 경우, 그리고 그 가드를 건너뛰는 것은 흔한 함정입니다. 이는 개발자가 잘못된 시뮬레이터 토큰으로 알림을 보내고, 실제 문제는 앱 설정인데도 Expo를 탓하게 만듭니다. 이는 다음과 같이 설명되어 있습니다. Eagerworks의 Expo 알림 구현 노트.
그래서 이 체크는 함수 맨 위에 있습니다. 헬퍼 뒤에 숨기지 말고, 분명하게 드러내세요.
시뮬레이터 결과는 UI 테스트에는 유용하지만, 푸시 토큰 등록을 검증하는 용도로는 신뢰할 수 없습니다.
적절한 시점에 권한 요청하기
스플래시 화면에서 바로 요청하지 마세요. 사용자가 가치를 이해하기도 전에 요청하지 마세요. 가장 좋은 타이밍은 배송 업데이트 활성화, 대화 참여, 관심 항목 저장과 같이 알림의 이점이 구체화되는 사용자 액션 직후입니다.
좋은 구현은 보통 다음 흐름을 따릅니다:
- 사용자가 의미 있는 기능 경계에 도달합니다.
- 앱이 자체 UI에서 알림의 가치를 설명합니다.
- 앱이 시스템 권한을 요청합니다.
- 권한이 허용되면 앱이 즉시 백엔드에 토큰을 저장합니다.
많은 앱이 바로 이 마지막 단계에서 실패합니다. 토큰을 가져와 로컬에 로그를 남기고 백엔드 등록을 미룹니다. 나중에 지원팀이 특정 시점에 어떤 기기에 어떤 토큰이 있었는지 파악할 수 없게 됩니다.
등록 후 토큰을 저장하는 간단한 예시입니다:
export async function enablePushForCurrentUser(userId: string) {
const result = await registerForExpoPushNotificationsAsync();
if (!result.ok) {
return result;
}
await fetch('https://api.example.com/push-tokens', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer user-session-token',
},
body: JSON.stringify({
userId,
token: result.token,
platform: Platform.OS,
}),
});
return result;
}
워크플로우 후반부에서 이 가이드는 유용한 시각적 참고 자료로 활용됩니다:
릴리스가 잦은 앱을 개발하는 팀이라면 토큰 등록을 온보딩의 일부가 아니라 앱의 운영 상태의 일부로 보는 것이 좋습니다. 이러한 사고방식은 더 넓은 Expo 앱 배포 워크플로우와 잘 맞습니다. 앱 동작이 자주 변하고 백엔드 상태가 동기화되어야 하는 환경이죠.
서버에서 알림 보내기
백엔드에 유효한 Expo Push Token이 있으면 알림 전송은 간단합니다. 어려운 부분은 요청 자체가 아니라, 페이로드에 무엇을 포함할지와 클라이언트 상태에 얼마나 신뢰를 둘지 결정하는 것입니다.
다음은 Node 스타일을 사용하는 최소 예시입니다. fetch:
type ExpoPushMessage = {
to: string;
title: string;
body: string;
sound?: 'default' | null;
data?: Record<string, unknown>;
};
export async function sendExpoPushNotification(token: string) {
const message: ExpoPushMessage = {
to: token,
title: 'New review received',
body: 'Tap to open the order details.',
sound: 'default',
data: {
type: 'new_review',
orderId: 'ord_123',
screen: 'OrderDetails',
},
};
const response = await fetch('https://exp.host/--/api/v2/push/send', {
method: 'POST',
headers: {
Accept: 'application/json',
'Accept-encoding': 'gzip, deflate',
'Content-Type': 'application/json',
},
body: JSON.stringify(message),
});
const result = await response.json();
return result;
}
각 페이로드 필드의 역할
페이로드를 아무 데이터나 넣는 용도로 쓰지 마세요. 각 필드는 명확한 목적을 가져야 합니다.
| 필드 | 목적 | 실무 팁 |
|---|---|---|
to |
대상 Expo Push Token | 현재 디바이스 레코드에 속하는지 확인하세요 |
title |
알림 제목 | 짧고 읽기 쉽게 작성하세요 |
body |
주요 표시 텍스트 | 수행할 동작을 명확히 하세요 |
sound |
시스템 사운드 동작 | 중요 알림에만 제한적으로 사용 |
data |
앱 전용 메타데이터 | 리치 콘텐츠보다 ID와 라우트 힌트를 권장 |
이 data object는 제품 워크플로우가 실제로 유용해지는 부분입니다. type과 record ID를 전달하면 사용자가 탭할 때 앱이 최신 데이터를 가져오게 할 수 있습니다. 이는 큰 데이터나 민감한 데이터를 페이로드에 직접 넣는 것보다 안전합니다.
페이로드는 작고 단순하게 유지하세요
에 따르면 Courier의 Expo 알림 가이드, Expo Push Tokens는 일시적인 것으로 취급해야 하며, 약 4 KB 를 초과하는 페이로드는 누락될 수 있습니다. 따라서 다음과 같은 작은 메타데이터 페이로드를 보내는 것이 안정적인 패턴입니다. { "type": "new_review", "id": 123 } 대용량 JSON이나 인라인 미디어 대신 작은 페이로드를 쓰라는 조언은 실제 시스템에서 통하는 방식과 맞습니다. 작은 페이로드는 실패가 적고 앱 로직이 바뀌어도 더 잘 견딥니다.
사용자를 라우팅할 만큼의 데이터만 보내세요. 나머지는 앱이 열린 뒤 가져오세요.
유용한 서버 측 습관
테스트용으로는 기본 전송 함수면 충분합니다. 프로덕션용에는 보통 몇 가지 역할이 더 추가됩니다:
- 전송 시도 저장: 사용자 ID, 토큰, 페이로드 타입, 타임스탬프와 함께 알림 전송 의도를 저장하세요.
- 콘텐츠 생성과 전송 분리: 한 레이어에서는 메시지 문구를 만들고, 다른 레이어에서는 Expo API 요청을 만드세요.
- 무효화 피드백 처리: Expo가 나중에 보고하면
DeviceNotRegistered, 해당 토큰을 만료 상태로 표시하고 무작정 재시도하지 마세요. - webhook 친화적인 설계 사용: 이미 시스템에서 이벤트를 발행하고 있다면, 알림 트리거도 기존에 사용하던 방식과 동일한 다른 곳에서 쓰는 백엔드 webhook 처리 패턴으로 라우팅하면 됩니다.
클라이언트 리스너를 디버깅하기 전에 먼저 수동 테스트 푸시를 보내보세요. 토큰이 아주 작은 페이로드의 일반 알림을 정상적으로 받는다면 전송 경로는 문제가 없는 것입니다. 그렇지 않다면 navigation code를 수정하는 것부터 시작하지 마세요. 먼저 토큰, 페이로드 구조, 권한 상태부터 확인해야 합니다.
앱에서 수신 알림 처리하기
알림 전달은 기능의 절반에 불과합니다. 알림이 도착했을 때와 사용자가 알림을 탭했을 때 앱이 일관되게 동작해야 합니다.
즉, 다음 두 가지 순간을 각각 처리해야 합니다:
- 앱이 열려 있는 상태에서 알림이 도착할 때
- 사용자가 시스템 트레이나 잠금 화면에서 알림과 상호작용할 때

포그라운드 수신과 사용자 응답은 서로 다른 이벤트입니다
안정적인 설정을 위해서는 보통 두 가지 리스너가 모두 필요합니다:
import { useEffect } from 'react';
import * as Notifications from 'expo-notifications';
export function useNotificationObservers(
onForegroundMessage: (notification: Notifications.Notification) => void,
onNotificationTap: (response: Notifications.NotificationResponse) => void
) {
useEffect(() => {
const receivedSub = Notifications.addNotificationReceivedListener(
(notification) => {
onForegroundMessage(notification);
}
);
const responseSub = Notifications.addNotificationResponseReceivedListener(
(response) => {
onNotificationTap(response);
}
);
return () => {
receivedSub.remove();
responseSub.remove();
};
}, [onForegroundMessage, onNotificationTap]);
}
addNotificationReceivedListener 앱이 활성 상태일 때 실행됩니다. addNotificationResponseReceivedListener 사용자가 도착한 알림을 탭했을 때 실행됩니다. 이 둘을 혼동하지 마세요. 서로 다른 UX 경로를 담당합니다.
데이터 페이로드를 읽고 의도적으로 네비게이션하기
탭 핸들링을 위한 실용적인 패턴은 다음과 같습니다:
type NotificationData = {
type?: string;
orderId?: string;
screen?: string;
};
export function handleNotificationTap(
response: Notifications.NotificationResponse,
navigation: any
) {
const data =
response.notification.request.content.data as NotificationData;
if (data.screen === 'OrderDetails' && data.orderId) {
navigation.navigate('OrderDetails', { orderId: data.orderId });
return;
}
if (data.type === 'new_review') {
navigation.navigate('Inbox');
return;
}
navigation.navigate('Home');
}
이 패턴은 페이로드에 전체 문서가 아닌 라우팅 힌트만 담기 때문에 견고합니다. 알림 전송 후 주문이 변경되었더라도 앱은 화면 이동 후 서버의 최신 상태를 가져올 수 있습니다.
알림을 탭하면 명확한 목적지로 이동해야 합니다. fallback 경로가 모호하면 사용자는 바로 알아차립니다.
포그라운드 동작은 사용자 컨텍스트와 일치해야 합니다
앱이 이미 열려 있는 상태에서 시스템 스타일 알림을 무작정 띄우는 것은 어색할 수 있습니다. 때로는 인앱 배너, 배지 업데이트 또는 조용한 새로고침이 더 나은 선택입니다. 사용자가 이미 해당 대화를 읽고 있다면 고객 지원 인박스 화면에는 눈에 보이는 알림이 필요 없을 수 있습니다.
따라서 포그라운드 리스너는 라우트와 알림 유형에 따라 분기 처리해야 합니다. 예시는 다음과 같습니다:
- 채팅 화면이 열려 있는 경우: 메시지를 추가하고 중복 배너 노출을 방지합니다.
- 대시보드가 열려 있는 경우: 가벼운 인앱 토스트 표시
- 중요 계정 이벤트: 더 강한 UI 표현 노출
간단한 접근 방식은 다음과 같습니다:
export function handleForegroundNotification(
notification: Notifications.Notification,
currentRouteName: string
) {
const data = notification.request.content.data as { type?: string };
if (currentRouteName === 'ChatThread' && data.type === 'new_message') {
// refresh local thread state
return;
}
// otherwise show your own in-app UI or update badges
}
앱에서 이러한 컨텍스트를 구분하지 않으면, 기술적으로 알림이 제대로 전달되더라도 사용자는 더 빨리 알림 피로를 느끼게 됩니다.
프로덕션 베스트 프랙티스와 흔한 함정
대부분의 Expo 푸시 알림 설정이 제대로 작동하지 않는 이유는 Expo가 너무 제한적이어서가 아닙니다. 팀이 토큰이 영구적이고, 페이로드에 무엇이든 담을 수 있으며, 앱 업데이트가 알림 로직에 영향을 주지 않는다고 가정하기 때문입니다.
그 가정은 프로덕션에서 통하지 않습니다.

토큰은 일시적일 뿐, 신원 기록이 아닙니다
Expo Push Token은 평생 유지되는 기기 식별자가 아니라 임대 계약처럼 취급하는 것이 좋습니다. 토큰은 재설치, OS 변경 또는 기타 라이프사이클 이벤트 후에 교체될 수 있습니다. 만약 토큰이 결국 다시 돌아온다면 DeviceNotRegistered, 백엔드에서는 해당 토큰을 더 이상 활성 상태로 취급해서는 안 됩니다.
실용적인 백엔드 모델은 다음을 저장합니다:
- 사용자 ID
- 플랫폼
- 설치별 메타데이터
- 현재 토큰
- 마지막 확인 타임스탬프
- active, stale, revoked 등의 상태
유저 테이블에 토큰 필드 하나만 저장하고 끝내지 마세요. 유저는 여러 기기를 사용하고, 기기 상태는 계속 바뀝니다.
리프레시 전략은 대부분의 튜토리얼이 말하는 것보다 더 중요합니다
공식 생태계 가이드는 이 부분에 실질적인 운영 공백을 남깁니다. 기존 Expo 푸시 알림 콘텐츠는 토큰 유효성을 유지하는 방법을 자주 설명하지 않습니다. App Store 심사 사이클 및 OTA 업데이트이는 라이브 변경 사항을 배포하는 팀에 특히 중요합니다. 푸시 신뢰성은 최신 토큰 상태와 백엔드 동기화에 달려 있기 때문입니다. 이는 다음에서도 언급됩니다: Expo 알림 문서.
이는 리프레시 트리거 설계 방식에 영향을 줍니다. 토큰 상태를 맞춰야 할 좋은 시점은 다음과 같습니다:
- 업데이트 후 앱 실행 시
- 유저 로그인 시
- 권한 설정 변경 시
- 릴리스 프로세스에서 자격 증명 로테이션 작업을 할 때
- 푸시 관련 지원 티켓 발생 후 복구 플로우를 진행할 때
보안과 컴플라이언스는 스프린트 마지막에 처리할 일이 아닙니다
많은 Expo 튜토리얼은 작동 방식에만 집중하고 운영 리스크는 건너뜁니다. 취미용 앱이라면 괜찮습니다. 하지만 헬스케어 인접 분야, 핀테크, 규제 대상 커머스 제품에는 적합하지 않습니다.
Courier가 엔터프라이즈 관점에서 짚은 Expo 알림 공백 동의 로깅, 감사 추적, 민감한 페이로드 노출 최소화에 대한 실무 가이드가 부족하다는 점을 지적합니다. 엔지니어링 관점의 핵심은 단순합니다:
- 민감한 비즈니스 데이터를 알림 텍스트나 페이로드 메타데이터에 넣지 마세요.
- 동의 변경 사항은 서버 측에서 로깅하세요.
- 어떤 알림 의도가 어떤 토큰으로 전송됐는지 기록하세요.
- 페이로드에는 ID를 사용하고, 앱을 연 뒤 보호된 콘텐츠를 가져오세요.
릴리스 운영을 더 넓은 앱 스토어 컴플라이언스, API 보안 관행에 맞춰 나가는 팀이라면, 푸시도 인증, 분석, 백엔드 이벤트 로깅과 동일한 리뷰 원칙 안에 넣어야 합니다.
푸시 알림은 사용자 대상 메시지지만, 동시에 분산 시스템 문제이기도 합니다. 인증 상태와 결제 이벤트에 적용하는 것과 동일한 수준의 주의를 기울이세요.
보통 잘 작동하는 것과 자주 깨지는 것
| 보통 작동함 | 보통 깨짐 |
|---|---|
| 명확한 가치 설명 후 권한 요청 | 첫 프레임에서 프롬프트 표시 |
| 실제 기기에서 테스트하기 | 시뮬레이터 등록을 신뢰하기 |
| 기기 컨텍스트와 함께 토큰 저장하기 | 사용자 레코드당 하나의 토큰 |
| 작은 메타데이터 페이로드 전송 | 대용량 또는 민감한 blob 포함하기 |
| 포그라운드 및 탭 이벤트 분리 처리 | 모든 알림이 단일 경로를 따른다고 가정 |
| 오래된 토큰을 적극적으로 만료시키기 | 무효 토큰을 무한히 재시도하기 |
견고한 Expo 푸시 설정은 복잡하지 않습니다. 원칙을 지키는 것이 핵심입니다.
팀에서 앱 로직을 자주 업데이트하며 릴리스 동작, 롤백, 배포 가시성을 더 정밀하게 제어해야 한다면, Capgo 한 번 살펴볼 가치가 있습니다. 스토어 리뷰를 기다리지 않고 모바일 팀이 빠르게 업데이트를 배포할 수 있어, 알림 플로우, 라우팅 로직, 클라이언트 측 수정 사항이 사용자에게 신속하게 전달되어야 할 때 특히 유용합니다.