운영 로그 1% 걸음수 초기화 — 네이티브-웹 상태 불일치 디버깅
상황
이마트 모바일앱 내 걷기 미션 서비스에서, 운영 중 약 1%의 유저 걸음수가 갑자기 0으로 초기화되는 현상이 보고됐습니다. 서비스는 하이브리드 앱 구조로, 네이티브 만보기가 20걸음 단위로 웹뷰 콜백을 호출하고, 프론트엔드는 이 값을 localStorage에 캐싱하면서 화면에 표시합니다.
문제의 특성:
- 특정 제조사 편중 없음 (OS 레벨 문제 아님)
- 항상 발생하는 게 아닌 간헐적 1% (구조적 결함 아님)
- 서버에 남는 로그에서 0이 찍힘 (단순 UI 버그가 아닌 데이터 레벨 이슈)
문제
걸음수가 0이 되는 알려진 케이스는 3개: stopWalk 호출, 자정 초기화, 날짜 변경. 그 외에 0이 될 수 있는 숨겨진 경로를 찾아야 했습니다.
제약:
- 네이티브 코드에 직접 접근 불가 (타입 정의와 콜백 인터페이스만 확인 가능)
- 서버 세션 환경 미구축 (서버 상태 추적 불가)
- 멀티웹뷰 아키텍처 (각 페이지가 독립 Vue 인스턴스)
- Throttle 의심
- localStorage 손상
- stopWalk 호출
- 네이티브 타임아웃
접근과 판단
가설 1: Throttle Leading-Edge (기각)
처음엔 프론트엔드 throttle 로직이 0을 먼저 서버로 보내는 것으로 의심했습니다. 하지만 서버 로그는 단순 기록용이고, 네이티브 콜백이 20걸음 단위로만 호출되므로 "첫 emission이 0"인 상황 자체가 자정 리셋 외에는 발생하지 않습니다.
가설 2: localStorage 손상 (기각)
JSON 파싱 실패로 캐시가 날아가는 시나리오. 그러나 이게 문제라면 구조적으로 항상 발생해야 하고, 일정한 1%와 부합하지 않습니다.
가설 3: 프론트 코드의 의도치 않은 stopWalk 호출 (부분 확인)
Gate 상태머신(orchestrateEntry)에서 서버 응답 started: false + 네이티브 measuring 상태 조합 시 stopWalk이 호출되는 경로를 발견:
if (walkStatus === 'measuring') {
await stopWalk(); // 서버는 미시작인데 네이티브는 측정 중 → 강제 정지
}
하지만 BQ 로그를 확인한 결과, 실제로 이 경로를 타지 않은 케이스에서도 0이 찍히고 있었습니다.
가설 4: 네이티브 자체 비활성 타임아웃 (확정)
BQ 로그 데이터를 시간순으로 추적한 결과:
| 시간 | 이벤트 | 걸음수 |
|---|---|---|
| 21:18 | 마지막 활동 | 4180 |
| 22:04 | Gate 재진입 → Mission 이동 안 됨 | — |
| 22:13 | Gate → Mission (1초만에 이동) | 0 |
22:04에 Mission으로 이동하지 못한 이유를 코드에서 추적:
if (isAvailable.value && isStarted.value) {
const { status: walkStatus } = await getWalkStatus();
if (walkStatus === 'measuring') {
return 'navigate_mission';
}
}
return 'stay'; // walkStatus가 'idle'이면 여기에 빠짐
서버는 started: true를 반환했지만, getWalkStatus()가 'idle'을 반환 — 즉 네이티브가 자체적으로 측정을 종료한 것입니다.
구현
이 케이스에서는 코드 수정이 아닌 원인 확정이 핵심 산출물입니다. 확정된 원인을 바탕으로 네이티브 팀에 전달할 내용:
- 네이티브 비활성 타임아웃(
WKEM_011 expired) 발동 시 카운터도 함께 0으로 리셋하고 있음 - 이후 재진입 시
startWalk을 호출하더라도startWalk은 카운터를 초기화하지 않으므로, 이미 0이 된 상태에서 다시 쌓기 시작 - 46분 이상 비활성 후 재진입하는 유저 비율이 약 1%와 일치
- 가설 1
Throttle leading-edge → 구조적으로 불가능 (기각)
- 가설 2
localStorage JSON 파싱 실패 → 1% 발생률과 불일치 (기각)
- 가설 3
orchestrateEntry의 stopWalk 호출 → BQ에서 미해당 케이스 발견 (부분)
- 가설 4
네이티브 비활성 타임아웃 WKEM_011 → 46분 비활성 = 1% 일치 (확정)
결과
- 운영 이슈 root cause 확정 → 네이티브 팀과의 논의 기반 확보
- 프론트 코드 경로(
orchestrateEntry의stopWalk)는 별도 시나리오로 분리 확인 - BQ 로그 + 코드 정적 분석 + 타입 정의를 교차 검증하는 디버깅 방법론 확립
핵심 증거
22:04 Gate 재진입 시 Mission 이동 실패 → walkStatus === 'idle' 서버는 started: true인데 네이티브는 idle = 네이티브 자체 종료
발생 메커니즘
46분 비활성 → WKEM_011 expired → 카운터 0 리셋 재진입 시 startWalk 호출해도 카운터는 0부터 재시작
배운 것
하이브리드 앱에서 간헐적 데이터 이상을 디버깅할 때:
- "누가 0을 만들었는가"를 코드 경로에서 역추적하되, 네이티브 코드에 접근 불가하면 타입 정의(
idle/measuring/expired)와 콜백 계약으로 행동을 추론할 수 있습니다. - 발생률로 가설을 필터링합니다. "항상 발생해야 하는 구조적 결함"과 "특정 조건에서만 발생하는 간헐적 이슈"를 구분하면 탐색 공간이 대폭 줄어듭니다.
- 로그의 부재도 증거입니다. "22:04에 Mission 이동 안 됨"이라는 부정적 증거가
walkStatus === 'idle'→ 네이티브 자체 종료를 가리켰습니다.