Delivery Lab 02. 조리 중인데 라이더는 이미 도착했다 글 표지
Delivery LabReact NativeTypeScriptTesting

Delivery Lab 02. 조리 중인데 라이더는 이미 도착했다

조리 중인 매장과 기다리는 라이더를 함께 표현하기 위해 상태를 나눴습니다. 120초 자동 시연에서 픽업과 전달 완료, 굽은 경로와 취소 경계를 확인한 기록입니다.

첫 시뮬레이터에서는 주문을 준비하고, 라이더가 이동하고, 배달을 완료했다. 준비 15초와 이동 60초를 이어 붙인 단순한 흐름이었다. 조회 횟수와 관계없이 같은 시간에 같은 상태가 나온다는 것까지 확인했다.

그런데 카페 배달의 과정을 조금 더 담으려니 이 순서만으로는 아쉬웠다. 매장이 주문을 수락하고 커피를 만드는 동안에도 라이더는 움직일 수 있다. 라이더가 먼저 매장에 도착해서 기다리는 장면도 넣고 싶었다.

여기서 상태 이름을 고르기가 애매해졌다.

커피는 아직 조리 중이고 라이더는 매장에 도착했다. 이 주문의 상태는 무엇일까?

‘조리 중’ 다음에 ‘라이더 도착’을 넣으면 될까?

하나의 상태 목록에 단계를 더 넣는 방법부터 생각할 수 있다. 주문 수락, 조리 중, 라이더 이동, 매장 도착, 조리 완료, 픽업 완료처럼 순서를 늘리는 것이다.

문제는 이 이름들이 서로 다른 대상을 설명한다는 데 있다. ‘조리 중’은 매장의 상황이고, ‘매장 도착’은 라이더의 상황이다. 라이더가 도착했다고 커피가 다 만들어지는 것은 아니다.

화면의 상태를 ‘매장 도착’으로 바꿔버리면 매장이 아직 조리 중이라는 사실이 사라진다. 계속 ‘조리 중’으로 두면 이번에는 라이더가 와서 기다린다는 사실이 보이지 않는다.

들판에서 손목시계를 보며 기다리는 미스터 빈 GIF
라이더: 저 도착했는데요.

‘조리 중이며 라이더 대기 중’이라는 상태를 하나 더 만들 수도 있다. 지금 시나리오만 보면 충분히 가능한 방법이다. 다만 매장 상황과 라이더 상황을 함께 이름에 넣다 보면, 조리가 먼저 끝나는 경우에도 또 다른 조합을 만들어야 한다.

이번에는 매장과 라이더가 각각 어디까지 진행했는지를 별도 필드에 담기로 했다. 고객에게 짧게 보여줄 주문 요약도 함께 두었다.

상태는 나누고, 시연은 하나의 시간표로

매장은 주문 확인, 수락, 조리, 준비 완료, 라이더에게 인계하는 과정을 따른다. 라이더는 배정, 매장으로 이동, 매장에서 대기, 픽업, 고객에게 이동, 도착, 전달 완료의 과정을 따른다.

API 응답에는 이를 merchantStatusriderStatus로 구분했다. 고객용 요약은 기존의 status에 담는다. 예를 들어 매장이 조리 중이고 라이더가 기다리는 동안에는 전체 요약을 ‘조리 중’으로 보여준다. 조리가 끝나면 ‘픽업 대기’로 바뀐다.

역할별로 직접 버튼을 누르는 앱까지 만들지는 않았다. 지금 확인하려는 것은 고객 앱에서 배달 과정을 따라가는 경험이어서, 새 배달을 시작하면 정해진 순서로 진행되는 자동 시연을 유지했다.

시나리오는 120초로 다시 짰다. 라이더의 매장 이동과 대기, 픽업, 고객에게 이동한 뒤 전달하는 순간을 나눴다. 주요 시점의 상태는 아래와 같다. 시간은 모두 주문 생성 이후의 경과 시간이다.

시점매장라이더
15초조리 중매장으로 이동 중
40초조리 중매장에서 대기
50초조리 완료매장에서 대기
55초라이더에게 인계 완료픽업 완료
60초라이더에게 인계 완료고객에게 이동 중
115초라이더에게 인계 완료고객 위치 도착
120초라이더에게 인계 완료전달 완료

