블로그
대화 기록은 서버에 맡기세요: 모바일 AI 코딩을 위한 세션 복원 설계
Tactic Remote 1.8.2가 채팅 기록을 모바일 기기에서 서버로 옮긴 이유, 그리고 이 결정이 순서 뒤바뀜과 기록 손실 문제를 구조적으로 제거한 방법을 설명합니다.
이 카테고리의 제품 및 엔지니어링 업데이트를 확인하세요.
카테고리 보기이 글은 하나의 아키텍처 결정에 관한 이야기입니다. Tactic Remote 1.8.2가 채팅 대화 기록을 모바일 기기에서 서버로 옮긴 이유, 그리고 이 결정이 반복적으로 수정해 왔던 여러 문제들을 근본적으로 해결한 방법에 대해 다룹니다.
처음부터 실패할 수밖에 없었던 방식
1.8.2 이전에는 채팅 기록 전체를 모바일 기기가 실시간으로 조립했습니다. 서버는 agent의 스트리밍 이벤트를 기기로 전송하고, 기기는 이 이벤트들을 차례로 말풍선으로 렌더링해 화면에 보이는 대화를 구성했습니다.
데스크톱에서는 연결이 안정적이므로 이 방식이 어느 정도 통했습니다. 그러나 모바일에서는 구조적인 결함이 있었습니다. 기록이 현재의 연결, 현재의 세션에만 존재한다는 점입니다. 문제는 잇따라 나타났습니다.
- 재연결 후 새 연결은 이전 이벤트를 받을 수 없어, 기록이 텅 비거나 불완전한 상태가 됩니다.
- 앱을 종료했다가 다시 열면 메모리 속 대화가 사라지고 처음부터 다시 시작해야 합니다.
- 같은 세션에 여러 기기가 연결되면, 각 기기는 자신이 접속한 이후의 이벤트만 볼 수 있어 각자 다른 내용을 보게 됩니다.
더 까다로운 문제도 있었는데, 순서에 관한 것이었습니다. 메시지를 전송할 때 기기는 낙관적으로 사용자 말풍선을 먼저 표시하지만, agent의 첫 번째 응답은 프로세스가 실제로 시작된 후에야 도착합니다. 이 사이에 약간의 타이밍 차이만 생겨도, 예를 들어 큐에 쌓인 메시지가 재연결 후 늦게 도착하면, 사용자 말풍선이 응답 아래로 밀려 내려갈 수 있었습니다. 이 "사용자 말풍선이 맨 아래로 내려가는" 문제는 여러 번 수정했지만, 매번 증상만 치료하는 것이었습니다. 근본 원인은 구조적인 것이었습니다. 이 설계 어디에도 "이 대화의 실제 순서"를 책임지는 주체가 없었습니다.
tmux에서 영감을 얻다
Tactic Remote의 터미널 모드가 안정적인 이유는 tmux 덕분입니다. tmux의 모델은 단순하면서도 강력합니다. 터미널 버퍼가 유일한 진실의 원천이며, 연결(attach)한다는 것은 이 사실을 그대로 재생해서 돌려받는 것입니다. 연결이 끊겨도, 기기를 바꿔도, 다시 시작해도 같은 버퍼에 재접속하는 것뿐이라 기록은 자연스럽게 남아 있습니다.
채팅 모드에는 바로 이 계층이 없었습니다. 그래서 1.8.2의 설계는 이 발상을 채팅에 적용하는 것이었습니다.
서버가 권위 있는 대화 기록을 보관하고, 클라이언트가 접속할 때 이를 완전하게 복원합니다.
서버가 순서의 권위자가 되는 순간, 앞서 언급한 문제들은 하나씩 수정해야 할 버그가 아니라 구조적으로 발생할 수 없는 것이 됩니다.
구체적인 구현 방법
서버: 세션마다 링 버퍼 하나
서버는 각 채팅 세션에 대한 대화 기록을 유지합니다. 브로드캐스트되는 모든 이벤트는 순서대로 링 버퍼에 추가됩니다. 버퍼에는 두 가지 상한이 있습니다. 이벤트 수 제한과 총 바이트 수 제한이며, 초과 시 가장 오래된 것부터 잘라냅니다. 단, 세션 시작 시의 초기화 이벤트는 복원의 앵커이므로 절대 잘라내지 않습니다. 잘라냄이 발생하면 버퍼에 "잘림" 표시가 붙어, 복원 시 클라이언트가 상단에 "더 오래된 메시지는 생략됨"이라고 솔직하게 표시할 수 있습니다. 처음부터인 척 가장하지 않습니다.
중요한 변경점이 있습니다. 사용자가 보낸 메시지도 이제 서버가 순서가 있는 이벤트로 생성해 대화 기록에 남깁니다. 이전에는 사용자 메시지가 기기의 낙관적 렌더링에만 존재했고 서버는 이를 전혀 몰랐습니다. 실행 중 삽입되는 메시지(steer)는 기록조차 없었습니다. 이제 서버가 순서의 권위자가 되었으므로, 사용자의 모든 메시지는 실제로 발생한 위치에 기록됩니다. "사용자 말풍선이 맨 아래로 내려가는" 오래된 문제는 구조적으로 사라집니다.
복원: 원시 이벤트 재전송 대신 봉투 방식
처음 구상한 방안은 접속 시 원시 이벤트를 다시 보내는 것이었습니다. 이 아이디어는 금방 폐기됐습니다. 구버전 클라이언트는 모르는 이벤트를 무시하지만, 재전송되는 이벤트는 클라이언트가 아는 이벤트들이라 새로운 실시간 메시지로 다시 추가되어 기록이 두 배가 됩니다.
실제로 채택한 방안은 전체 기록을 새로운 이벤트 하나에 담아 전송하는 것입니다. structured_replay라는 이벤트에 시퀀스 번호, 마지막 배치 여부, 잘림 여부, 현재 실행 중인지 여부, 그리고 이벤트 목록이 포함됩니다. 구버전 클라이언트는 이 새 이벤트를 모르므로 그냥 무시하고 아무 문제가 없습니다. 신버전 클라이언트는 이 "봉투"를 열어 안의 이벤트들을 동일한 처리 흐름에 다시 공급해 전체 대화를 재구성합니다.
iOS의 WebSocket 단일 프레임 수신 한도가 약 1 MiB이므로, 복원 시 256 KB 단위로 분할해 전송하고 시퀀스 번호로 순서를 표시합니다. 이미 처리된 도구 승인의 경우, 복원 시 "허용됨 / 거부됨" 도장이 찍혀, 과거 승인 카드가 상태 없이 아직 조작을 기다리는 카드로 잘못 렌더링되지 않도록 합니다.
클라이언트: 접속하면 재구성, 부작용은 차단
클라이언트는 시퀀스 번호 0인 복원 데이터를 받으면 로컬 대화를 먼저 비운 다음, 이벤트 목록을 동일한 처리 함수에 차례로 공급해 재구성합니다. 즉, 복원은 실시간과 완전히 동일한 렌더링 경로를 사용하며 별도의 로직이 필요하지 않습니다.
반드시 처리해야 할 세부 사항이 있습니다. 복원 중에는 실시간 상황에서만 발동해야 하는 부작용들을 차단해야 합니다. 큐 자동 전송, 타임아웃 워치독, 실행 상태 전환, 승인 자동 준비 등입니다. 플래그 하나로 이들을 모두 막아 두고, 마지막 배치가 도착하면 봉투 안의 권위 있는 실행 상태를 채택합니다.
전송 시 낙관적 사용자 말풍선은 그대로 유지했습니다. 첫 번째 메시지는 agent 프로세스 시작까지 1~2초가 걸리는데, 복원만 기다리면 공백이 생겨 경험이 나빠지기 때문입니다. 대신 이를 "확인 대기 중" 플레이스홀더로 바꿨습니다. 서버에서 확인이 오면 이 플레이스홀더를 제거하고, 권위 있는 기록의 실제 위치에 다시 삽입합니다. 순서 뒤바뀜은 구조적으로 지속될 수 없게 됩니다.
호환성: "알 수 없는 이벤트는 무시"가 설계 결정인 이유
복원 방안이 안전하게 적용될 수 있었던 것은 오래전부터 세워 둔 약속 덕분입니다. 클라이언트는 모르는 이벤트를 일률적으로 무시한다는 것입니다. 바로 이 약속이 서버가 structured_replay 같은 새 이벤트를 도입할 때 아직 업그레이드되지 않은 구버전 클라이언트를 전혀 건드리지 않을 수 있게 해 줍니다. 구버전은 복원 기능을 볼 수 없지만, 그 때문에 오류가 생기지도 않습니다.
iOS 앱, Mac / Linux / Windows 세 플랫폼 서버, 그리고 npm CLI가 각자 독립적으로 배포되고 버전이 오랫동안 엇갈릴 수 있는 시스템에서, "추가만 하고 수정하지 않으며 알 수 없는 것은 무시한다"는 프로토콜 원칙은 서버가 먼저 나아가고 클라이언트가 뒤따를 수 있는 전제 조건입니다.
한계와 솔직한 트레이드오프
이 설계에 한계가 없는 것은 아닙니다. 오히려 명확하게 밝히는 것이 낫다고 생각합니다.
- 서버 버퍼에는 상한이 있습니다. 매우 긴 세션에서는 가장 초기 기록 일부가 잘릴 수 있습니다. 복원 시 "더 오래된 메시지는 생략됨"으로 명확하게 알리며, 완전한 척 가장하지 않습니다.
- 회수된 세션은 처음부터 시작합니다. 세션이 오랫동안 구독자 없이 방치되면 서버가 이를 회수합니다. 이후
claude --resume으로 복원된 세션은 버퍼가 빈 상태에서 재시작하며 이전 기록은 재생되지 않습니다. 이는 의도적인 트레이드오프입니다. 제한적인 기록 보존을 선택하는 대신 메모리 사용량을 제어할 수 있습니다. - 클라이언트 로컬 영속성은 다음 단계입니다. 현재 이 권위 있는 기록은 서버가 온라인인 동안만 존재합니다. 기록을 클라이언트 로컬에 추가로 저장하는 것은 다음 단계의 개선이며, 이번 버전에는 포함되지 않습니다.
언젠가 기록이 갑자기 일부 사라진 것을 발견하고 스스로 이유를 추측하게 되는 것보다, 미리 이 한계들을 분명히 밝혀 두는 것이 낫습니다.
"접속하면 즉시 복원"되는 효과를 직접 경험해 보고 싶으신가요? iOS 앱은 App Store에서 무료로 다운로드할 수 있으며, 서버 앱과 CLI도 무료입니다. 이번 버전의 또 다른 난제였던 "비즈니스 코드 바깥에서 발생한" 화면 멈춤 현상에 관심이 있으시다면, SwiftUI 화면 멈춤 추적를 읽어 보세요.