시간정원
← Blog

sideproject · 2026-08-03 · 조회수 5

로컬에서 먼저 바꾸고 서버는 나중에 따라오게 — 우리콕 개발기 3

우리콕 개발기 3편이다. 한 공간의 메모를 두 기기에서 맞추는 것만으로도 동기화 문제가 생겼다. 여러 공간을 오갈 수 있게 되자 어떤 공간의 데이터를 화면에 남기고 폐기할지도 함께 해결해야 했다. 앱이 백그라운드로 갔다 돌아오는 순간, 네트워크 응답이 늦거나 Realtime 연결이 잠시 끊기면 새 메모와 남은 메모의 위치가 흔들렸다.

서버 응답보다 로컬 상태를 먼저 바꾼다

읽음이나 완료를 누를 때 Supabase 응답을 기다린 뒤 화면을 바꾸면 사용자는 네트워크 지연을 그대로 느낀다. 그래서 Drift 트랜잭션 안에서 로컬 메모와 outbox 명령을 함께 기록하고 화면을 즉시 갱신한다. 서버 전송은 별도 큐가 담당한다.

사용자 동작
  → Drift 로컬 상태 변경
  → outbox 명령 저장
  → 화면 즉시 갱신
  → 서버 전송

모든 명령에는 클라이언트가 만든 command_id가 있다. 서버는 이미 처리한 ID를 기록하므로 재시도 중 같은 요청이 도착해도 한 번만 반영한다.

같은 메모의 완료 → 다시 열기 순서가 뒤집히지 않도록 memo ID별 명령은 직렬 처리한다. 서로 다른 메모는 독립적으로 진행할 수 있다. 실패한 명령은 재시도 간격을 점차 늘리고, 정해진 한도를 넘으면 중단한다. 영구 실패한 명령이 있으면 같은 메모의 뒤쪽 명령도 dead letter로 보내 조용한 순서 역전을 막는다.

Realtime은 놓친 기록을 재생하지 않는다

Realtime은 새 변경을 빠르게 알려주지만 연결이 끊긴 동안 발생한 이벤트 전달까지 보장하지 않는다. 앱을 처음 켤 때는 공간 데이터를 한 번 가져오고, 이후에는 updated_at + id 커서로 변경분만 조회한다. 재연결 시 REST catch-up을 먼저 실행한 다음 Realtime 스트림을 이어 붙인다.

로컬 DB가 화면의 기준이기 때문에 네트워크가 끊겨도 마지막 목록은 남는다. 상대가 오프라인 동안 만든 메모는 앱 복귀 시 증분 동기화로 들어온다.

공간이 여러 개일 때

메인 화면 상단의 공간명을 누르면 그 자리에서 공간을 바꾼다. 공간 탭은 현재 공간의 멤버와 초대, 새 공간 추가를 관리한다. 다른 공간에서 온 새 메모 수는 공간 선택기에만 표시하며 전체 메모 수가 아니라 내가 아직 확인하지 않은 수만 센다.

여러 공간과 읽지 않은 메모 수를 보여주는 공간 선택기

일반 멤버와 방장 모두 초대 코드를 만들 수 있다. 일반 멤버가 나가면 자신의 membership만 제거하고, 방장이 공간을 종료하면 모든 membership과 활성 초대를 제거해 누구도 더 이상 접근하지 못하게 한다. 로컬 Drift 캐시와 outbox도 같이 비워 탈퇴한 공간의 내용이 기기에 남지 않게 했다.

방장과 멤버, 초대 기능을 관리하는 공간 화면

공간 전환과 앱 재개가 겹칠 때 이미 폐기한 저장소가 늦게 notifyListeners를 호출하는 버그도 있었다. 새 저장소를 먼저 화면에 연결하고 이전 저장소는 프레임 교체 뒤 폐기했으며, 초기화·catch-up·재개 작업마다 폐기 상태를 확인하도록 보강했다.

이 구조 덕분에 화면은 로컬에서 즉시 반응하고, Supabase는 기기 사이의 최종 상태를 맞추는 역할에 집중한다. 아직 동기화 상태를 사용자가 직접 확인하는 진단 UI는 최소 수준이지만, 실제 부부 사용에서 명령이 유실되지 않는 기반은 갖췄다.

댓글

목차