앱이 실행 중이 아니어도 서버가 기기에 메시지를 보낼 수 있게 해 주는 구글의 푸시 서비스.
왜 앱이 직접 연결을 유지하지 않나
앱마다 서버와 연결을 붙들고 있으면 그 수만큼 소켓이 살아 있어야 하고, 각자 주기적으로 신호를 보내야 한다. 연결 하나하나가 라디오를 깨우므로 배터리가 빠르게 준다.
FCM 은 시스템이 연결 하나만 유지하고 모든 앱의 메시지를 그 통로로 받는다. 도착하면 해당 앱을 깨워 전달한다. 앱 백 개가 있어도 연결은 하나다 — 이것이 푸시를 직접 구현하지 않는 이유다.
메시지가 두 종류다
| 알림 메시지 | 데이터 메시지 | |
|---|---|---|
| 앱이 백그라운드 | 시스템이 알아서 표시한다. 앱 코드가 안 돈다 | 앱이 깨어나 직접 처리한다 |
| 앱이 포그라운드 | 앱 콜백으로 온다 | 앱 콜백으로 온다 |
| 표시 내용 | 서버가 정한 제목·본문 | 앱이 만든다 |
백그라운드에서 동작이 갈리는 것이 혼란의 근원이다. "테스트할 땐 잘 되는데 앱을 닫으면 커스텀 알림이 안 나온다" 는 대개 알림 메시지를 보내 시스템이 기본 형태로 표시한 경우다. 알림을 앱이 직접 그려야 한다면 데이터 메시지를 쓴다.
토큰이 곧 주소다
기기마다 발급되는 등록 토큰으로 대상을 지정한다. 이 토큰은 고정이 아니다 — 앱 재설치, 데이터 삭제, 기기 복원 시 바뀐다. 그래서 앱은 토큰이 갱신될 때마다 서버에 다시 알려야 하고, 서버는 더 이상 유효하지 않다는 응답을 받으면 저장된 토큰을 지워야 한다. 안 지우면 죽은 토큰이 쌓여 발송 비용과 실패율이 함께 오른다.
주제 구독 — 다수에게 한 번에
FirebaseMessaging.getInstance().subscribeToTopic("notice-kr")
토큰을 하나씩 지정하는 대신 주제(topic) 를 구독시키면 서버는 그 주제 하나로 발송한다. 공지처럼 대상이 넓을 때 서버가 토큰 목록을 관리하지 않아도 된다.
대신 누가 구독 중인지 서버가 정확히 알 수 없고 개인화도 못 한다. 개인 알림은 토큰으로, 브로드캐스트는 주제로 나누는 것이 일반적인 구성이다.
전달은 보장되지 않는다
FCM 은 최선 노력이다. 기기가 오래 꺼져 있으면 메시지가 합쳐지거나 버려질 수 있고, 절전 상태에서는 지연될 수 있다. 우선순위를 높이면 절전 중에도 깨우지만 남용하면 시스템이 앱의 우선순위를 낮춘다.
그래서 푸시를 데이터 전달 수단으로 쓰지 않는다. 푸시는 "새 것이 있으니 확인하라" 는 신호로 쓰고, 실제 내용은 앱이 서버에서 받아 오게 한다. 그러면 한 번 놓쳐도 다음 실행에서 복구된다.
표시는 알림 채널을 거친다
FCM 은 메시지를 전달할 뿐 화면에 띄우는 일은 알림 시스템이 한다. Android 8 부터 모든 알림은 채널에 속해야 하고, 소리·진동·중요도는 앱이 아니라 채널 설정과 사용자 선택이 정한다.
그래서 "푸시는 오는데 소리가 안 난다" 는 대개 FCM 문제가 아니라 채널 중요도 문제다. 채널은 한 번 만들면 앱이 중요도를 다시 올릴 수 없다 — 통제권이 사용자에게 있기 때문이다.
권한이 필요하다
Android 13 부터 알림 표시는 런타임 권한이다. 거부되면 조용히 아무것도 표시되지 않으므로, 왜 필요한지 맥락이 있는 시점에 요청해야 승낙률이 오른다.
콘솔에서 보낸 것과 서버에서 보낸 것이 다르다
Firebase 콘솔의 테스트 발송은 알림 메시지를 보낸다. 백그라운드에서는 시스템이 표시하므로 앱 코드가 돌지 않는다. 서버가 데이터 메시지를 보내도록 만들어 두고 콘솔로만 테스트하면, 개발 중엔 되던 것이 실제 발송에서 안 되는 상황이 생긴다.
디버깅 순서는 정해져 있다 — 토큰이 유효한가 → 서버가 어떤 종류로 보냈나 → 앱이 포그라운드인가 → 채널 중요도가 무엇인가. 이 순서로 좁히면 대부분 걸린다.
면접 함정
- ❌ "푸시는 반드시 도착한다" → 최선 노력이다. 중요한 것은 앱이 서버에서 다시 확인하게 한다.
- ❌ "토큰은 기기마다 고정" → 재설치·복원으로 바뀐다. 갱신 처리와 죽은 토큰 정리가 필요하다.
- ❌ "우선순위를 높이면 항상 즉시 온다" → 남용하면 시스템이 앱을 제한한다.