"outbox 하나에 신뢰 모델 전체가 들어가. 커밋은 끝났고 배달만 기다리는 것들."
왜 localStorage 말고 IndexedDB 냐
폰에서 안 없어지게 캡처하려면 저장소 선택이 장식이 아냐. localStorage 는 동기고, 문자열만 담고, 용량도 작아. 메인 스레드를 막고 이미지 blob 은 아예 못 담고. IndexedDB 가 맞는 도구야. 비동기라 UI 를 절대 안 얼리고, 트랜잭션을 지원하고(지난 레슨의 통째 커밋이 여기 기대고 있어), 구조화된 객체랑 바이너리 Blob 을 그대로 저장해. 앱을 껐다 켜도 이미지 붙은 질문을 안전하게 쥐고 있는 게 할 일의 전부인 클라이언트한테, 이 조합은 있으면 좋은 정도가 아니라 바닥이야.
outbox 패턴
핵심 구조가 outbox 야. 캡처한 턴들이 sync 상태 딱지를 하나씩 달고 줄 서 있는, 안 없어지는 큐. 메시징 쪽에서 쓰는 outbox 패턴을 기기로 가져온 거지. 흐름은 어느 지점에서 끊겨도 안전한 루프야.
- Enqueue: 제출된 턴이 blob 이랑 통째로 묶여서
pending상태로 outbox 에 들어가. - Drain: 워커가
pending턴을 집어서 배달을 시도해.sending으로 갔다가answered나failed로 넘어가고. - Resume: 앱을 켤 때 워커가 아직
answered가 아닌 것들을 outbox 에서 다시 훑어서 멈춘 데부터 이어가.
outbox 가 안 없어지니까, 이 단계들 중 뭐도 앞 단계가 같은 세션에서 끝났다고 전제하지 않아. enqueue 랑 drain 사이에 앱을 닫아 버리면 drain 은 그냥 다음에 켤 때 일어나. 큐가 곧 기억이야.
outbox 는 폰 쪽 정본
기기 입장에서 outbox 는 서버가 아는 걸 베껴둔 캐시가 아냐. 서버가 들었든 말든, 사용자가 뭘 물었는지에 대해선 여기가 정본이야. 그래서 몇 시간을 오프라인으로 버틸 수 있어. 로컬 상태가 맞는지 확인받으려고 서버를 찾을 일이 없으니까. (canonical 대화의 진실 출처는 여전히 백엔드야. outbox 는 폰 쪽에서 안 없어지게 붙잡아 두는 층이고, 하는 일이 달라. 트랙 5 가 그 선을 정확히 그어.)