40초부터 50초까지는 제목에 적은 장면이 된다. 라이더는 매장에 있지만 조리는 아직 끝나지 않았다. 50초에 조리가 끝난 뒤에도 바로 출발하지 않는다. 55초에 픽업하고, 60초에 고객에게 이동하기 시작한다.

구현도 이 시간표를 그대로 따른다. 아래는 DEMO_TIMELINE에서 세 구간을 발췌한 코드다. 인자는 사건 이름, 경과 초, 매장 상태, 라이더 상태, 고객용 요약 순서다.

milestone('rider_arrived_at_store', 40, 'cooking', 'at_store', 'preparing'),
milestone('cooking_completed', 50, 'ready', 'at_store', 'ready_for_pickup'),
milestone('order_picked_up', 55, 'handed_over', 'picked_up', 'picked_up'),

조회할 때는 현재 경과 시간에 해당하는 행을 고른다. 매장 상태와 라이더 상태를 따로 받은 뒤 고객용 요약을 추론하는 구조는 아니다. 같은 시간표에서 세 값을 함께 정한다.

이렇게 하면 시연에서 보여주려는 조합과 전환 시점을 한곳에서 확인할 수 있다. 대신 매장의 조리 완료 요청이나 라이더의 도착 요청이 서로 다른 순서로 들어오는 상황까지 처리하는 모델은 아니다. 그런 입력을 받게 된다면, 가능한 전환과 고객용 요약을 정하는 규칙도 다시 살펴봐야 한다.

같은 주문 안에 두 가지 진행이 보이도록

앱에도 매장 상태와 라이더 상태를 나란히 표시했다. 큰 주문 상태 하나만 읽을 때보다, 무엇을 기다리는 중인지 구체적으로 보이게 하고 싶었다.

iPhone 17 Pro 시뮬레이터에서 남은 시간이 80초일 때 ‘조리 중’과 ‘매장에서 대기’가 함께 표시되는 것을 관찰했다. 아래에 남겨둔 캡처는 그다음 장면이다. 조리는 끝났고 라이더는 픽업을 기다리고 있다.

매장은 조리 완료, 라이더는 매장에서 대기로 표시된 Delivery Lab 앱
커피는 완성. 이제 라이더에게 건네줄 차례다.

매장에 도착한 40초부터 고객에게 출발하는 60초까지는 지도 위 라이더의 좌표도 매장에 머문다. 이 사이에 조리가 끝나고 픽업 상태가 바뀌지만 이동 진행률은 증가하지 않는다.

처음에는 라이더가 지도에서 움직이면 배달이 진행되는 것처럼 보였다. 상태를 나누고 나니 움직이지 않는 시간에도 표현할 일이 있었다. 커피를 기다리거나 건네받는 동안 위치는 같아도 주문의 과정은 계속 바뀐다.

목적지에 도착해도 아직 건네준 것은 아니다

매장에서의 픽업을 구분하니 고객 쪽 도착도 다시 보게 됐다. 목적지 좌표에 도달한 순간을 곧바로 배달 완료로 처리해도 될까?

이번 시연에서는 도착을 115초, 전달 완료를 120초로 나눴다. 문 앞까지 이동하는 것과 주문자에게 건네주는 것을 서로 다른 단계로 표현한 것이다. 5초는 이 차이를 보여주기 위해 정한 시연용 간격이다.

이때 진행률의 의미도 분명해야 했다. progress는 경로를 얼마나 이동했는지 나타내는 값이다. 목적지에 도착하면 1이지만, 주문 상태는 아직 arrived다. 화면에서도 이를 ‘전체 이동’으로 표시한다.

아래는 실제 테스트의 일부다. 주문의 시작 시각이 1_000_000밀리초일 때, 115초 뒤의 결과를 확인한다.

expect(getOrderSnapshot(order, 1_115_000)).toMatchObject({
  status: 'arrived',
  progress: 1,
  position: DEMO_LOCATIONS.customer
});

거리 계산이 끝났다는 이유로 주문까지 완료 처리하지 않도록, 위치와 상태를 함께 확인했다. 120초에는 별도 경계 테스트에서 delivered가 되는지도 확인했다.

시뮬레이터에서는 최종 전달 완료 화면을 확인했지만, 그 직전 5초 동안의 ‘도착’ 화면은 따로 포착하지 못했다. 이 구간의 상태 전환은 테스트에서 확인한 결과로 구분해두었다.

