Delivery Lab
카페 배달을 잠금화면까지 연결하며, 질문과 실험을 하나씩 쌓아가는 React Native 개발 기록입니다.
목차
7편에서는 같은 배달 시나리오를 Android에 연결했다. API 35 기기에는 일반 진행 알림이, API 36.1 에뮬레이터에는 트럭 아이콘과 시각이 있는 상태 칩이 나타났다.
처음에는 iOS에서처럼 남은 시간이 줄어들면 배달을 기다리는 느낌이 더 잘 들 것 같아서 Android의 시스템 카운트다운을 붙였다. 칩에 시각을 적은 건 구현을 검토하다가 카운트다운을 뺀 뒤였다.
3편에서 예상 시간이 지났다는 이유로 배달을 완료 처리하지 않기로 했다. 그때는 표시 함수에 시간을 바꿔 넣어 확인했다. 이번에는 같은 기준을 Android 알림에 적용한 뒤 실제로 API 연결을 끊어봤다.
시스템 타이머가 바꿔주지 않는 것
Android 알림에는 chronometer가 있다. 기준 시각을 주고 카운트다운을 설정하면 시스템이 시간 표시를 갱신한다. 앱이 매초 알림을 다시 게시하지 않아도 되니 처음에는 이 기능이 잘 맞아 보였다.
내가 작성한 나머지 표시 코드가 문제였다. 마지막 응답이 오래됐는지, 도착 예상 시각이 지났는지는 알림을 그릴 때 판단했다. 시스템 타이머가 움직여도 이 함수까지 다시 실행되지는 않았다. 조회가 멈춰도 숫자는 움직일 수 있는데, 상태 문구는 그대로 남는 구조였다.
나는 예상 시간이 지나면 도착 여부를 확인하고 있다고 안내하고 싶었다. 연결이 끊겼다면 마지막 정보를 보고 있다는 설명도 붙여야 했다. chronometer를 켜는 것만으로 이 두 문구가 저절로 바뀌지는 않았다.

이때 음수 카운트다운이 표시되는 장면을 관찰한 것은 아니다. 공식 Live Updates 안내도 상태 칩의 chronometer를 양수일 때 표시한다고 설명한다. 내가 걸린 부분은 시간이 지난 뒤 앱이 정한 안내 문구를 갱신하는 경로였다.
남은 시간 대신 마지막 예상 시각
나는 마지막 확인 시각과 예상 도착 시각을 그대로 적기로 했다. 시스템 타이머를 끄고, 알림의 짧은 칩에도 11:47처럼 절대 시각을 넣었다. 현재 코드에서는 두 설정을 함께 끈다.
카드에는 마지막 확인과 마지막 도착 예상을 함께 적었다. 언제 확인한 정보인지, 그 응답으로 계산한 도착 시각은 언제인지를 나란히 읽도록 했다. 다음 조회가 실패하면 두 시각을 지금으로 바꾸지 않고 유지한다.
남은 분 수를 바로 읽지 못하는 점은 아쉽다. 현재 시각과 예상 시각을 한 번 비교해야 한다. 그래도 조회가 끊겼는데 숫자만 움직이면 새로운 정보를 받고 있다고 느끼기 쉽지 않을까. 정적 시각을 쓰면 그런 인상이 줄어들 것 같았다.
알림을 더 자주 갱신할 실행 경로를 만들고 카운트다운을 유지하는 선택도 가능하다. 다만 이번 실험에 백그라운드 서비스나 정확한 알람 권한까지 추가하지는 않았다. 먼저 현재의 갱신 경로가 멈췄을 때 무엇이 남는지 확인하고 싶었다. Android에서 카운트다운을 만들 수 없어서 내린 결정은 아니다.
연결을 끊어도 조회가 바로 실패하지는 않았다
실험은 Android 16/API 36.1 전용 에뮬레이터에서 했다. 새 배달을 시작하고 앱을 홈으로 보낸 다음, API 포트로 이어지는 adb reverse 연결을 제거했다. 이 연결 덕분에 기기의 로컬 주소에서 Mac의 개발 서버에 접근하고 있었다.
여기서 Mac worker까지 멈추지는 않았다. worker는 계속 ADB 신호를 보내고, debug receiver는 신호를 받을 때 저장된 현재 주문을 한 번 GET했다. API로 가는 길만 끊어 조회 실패 뒤 알림이 어떻게 남는지 보는 조건이었다. FCM을 보내거나 받은 실험은 아니다.
기존 연결로 GET이 잠시 더 성공했다. 연결을 제거한 순간부터 모든 요청이 실패한 건 아니었다. 마지막으로 성공한 응답 시각을 따로 기록해야 했다.
마지막 확인은 11:53:19, 주문 상태는 조리 중이었다. 이후 GET이 실패하는 동안 알림에 분 단위로 표시한 도착 예상 시각 11:54가 지났다. 이때 저장된 상태는 여전히 preparing, terminal=false였다.
잠금화면에는 작은 알림 아이콘들이 모여 있었다. 배달 트럭이 있는 pill을 누르자 잠금을 해제하지 않은 채 알림창에 카드가 펼쳐졌다. 카드에는 조리 중이라는 제목과 함께 다음 문구가 남아 있었다.
마지막 확인 11:53:19 · 마지막 도착 예상 11:54 · 도착 상태 확인 대기 · 마지막 정보·갱신 대기

