동의 시트는 왜 뜨지 않았나 — 규칙을 적어두고 기존 앱엔 적용하지 않았습니다
5일 전에 '마지막 사고'라고 쓴 글이 틀렸습니다. 콘솔은 전부 승인이고 매일 발송 완료로 보이는데 받는 사람은 0명이었습니다. 그룹 코드와 템플릿 코드가 한 글자 다르면 동의 시트가 아예 뜨지 않고, 실패를 '물어봤다'로 기록하면 그 기기는 영영 다시 묻지 않습니다
닷새 전에 기능성 푸시 시행착오를 정리한 글을 올리면서 “오늘 마지막(이라고 믿는) 사고”라고 썼습니다. 그 문장이 틀렸습니다. 그때 저는 원인을 찾은 게 아니라 규칙을 하나 적어두고 기존 앱에는 적용하지 않은 상태였습니다.
증상은 그대로였다
콘솔에서는 모든 게 정상으로 보입니다. 템플릿은 APPROVED, 예약은 활성, 매일 “발송 완료”. 그런데 push_stats의 sentCount가 계속 0입니다. 워크스페이스의 앱 대부분이 몇 주째 이 상태였습니다.
특히 이해가 안 됐던 건 주스 스피너였습니다. 이 앱은 처음부터 동의 요청 코드가 들어 있었는데도 동의자가 0명이었습니다. 코드가 없어서 못 받는 거라면 설명이 되는데, 있는데도 0명이면 다른 이야기입니다.
먼저 엉뚱한 곳을 짚었다
제 폰에서 테스트하는 동안 동의 시트가 뜨지 않으니 자연스럽게 이런 가설들이 나왔습니다.
- 테스트 모드라서 그런 걸까? 테스트 기기에는 원래 안 뜨는 게 아닐까 싶어 정식 출시 후 다시 확인해 보기로 했습니다.
- SDK가 낡아서 그런 걸까? 최신 버전으로 올려 다시 빌드하고 올렸습니다.
- 앱에 넣어둔 진단 패널은
unsupported를 뱉었고, 그다음에는NOTIFICATION_AGREEMENT_FAILED가 떴습니다.
둘 다 아니었습니다. 출시해도 안 떴고, SDK를 올려도 안 떴습니다. 이틀을 여기서 썼습니다.
결정적 실마리는 ‘되는 앱’이었다
벽돌깨기를 테스트하다가 동의 팝업이 떴습니다. 같은 코드, 같은 SDK, 같은 게이트인데 이 앱에서만 떴습니다.
여기서 방향이 바뀌었습니다. 안 되는 앱만 계속 들여다볼 게 아니라 되는 앱과 안 되는 앱의 차이를 찾으면 됐던 겁니다. 앱을 코드가 아니라 “언제 어떻게 만들어졌는가”로 줄 세우자 경계가 선명했습니다.
| 앱 | 템플릿을 만든 방법 | 결과 |
|---|---|---|
| 벽돌깨기·단어 맞추기 등 | 9월, API로 생성 | 시트가 뜬다 |
| 타로 2종·기억력 카드·반응속도 등 | 8월, 콘솔 웹으로 생성 | 실패한다 |
원인 ① 그룹 코드와 템플릿 코드가 한 글자라도 다르면 안 뜬다
requestAgreement({ templateCode })는 그룹 코드와 템플릿 코드가 완전히 같을 때만 짝을 찾습니다. 다르면 네이티브가 NOTIFICATION_AGREEMENT_FAILED로 거부하고, 둘 중 어느 쪽 코드를 넣어도 실패합니다. 실제로 양쪽 다 넣어 봤고 둘 다 실패했습니다.
| 앱 | 그룹 코드 | 템플릿 코드 | |
|---|---|---|---|
| 벽돌깨기 | breakout-DAILY_20 | breakout-DAILY_20 | 일치 |
| 타로 운세 | fadong-tarot-daily-card | fadong-tarot-daily-card-01 | 어긋남 |
| 기억력 카드 | memory-card-daily-1900 | memory-card-daily-1900-a | 어긋남 |
콘솔 웹으로 템플릿을 만들면 템플릿 코드에 -01이나 -a 같은 접미어가 자동으로 붙습니다. 그래서 콘솔로 만든 것은 예외 없이 어긋나 있었습니다. API로 만들면 넣은 값이 그룹과 템플릿 양쪽에 그대로 들어가므로 자동으로 일치합니다.
여기서 제일 뼈아픈 부분은, 닷새 전 글의 체크리스트에 “그룹 코드 = 템플릿 코드”를 이미 적어뒀다는 것입니다. 9월에 새로 만드는 앱에는 그 규칙을 지켰고, 8월에 이미 만들어 둔 것들은 한 번도 대조해 보지 않았습니다. 규칙을 아는 것과 기존 자산에 적용하는 것은 다른 일이었습니다.
점검은 한 줄이면 됩니다. 템플릿 목록을 조회해 joinedTemplateSet.code와 templateSetList[0].code를 눈으로 비교하면 끝입니다.
원인 ② 실패해도 ‘물어봤다’고 기록해 기기를 태웠다
동의 요청은 기기당 한 번만 해야 합니다. 그래서 요청했다는 사실을 localStorage에 남기는데, 그 기록을 요청을 보내기 전에 찍고 있었습니다.
1
2
3
// 예전 — 요청 결과와 무관하게 기록
localStorage.setItem(key, '1');
await requestNotificationAgreement(code);
원인 ①로 요청이 실패해도 기록은 남습니다. 그 기기는 두 번 다시 시트를 보지 못합니다. 서버 쪽을 고쳐도 복구되지 않습니다. 진단하는 내내 제 폰이 계속 조용했던 이유이기도 합니다.
1
2
3
// 지금 — 시트가 실제로 떴을 때만 기록
const res = await requestNotificationAgreement(code);
if (SHEET_SHOWN.has(String(res))) localStorage.setItem(key, '1');
SHEET_SHOWN은 newAgreement·alreadyAgreed·agreementRejected 세 가지입니다. 사용자가 거절한 경우도 시트는 뜬 것이므로 기록합니다. 그 밖의 값은 시트가 안 뜬 것이니 기록하지 않고 다음 실행에 다시 시도합니다.
원인 ③ 게이트가 사실상 도달 불가였다
진입하자마자 동의 시트를 띄우면 검수에서 반려됩니다. 그래서 “몇 판 이상 + 몇 분 이상 사용” 게이트를 두는데, 이 값을 변수에만 들고 있었습니다.
웹뷰는 리로드되면 변수가 0으로 돌아갑니다. 타로처럼 한 세션이 1분인 앱은 “한 세션에서 3판 + 3분 연속 체류”를 영원히 못 넘습니다. 대상이 0명인 걸 고치려고 넣은 훅이 또 0명이 될 뻔했습니다.
지금은 최초 실행 시각과 누적 사용 횟수를 localStorage에 남겨 기기 기준으로 누적하고, 진입 직후 노출 방지는 “이번 세션에서도 최소 1회 사용”이 따로 맡습니다.
전수 점검에서 더 나온 것들
원인을 확정한 뒤 앱을 전부 훑었는데, 문제가 한 종류가 아니었습니다.
- 동의 요청 코드가 아예 없는 앱 — 한글 몬스터, 수학 몬스터를 포함해 6개. 콘솔에는 매일 알림이 몇 주째 예약돼 돌고 있었습니다.
- 죽은 코드를 가리키던 앱 — 주스 스피너와 병 돌리기는 8월에 중지된 그룹을, 사주로또는 아예 존재하지 않는 코드를 소스에 하드코딩하고 있었습니다. 코드가 일치했더라도 실패했을 겁니다.
고치는 방법
동의는 템플릿이 아니라 동의문(termsId)에 붙습니다. 그래서 이미 받은 동의를 잃지 않고 고칠 수 있습니다.
- 기존 동의문을 그대로 재사용해, 그룹 코드 = 템플릿 코드인 새 템플릿만 만든다
- 앱의
push-consent.json을 새 코드로 바꾸고 재배포한다 - 옛 템플릿은 비활성화한다
기존 발송 일정이 그대로 그 동의자에게 나갑니다. 이 방식으로 템플릿 10개를 다시 만들었고 전부 승인됐습니다. 동의문이 AI 검수를 못 넘은 앱 하나만 발송 시각을 다른 게임과 같은 20시로 맞춰 새로 만들었습니다.
배운 것
콘솔의 초록불은 성공 지표가 아닙니다. 템플릿 승인, 예약 활성, 발송 완료 — 이 셋이 전부 정상이어도 받는 사람은 0명일 수 있습니다. 대상이 0명이어도 “발송 완료”가 뜨기 때문입니다. 유일하게 믿을 수 있는 숫자는 sentCount가 0에서 올라가는 것입니다.
그리고 하나 더. 닷새 전 글에 규칙을 적어놓고도 같은 함정에 다시 빠진 이유는, 그 규칙을 앞으로 만들 것에만 적용했기 때문입니다. 규칙을 새로 배웠으면 이미 만들어 둔 것들도 그 규칙으로 한 번 훑어야 했습니다. 이번에는 점검 방법까지 같이 적어뒀습니다.
마지막으로 정직하게 덧붙이면, 지금 확인된 것은 동의 시트가 실제로 뜬다는 것까지입니다. 벽돌깨기에서 동의자가 생겼고, 오늘 저녁 발송에서 sentCount가 0에서 올라가면 “동의 → 발송”까지 이어지는 게 최종 확인됩니다. 그 숫자를 보기 전까지는 아직 끝난 게 아닙니다. 이번에는 “마지막 사고”라고 쓰지 않겠습니다.
