"전송 버튼이랑 저장 버튼은 똑같이 생겼어. 이 둘을 한 동작에 묶어버린 게, 사라진 메시지 수천 개의 진범이야."
입력이 날아가는 자리엔 늘 이 혼동이 있어
'앱이 내 메시지 먹었어' 하는 사연은 거의 다 뿌리가 같아. 앱이 전송을 저장으로 다룬 거야. 타이핑하고 버튼 눌렀는데, 그 순간 네 말이 존재하는 데라곤 네트워크 위를 날아가는 중인 패킷뿐이었던 거지. 그 요청이 실패하면, 터널이든 타임아웃이든 앱이 백그라운드로 밀렸든, 돌아갈 데가 없어. 미리 적어둔 게 아무것도 없었으니까.
캡처 먼저는 그걸 거부해. 하나로 보이던 동작을 순서가 정해진 둘로 쪼개. 로컬에 커밋하고, 그다음에 보내기. 저장은 로컬에서 바로 끝나는, 안 없어지는 쓰기야. 전송은 그렇게 저장된 걸 읽어다 보내는 별개의 시도고, 몇 번이든 다시 할 수 있어. 네트워크는 저장 경로엔 절대 안 껴. 배달 경로에만 끼고.
임계 저장 경로
사용자가 뭘 하려는 마음이랑 그게 안 없어지는 상태 사이에 뭐가 끼어 있냐, 그 규칙이라고 생각하면 돼. 임계 저장 경로(critical save path)는 사용자 입력이 안전해지려면 반드시 성공해야 하는 단계 전부야. 네트워크가 그 경로에 끼어 있으면, 입력이 안 없어질 확률은 딱 연결이 버텨줄 확률만큼이고. 기차에선 그냥 동전 던지기지. 캡처 먼저는 네트워크를 그 경로에서 아예 빼.
- 사용자 제출 → 로컬 저장소에 쓰기 → 끝. 입력은 안전.
- 그거랑 별개로, sync 워커가 저장소를 읽어서 배달을 시도해. 지금이든, 나중이든, 앱 다시 켠 다음이든.
패킷이 폰을 떠날 때쯤이면 질문은 이미 잃을 수가 없어. 배달은 실컷 실패해도 돼. 네 말을 데려가진 못하니까.
순서가 전부야
마법은 전부 순서에 있어. await commitLocally() 가 fetch() 를 부르기 전에 끝나야 해. 순서를 뒤집거나 둘을 같이 돌려놓고 잘되기를 바라면, 네트워크를 슬그머니 저장 경로에 도로 갖다 놓은 거야. 로컬 우선이라고 해놓고 사실은 아닌 경우가 제일 흔하게 이런 모양이야. 로컬 쓰기가 있긴 한데 전송 뒤에 붙어 있어서, 영영 안 올지도 모를 성공을 캐시하는 역할밖에 못 하는 거.