중고차 점검자가 쓰는 모바일 앱입니다. 입고·세차·도장·상품화·사진·출고, 성능점검과 품질검사, 타이어·쇼바를 포함한 종합검수까지 25개 화면으로 현장 업무 전체를 덮습니다.
기술적으로 어려웠던 건 화면 수가 아니라 점검이 이뤄지는 장소였습니다.
문제
점검은 지하 주차장과 정비고에서 일어납니다. 회선이 자주 끊깁니다.
차량 한 대에 사진을 수십 장 찍는데, 업로드가 끝날 때까지 기다리게 하면 점검이 멈춥니다. 끊긴 상태에서 찍은 사진이 사라지면 점검자는 그 차로 다시 돌아가야 합니다.
즉 요구사항은 "업로드를 빠르게"가 아니라 "업로드를 점검자가 신경 쓰지 않게" 였습니다.
결정
업로드를 화면에서 떼어냈다
사진을 찍으면 즉시 로컬에 넣고, 화면은 서버가 아니라 로컬 파일을 보고 그립니다. 점검자 입장에서 업로드는 이미 끝난 일입니다. 실제 전송은 뒤에서 알아서 됩니다.
큐를 브라우저 DB 에 뒀다
업로드 대기열을 메모리가 아니라 IndexedDB 에 넣었습니다. 앱을 껐다 켜도 큐가 살아남습니다. 시작할 때 생성 시각 순으로 전부 읽어 다시 밀어 넣기 때문에, 배터리가 나가 앱이 죽어도 사진은 남습니다.
DB 를 못 쓰는 환경에서는 기능을 끄지 않고 등급을 낮췄다
브라우저 설정이나 프라이빗 모드에서 IndexedDB 초기화가 실패할 수 있습니다. 이때 앱을 멈추는 대신 메모리 큐로 자동 강등합니다.
기능이 사라지는 게 아니라 보장 수준만 낮아집니다. 앱을 껐다 켜면 큐는 사라지지만, 그 세션 안에서는 똑같이 동작합니다. 현장에서 "안 되는 폰"이 생기지 않는 쪽을 택했습니다.
동시 업로드를 1개로 묶었다
병렬로 올리면 빠를 것 같지만, 현장 회선에서는 반대입니다. 여러 요청이 좁은 대역을 나눠 쓰면서 전부 느려지고 전부 실패합니다.
동시 실행을 하나로 직렬화했습니다. 하나씩 확실히 보내는 편이 결과적으로 더 빨리 끝납니다.
자격증명을 앱에 두지 않았다
업로드는 서버에서 서명 URL 을 받아 거기에 직접 올립니다. 스토리지 접근 권한이 앱 안에 존재하지 않으므로, 앱을 뜯어봐도 얻을 게 없습니다.
배포
앱 마켓 심사 주기에 매이지 않으려고 PWA 로 만들고 안드로이드에서는 TWA 로 감쌌습니다. 웹으로 배포하면 현장에 그날 반영됩니다. 설치형 앱의 겉모습은 유지하면서 배포 주기는 웹을 따릅니다.
그런데 오프라인에서 사진이 사라졌다
여기까지가 원래 설계였고, 한 군데가 정확히 반대로 동작하고 있었습니다.
업로드가 실패하면 즉시 다시 쐈습니다. 그리고 세 번 실패하면 포기하고 파일을 지웠습니다. 연결이 느린 상황을 가정한 숫자였습니다.
문제는 연결이 아예 없는 상황입니다. 요청이 즉시 실패하므로 세 번이 1초 안에 소진되고, 사진은 지워집니다. 지하 주차장에서 찍은 사진이 그 자리에서 사라진다는 뜻입니다. 오프라인 큐를 만들어 놓고 정작 오프라인에서 가장 취약했습니다.
더 나빴던 건 화면입니다. 상단에 대기 장수를 띄우고 있었는데, 그 숫자가 업로드 성공과 최종 실패를 구분하지 못했습니다. 둘 다 똑같이 큐에서 빠지니 숫자가 똑같이 줄어듭니다. 점검자 입장에서는 올라간 것처럼 보입니다. 조용한 실패가 아니라 잘못된 성공 신호였습니다.
세 가지를 고쳤다
연결이 없으면 시도하지 않습니다. online 이벤트를 기다립니다. 연결
없음은 실패가 아니라 대기 사유입니다. 이것만으로 터널을 지나는 동안 큐가
증발하는 일이 없어집니다.
재시도에 지수 백오프와 지터를 넣었습니다. 지터를 섞는 이유는 사진 수십 장이 한꺼번에 실패했을 때 전부 같은 순간에 재시도하면 회선을 다시 막기 때문입니다.
최종 실패해도 파일을 버리지 않습니다. 큐에 남기고 실패로 표시만 해서, 화면이 성공과 구분해 보여주고 누르면 다시 시도합니다. 사진을 지우면 점검자가 그 차로 다시 가야 합니다. 지우는 쪽이 언제나 더 비쌉니다.
재시도 정책은 순수 함수로 떼어내 테스트를 붙였습니다. 큐 본체는 IndexedDB 와 RxJS 에 얽혀 단위 테스트가 어렵지만, "언제 얼마나 기다렸다 다시 쏘는가" 는 따로 검증할 수 있습니다.
규모와 역할
25,871 줄, 25 화면. 2025년 3월부터 2026년 6월까지 4인 팀으로 만들었고, 저는 전체 커밋의 44% 로 최다 기여자였습니다. 위에 적은 업로드 파이프라인이 제가 설계하고 구현한 부분입니다.
무엇을 배웠나
오프라인 대응은 "네트워크가 없을 때 어떻게 하나"의 문제가 아닙니다. 네트워크가 있는지 없는지를 사용자가 생각하지 않게 만드는 문제입니다.
그래서 답이 재시도 로직이 아니라 화면 설계에서 나왔습니다. 업로드 상태를 화면에서 지운 순간, 나머지는 뒤에서 조용히 처리하면 되는 일이 됐습니다.
그리고 하나 더. 실패를 성공과 같은 모양으로 보여주면, 실패를 숨긴 것보다 나쁩니다. 숨겼으면 나중에라도 이상하다고 느끼지만, 성공처럼 보이면 아무도 확인하지 않습니다. 카운터 하나가 사진 몇 장을 조용히 삼키고 있었습니다.