"전송 버튼이랑 저장 버튼은 똑같이 생겼어. 둘을 같은 동작에 묶는 게, 천 개의 잃어버린 메시지 뒤에 있는 버그야."
잃어버린 입력의 심장에 있는 혼동
'앱이 내 메시지 먹었어' 이야기는 거의 다 뿌리가 같아. 앱이 전송을 저장으로 다룬 거야. 타이핑하고, 버튼 누르고, 네 말이 존재하는 유일한 곳은 네트워크 위를 날아가는 중이었어. 요청이 실패하면 — 터널, 타임아웃, 백그라운드로 밀린 앱 — 돌아갈 데가 없었어. 먼저 적어둔 게 아무것도 없었으니까.
캡처 먼저는 이걸 거부해. 하나처럼 보이는 동작을 정해진 순서로 둘로 쪼개: 로컬에 커밋, 그다음 운송. 저장은 로컬의, 동기처럼 느껴지는 내구성 있는 쓰기야. 전송은 이미 저장된 걸 읽는 별개의, 재시도 가능한 네트워크 시도고. 네트워크는 저장 경로에 절대 안 껴 — 배달 경로에만 껴.
임계 저장 경로
사용자 의도랑 내구성 사이에 뭐가 있냐에 대한 규칙이라고 생각해. 임계 저장 경로(critical save path)는 사용자 입력이 안전해지려면 반드시 성공해야 하는 모든 단계야. 네트워크가 그 경로에 있으면, 내구성은 연결만큼만 믿을 만해 — 기차에선 동전 던지기지. 캡처 먼저는 네트워크를 임계 저장 경로에서 완전히 빼:
- 사용자 제출 → 로컬 내구 저장소에 쓰기 → 끝, 입력은 안전.
- 별개로, sync 워커가 저장소를 읽어서 배달을 시도해. 지금, 나중에, 또는 재시작 후에.
어떤 패킷이든 폰을 떠날 때쯤엔, 질문은 이미 잃을 수 없어. 배달은 실컷 실패해도 돼. 네 말을 데려가진 못해.
순서가 트릭 전부
마법은 전부 순서에 있어. await commitLocally() 가 fetch() 를 부르기 전에 끝나야 해. 뒤집거나 — 같이 돌리고 잘되길 바라면 — 넌 조용히 네트워크를 저장 경로에 다시 갖다놓은 거야. '로컬 우선' 클라이언트가 실은 로컬 우선이 아닌 제일 흔한 방식이 이거야: 로컬 쓰기는 있는데, 전송 뒤에, 영영 안 올지도 모를 성공의 캐시로 일어나.