Mac에서 같은 주문을 조회했을 때 서버는 이미 delivered라고 답했다. 기기는 이 응답을 받지 못했다. 서버와 기기가 알고 있는 상태가 실제로 달라진 순간이었다. 예정 시각을 알고 있다는 이유로 기기의 마지막 상태까지 완료로 바꾸지는 않았다.
확인 대기 문구도 실행 기회가 필요했다
캡처에 갱신 대기 문구가 나왔다고 해서 시간이 지나면 항상 알아서 바뀐다고 말할 수는 없었다. 이번에 이 문구를 그린 계기는 GET 실패 처리였다.
receiver의 GET이 실패하면 마지막으로 저장한 정보를 유지하고, stale 기준을 지났을 때 알림을 다시 그리도록 했다. 이때 현재 시각과 도착 예상 시각도 비교해서 확인 대기 문구를 붙인다. 주문 상태와 마지막 확인 시각은 그대로 둔다.
이번에는 worker가 계속 receiver를 깨웠기 때문에 그 실패 처리까지 실행됐다. 기기를 깨우는 신호도 함께 끊긴 경우는 확인하지 않았다. 다시 그릴 기회가 없다면 정적 시각이 남고, 정확히 stale 시각에 맞춰 문구가 전환된다는 보장도 없다.
그래서 마지막 확인 시각을 항상 적어둔 선택이 필요했다. 안내 문구를 새로 붙일 수 없는 동안에도 지금 보고 있는 내용이 언제 확인된 것인지는 남는다. 이 실험으로 FCM 전달이나 Doze 중 수신, 사용자 force-stop 뒤 복구까지 검증한 것은 아니다.
다시 연결한 뒤에야 알림을 지웠다
11:55:21에 API 연결을 복구했다. 다음 조회를 처리한 11:55:23.758, 네이티브 로그에 terminal=true active=false가 남았다. 알림도 사라졌다. 기기가 서버의 완료 응답을 받고 나서야 마지막 조리 중 표시를 정리했다.
아래는 연결 단절 실험에 앞서 11:47:48에 자연 완료한 별도 주문의 화면이다. 정상 조회 중에도 서버가 완료를 알려준 뒤 홈 화면의 칩이 제거되는 것을 확인했다.

사용자가 직접 알림을 지웠을 때도 구분이 필요했다. API 36.1에서 카드를 스와이프하고 앱으로 돌아와 조회를 계속해도 알림은 다시 생기지 않았다. 수동 표시 버튼을 눌렀을 때만 새 인스턴스를 만들었다. 주문이 진행 중이라는 이유로 사용자가 닫은 표시를 계속 되살릴 수는 없었다.
리뷰에서는 저장과 게시 순서도 바꿨다. 새 응답을 먼저 보여준 뒤 저장에 실패하면 화면과 저장소가 서로 다른 서버 시각을 기억한다. 그사이 시각의 늦은 응답을 받아 화면이 되돌아갈 수 있었다. 업데이트는 저장에 성공한 뒤 게시하도록 바꾸고, AtomicFile 쓰기 실패를 만든 회귀 테스트로 확인했다. 기기에서 저장 오류를 직접 일으킨 실험은 아니다.
나는 남은 시간을 더 생생하게 보여주고 싶어서 카운트다운을 붙였다. 하지만 이 단계에서 유지할 수 있는 표시는 마지막으로 확인한 상태와 예상 시각이었다. 숫자가 조금 덜 움직이더라도, 서버가 알려주지 않은 완료를 화면에서 먼저 말하지 않도록 정리했다.
