모바일 앱 스캐폴딩부터 FCM, 그리고 웹 푸시까지
Design 단계에서 이미 구멍을 발견
mobile-app Plan/Design 문서를 쓰다가 UserSettings.kt를 확인해보니 fcm_tokens 컬럼은 있는데 이걸 등록할 API가 없었다. "backend 변경 0"이라던 Plan의 가정을 PUT /api/settings에 선택적 fcmToken 필드를 추가하는 최소 확장으로 수정했다.
Expo 스캐폴딩
create-expo-app 기본 템플릿(SDK 57)에서 데모 콘텐츠를 걷어내고 NativeWind v4를 설정했다. 이어서 fcmToken 필드를 백엔드에 추가하고, 원시 FCM 토큰에 직접 보내는 방식(Firebase Admin SDK)에서 Expo Push Service로 전환했다 — iOS(APNs)와 Android(FCM) 토큰 형식이 달라서, 원시 방식으로 iOS까지 받으려면 react-native-firebase와 커스텀 개발 빌드가 필요했기 때문이다(Expo Go로는 불가능). FcmSender 인터페이스는 그대로 두고 구현체만 교체해서 PushService 쪽 변경은 없었다.
브리핑 홈 화면(요약 카드+바텀시트+오프라인 캐시), 설정 화면(수신 시간+관심 키워드)까지 붙이고 나니 모바일 코드는 다 끝났는데, 실제 알림을 받아보려니 사용자의 EAS 프로젝트 연결이 필요해서 거기서 잠시 멈췄다.
폴링으로 바꾼 이유
그러다 발견한 버그 — Settings 화면의 "수신 시간"이 저장만 될 뿐 어디서도 읽히지 않아서, 항상 고정 08:00 KST에만 발송되고 있었다. GitHub Actions를 15분마다 폴링하도록 바꾸고, 새 trigger-if-due 엔드포인트가 현재 시각과 저장된 pushTime을 비교해 맞을 때만 실제로 파이프라인을 실행하게 했다.
EAS 연결과 로고
디스크 공간 부족으로 막혀 있던 EAS 연결(npx eas init)이 공간을 확보하고 나서야 풀렸다 — 원인 진단에 제법 걸렸다. FireWatch 로고를 모바일 아이콘·웹 파비콘에 반영하고, Android 적응형 아이콘은 중앙 62% 안에 들어가도록 여백을 두고 재처리했다(sharp). APK는 Expo 빌드 아티팩트가 14일 후 만료되길래 GitHub Release에 영구 링크로 올렸다.
그래도 안 되면 웹 푸시
Android 사이드로드 APK가 Play Protect에 막혀서, 앱 설치 없이 브라우저로 알림을 받는 웹 푸시 채널을 추가로 붙였다. web-push 라이브러리(VAPID 서명 + RFC 8291 암호화)로 브라우저 구독에 직접 발송한다. FCM 토큰과 웹 푸시 구독을 각각 다른 채널로 두고, PushService가 둘 다 시도해서 한쪽이 비어있어도 나머지로는 발송되게 했다. Web Push는 구독자마다 키가 달라 멀티캐스트가 안 되기 때문에 WebPushSender 인터페이스를 FcmSender와 분리했다.
6일간의 기록
8/19 계획부터 8/24 웹 푸시까지, FireWatch는 나흘 반 만에 백엔드-웹-모바일-알림 채널까지 갖춘 개인 프로젝트가 됐다. bkit PDCA로 미리 설계 결정을 문서화해둔 덕에, 무료 티어 제약(Gemini 할당량, Render 콜드스타트, $0 원칙)에 부딪힐 때마다 ADR로 근거를 남기면서 방향을 바꿀 수 있었다.