경로를 구불구불하게 만들면서 바꾼 계산

지도도 출발지에서 목적지로 곧장 이어지는 모양보다 복잡하게 만들고 싶었다. 배경에는 실제 Apple Maps를 사용하고, 성수 지역에 정해둔 가상 좌표들을 굽은 경로로 연결했다. 라이더가 매장으로 오는 경로와 매장에서 고객에게 가는 경로를 나눴다.

좌표가 여러 개가 되니 ‘배열의 어디쯤인가’와 ‘거리의 어디쯤인가’가 달라졌다.

예를 들어 한 구간이 100m이고 다음 구간이 900m인데 둘에 같은 시간을 배분하면, 긴 구간에서 갑자기 빨라진다. 지도에 점을 몇 개 찍었는지가 라이더의 속도에 영향을 주는 셈이다.

그래서 각 좌표 사이의 거리를 구하고, 누적 거리를 기준으로 현재 위치를 찾도록 했다. 매장으로 이동하는 시간과 고객에게 이동하는 시간은 각각 정해져 있고, 각 이동 안에서 경과한 비율을 거리로 바꾼다. 두 경로를 합친 전체 이동률도 거리를 기준으로 계산한다.

덕분에 매장에 도착했다고 무조건 전체 이동의 50%가 되지는 않는다. 위 캡처에서 픽업 대기 중 이동률이 41%인 것도 두 경로의 길이가 다르기 때문이다.

실제 지도를 썼어도 위치와 경로는 합성 데이터다. 도로를 검색해 경로를 찾거나 실제 GPS를 추적한 것은 아니다. 지금은 굽은 길의 이동과 매장에서 멈추는 장면을 반복해서 확인하는 데 사용하고 있다.

취소하면 앞으로 일어날 일도 멈춰야 한다

상태가 늘면서 취소할 때 고정해야 할 것도 늘었다. 위치만 멈추고 매장 상태가 계속 바뀌면, 취소한 주문의 커피가 뒤늦게 완성된다.

조회 결과에 보여주는 진행 내역도 같은 기준이 필요했다. 현재 내역은 시간표에서 이미 지난 사건들을 계산한 것이다. 사용자의 행동을 모두 저장하는 로그는 아니다. 취소되면 취소 시점까지의 내역에 취소 사건만 더하고, 이후의 예정된 사건은 포함하지 않도록 했다.

회귀 테스트에서는 주문을 43초에 취소하고 200초 시점에 다시 조회했다. 매장은 ‘조리 중’, 라이더는 ‘매장에서 대기’로 남고, 위치와 이동률도 취소 당시 값이어야 한다. 내역에 50초의 조리 완료가 추가되지 않는지도 확인했다.

이때 응답의 모든 값이 고정되는 것은 아니다. 지금 조회했다는 사실을 나타내는 서버 시각은 갱신된다. 멈추는 것은 취소한 배달의 진행이다.

다음에는 화면이 아는 시간이 맞는지

이번에는 매장과 라이더의 상황을 따로 표현하면서, 지도에서 멈춰 있는 시간도 배달 과정에 포함했다. 조리 완료와 픽업 완료, 목적지 도착과 전달 완료를 구분하니 화면의 문구와 테스트가 확인해야 할 경계도 더 구체적으로 정해졌다.

다만 여기까지는 서버에 현재 시각을 주면 올바른 상태가 나온다는 이야기다. 앱이 그 결과를 제때 받는지는 다른 문제다.

화면의 남은 시간은 0초가 됐는데, 마지막으로 받은 응답은 아직 ‘배달 중’이라면 어떻게 보여줘야 할까? 취소는 성공했는데 그전에 보낸 조회 응답이 뒤늦게 도착하면 어떨까?

서버에서 계산한 상태와 앱이 마지막으로 확인한 상태가 어긋날 때의 처리는 다음 편에서 다룬다.


이 글은 구현 후 시뮬레이터에서 확인한 120초 시나리오를 정리한 글이다. 프로젝트는 개발 중이며, 매장·라이더가 직접 조작하는 흐름과 실제 GPS 추적은 현재 범위에 포함하지 않았다. 이후 조리 완료와 라이더 도착의 순서를 바꾸는 실험을 한다면 해당 실험의 결과를 별도로 남길 예정이다.