푸시 알림은 보내는 기능이 아니라 관측하는 기능이었다

발송을 걸어두고 다음 날 지표를 봤는데, 성공으로 찍힌 숫자와 실제로 알림을 봤다는 반응이 맞지 않았다. 어디서부터 틀렸는지 알 수가 없었다. 발송이 안 된 건지, 발송은 됐는데 기기에서 안 뜬 건지, 떴는데 아무도 안 눌렀는지 구분할 방법이 없었다는 게 진짜 문제였다.

리텐션을 올려보겠다고 헤드라인 스코어링 엔진을 붙이고 개봉률 트래킹을 넣은 직후였다. 문구를 고르는 로직은 만들었는데, 그 문구가 실제로 도착했는지를 보는 눈은 안 만들어둔 상태였다.

발송이 중간에 멈춰 있었다

제일 먼저 잡은 건 발송이 도중에 서는 문제였다. 전체를 다 돌아야 하는데 어느 지점에서 조용히 끝나 있었고, 로그상으로는 정상 종료처럼 보였다. 실패한 건과 아예 시도조차 안 한 건이 같은 얼굴을 하고 있으면 원인을 좁힐 수가 없다.

같이 나온 게 통계 불일치였다. 발송 대상 수, 요청한 수, 성공으로 집계된 수가 서로 다른 기준으로 세어지고 있었다. 숫자 자체보다, 숫자를 믿을 수 없게 되면 그 다음 판단이 전부 흔들린다는 게 더 컸다. 이 화면을 보고 "오늘은 잘 나갔네" 하고 넘어갔던 날들이 있었을 거라는 생각이 들었다.

중복 발송은 재검증하다 발견했다

안정화 작업을 끝내고 다시 검증하는 과정에서 같은 알림이 두 번 나가는 경로가 보였다. 재시도와 신규 발송이 겹칠 수 있는 구간이었다. 사용자 입장에서 중복 알림은 버그가 아니라 그냥 짜증나는 앱이다. 한 번 그렇게 인식되면 알림 권한을 끄고, 그러면 리텐션 개편은 의미가 없어진다.

가드를 넣어서 막긴 했는데, 애초에 이걸 처음부터 설계에 넣지 않은 이유가 "한 번에 한 번만 보내면 되니까"였다. 재시도를 붙이는 순간 그 전제가 깨진다는 걸 늦게 알았다.

실패는 한 종류가 아니었다

그래서 푸시 상세 화면에 실패 사유별 카운트를 넣었다. 토큰이 만료된 건, 앱이 지워진 건, 전송 자체가 거절된 건은 대응이 전부 다르다. 뭉뚱그려 "실패 N건"으로 보고 있을 때는 손댈 곳을 못 정했다.

증상 실제로 봐야 했던 곳
발송이 중간에 멈춤 배치 처리 종료 조건, 예외 삼킴
성공 수치가 안 맞음 집계 기준이 단계별로 제각각
같은 알림 두 번 재시도와 신규 발송의 중복 구간
눌러도 엉뚱한 화면 백그라운드 상태의 딥링크 처리

마지막 줄이 제일 뒤늦게 나왔다. 앱이 백그라운드에 있을 때 알림을 누르면 딥링크로 안 가고 기본 화면으로 떨어졌다. 포그라운드에서 테스트할 땐 잘 됐다. 그래서 오래 몰랐다.

다음에 알림을 붙일 때

순서를 반대로 했어야 했다. 문구 최적화보다 계기판이 먼저다. 최소한 이 네 가지는 첫 배포 전에 만들어두려고 한다.

  • 대상 수 · 요청 수 · 성공 수를 같은 기준으로 세는 집계
  • 실패 사유를 코드 단위로 나눠 저장
  • 중복 발송 차단 키 (재시도를 넣을 계획이 없어도)
  • 백그라운드 · 종료 상태에서의 딥링크 실기기 확인

개봉률 트래킹은 넣어뒀는데, 이 숫자가 문구 품질을 정말 반영하는지는 아직 잘 모르겠다. 시간대나 요일 영향이 더 클 수도 있어서 몇 주는 더 쌓아봐야 판단이 될 것 같다. 지금 확실한 건, 안 보내진 걸 안 보내졌다고 말해주는 화면이 생긴 뒤로는 원인을 찾는 시간이 훨씬 짧아졌다는 정도다.

푸시 알림은 보내는 기능이 아니라 관측하는 기능이었다 · peppercode 개발 노트