Intro
- 로그인을 기준으로 푸시 알림 초기화를 제어하지 않으면 알림이 뒤늦게 도착하거나 아예 빠지는 일이 잦았어요.
- 저는 Expo 기반 프로젝트에서 토큰 등록과 라우팅까지 한 번에 묶어 지연과 중복을 없애는 루틴을 만들었습니다.
핵심 아이디어 요약
- 로그인 여부를 확인한 뒤에만 Expo 토큰을 발급하고, Supabase에 디바이스 정보를 멱등하게 업서트합니다.
- iOS/Android별 권한 체계와 알림 채널을 분기해 초기화 타이밍을 안정화합니다.
- 포그라운드/탭 이벤트를 감지해 뱃지 카운트를 즉시 동기화하고 알림 페이로드를 라우터로 넘깁니다.
준비와 선택
- Expo SDK를 쓰고 있어서
expo-notifications, expo-constants, expo-device를 그대로 활용했습니다.
- 백엔드와 토큰을 공유하기 위해 Supabase 서비스 모듈을 호출하는 구조를 유지했습니다.
- 딥링크는
expo-router로 처리하고 있어 router.replace로 알림 탭 시 이동하도록 설계했습니다.
구현 여정
- 세션 선 확인:
supabase.auth.getSession()을 먼저 호출해 로그인 상태가 아니면 루틴 전체를 중단하고 뱃지를 0으로 초기화했습니다.
- 알림 채널과 권한 셋업: Android에서는 채널을 직접 생성하고, iOS에서는 세분화된 권한 상태(
AUTHORIZED, PROVISIONAL, EPHEMERAL)를 다시 점검했습니다.
- 프로젝트 ID와 토큰 발급:
Constants.easConfig.projectId가 없을 때를 대비해 expoConfig.extra.eas.projectId까지 탐색했습니다.
- 디바이스 메타 업서트:
Device.deviceName, Device.osVersion 등 네이티브 정보를 함께 등록해 서버가 푸시 실패를 추적하기 쉬워졌습니다.
- 알림 리스너: 수신/탭 이벤트 모두에서
updateBadgeCount를 호출해 뱃지를 맞추고, 커스텀 라우팅 함수로 화면 이동을 처리했습니다.
export function usePushNotificationsSetup() {
const lastRegisteredTokenRef = useRef<string | null>(null);
const [isLoggedIn, setIsLoggedIn] = useState(false);
useEffect(() => {
const supabase = getSupabase();
(async () => {
const res = await supabase.auth.getSession();
setIsLoggedIn(Boolean(res?.data?.session?.user?.id));
})();
}, []);
useEffect(() => {
if (Platform.OS !== 'ios' && Platform.OS !== 'android') return;
if (!isLoggedIn) return;
(async () => {
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldPlaySound: false,
shouldSetBadge: false,
shouldShowBanner: true,
shouldShowList: true,
}),
});
if (Platform.OS === 'android') {
await Notifications.setNotificationChannelAsync('default', {
name: 'default',
importance: Notifications.AndroidImportance.MAX,
});
}
const { status } = await Notifications.requestPermissionsAsync({
ios: { allowAlert: true, allowBadge: true, allowSound: true },
});
if (status !== 'granted') return;
const projectId = Constants.easConfig?.projectId;
if (!projectId) return;
const tokenData = await Notifications.getExpoPushTokenAsync({ projectId });
lastRegisteredTokenRef.current = tokenData.data;
await registerUserPushToken({
token: tokenData.data,
provider: 'expo',
projectId,
deviceName: Device.deviceName ?? null,
appVersion: Constants.nativeAppVersion ?? null,
deviceInfo: { osName: Device.osName ?? undefined },
});
})();
const receivedSub = Notifications.addNotificationReceivedListener(
async () => updateBadgeCount(),
);
const responseSub = Notifications.addNotificationResponseReceivedListener(
async response => {
await updateBadgeCount();
const payload = response.notification.request.content.data as NotificationPayload;
const { route } = resolveNotificationRoute(payload);
router.replace(route);
},
);
return () => {
receivedSub.remove();
responseSub.remove();
};
}, [isLoggedIn]);
}
결과와 회고
- 로그인 후 약 2초 안에 모든 디바이스에서 토큰이 등록되고, 서버 로그상 토큰 중복이 사라졌습니다.
- 안내 배너를 숨기고 있었던 알림도 즉시 라우팅되면서 QA 팀이 테스트를 훨씬 빠르게 끝낼 수 있었습니다.
- 다음엔 백엔드에서 토큰 중복을 더 정교하게 정리하고, 앱 내에서 실패 원인을 Toast로 바로 보여주려 합니다.
- 여러분 팀은 Expo 푸시 등록 과정에서 어떤 문제를 자주 만났나요? 댓글로 경험을 알려주시면 감사하겠습니다.
Reference