블로그
왜 Tactic Remote를 tmux 위에 구축했는가: 세션 영속성과 터미널 아키텍처
Tactic Remote는 tmux를 사용하여 Claude Code 세션을 관리하며, 네트워크 끊김, Mac 슬립 주기, 앱 재시작에 걸쳐 영속성을 제공하면서 터미널 출력을 iPhone에 실시간으로 스트리밍합니다.
이 카테고리의 제품 및 엔지니어링 업데이트를 확인하세요.
카테고리 보기Tactic Remote를 구축하기 시작했을 때, 처음 직면한 아키텍처 결정은 근본적인 것이었습니다: 제어 장치(iPhone)의 연결이 불안정할 때 Claude Code 세션을 어떻게 살아 있고 관찰 가능하게 유지할 것인가? 답은 tmux였고, 이 결정이 제품의 거의 모든 것을 형성했습니다.
문제
Claude Code는 장기 실행 대화형 프로세스로 작동합니다. 일반적인 세션은 10분에서 60분간 지속됩니다. 이 시간 동안 Claude는 파일을 읽고, 변경을 제안하고, 명령을 실행하고, 승인을 기다리고, 피드백에 따라 반복합니다. 세션 도중에 프로세스가 종료되면 모든 컨텍스트가 손실됩니다. Claude는 처음부터 다시 시작하고, 파일을 다시 읽고, 작업에 대한 이해를 재구성해야 합니다.
로컬 터미널에서는 문제가 되지 않습니다 — Mac이 켜져 있고 프로세스가 완료될 때까지 실행됩니다. 하지만 Tactic Remote는 모바일 클라이언트를 도입합니다. iPhone은 Wi-Fi와 셀룰러 사이를 전환합니다. 화면을 잠그고 백그라운드 앱을 일시 중단합니다. 사용자는 신호가 없는 지역을 지나갑니다. Claude Code 프로세스 생명주기를 iPhone의 네트워크 연결에 묶는 아키텍처는 근본적으로 결함이 있습니다.
Claude Code가 어떤 클라이언트 연결과도 독립적으로 실행되고, 클라이언트가 실행 중인 세션에 영향을 주지 않고 연결하고 분리할 수 있어야 했습니다.
왜 tmux인가
tmux는 2007년부터 프로덕션 환경에서 사용되어 온 터미널 멀티플렉서입니다. 터미널 세션의 개념을 그것을 표시하는 터미널 에뮬레이터와 분리합니다. tmux 세션은 호스트 머신의 서버 프로세스로 실행됩니다. 클라이언트가 세션에 연결하여 보고 상호작용하며, 분리해도 실행 중인 프로세스에 영향을 주지 않습니다.
tmux로 결정하기 전에 여러 대안을 평가했습니다:
screen — 원조 터미널 멀티플렉서. 기능적이지만 유지보수가 덜 활발하고 스크립팅 API가 더 제한적입니다. tmux의 구조화된 명령 인터페이스가 프로그래밍 방식의 제어를 상당히 쉽게 만들었습니다.
커스텀 PTY 관리 — node-pty를 사용한 원시 의사 터미널 솔루션을 프로토타이핑했습니다. 기본적인 경우에는 작동했지만 세션 영속성, 창 관리, 출력 버퍼링을 재구현해야 했습니다. 더 열악한 tmux를 처음부터 만들고 있었습니다.
컨테이너 기반 격리 — PTY가 연결된 Docker 컨테이너에서 Claude Code를 실행하는 방법. 이는 오버헤드를 추가하고, 파일 시스템 접근을 복잡하게 하며(Claude Code는 실제 프로젝트 파일을 읽고 써야 합니다), 해결하는 것보다 더 많은 문제를 만드는 추상화 계층을 도입합니다.
nohup / disown — 단순한 백그라운드 프로세스 관리. 출력 캡처 없음, 재연결 없음, 프로그래밍 방식의 제어 없음. 대화형 세션에는 부적합합니다.
tmux는 필요한 모든 것을 기본으로 제공했습니다: 세션 영속성, 프로그래밍 방식의 패인 제어, 출력 캡처, 안정적이고 문서가 잘 갖춰진 API. 트레이드오프는 개발자의 Mac에 tmux가 설치되어야 한다는 것이지만, brew install tmux는 일회성 설정 비용이며 수용 가능하다고 판단했습니다.
아키텍처
Tactic Remote의 tmux 통합은 세 가지 계층에 걸쳐 작동합니다:
계층 1: 세션 관리
Tactic Remote를 통해 Claude Code 세션을 시작하면, Mac 컴패니언이 고유 식별자를 가진 새 tmux 세션을 생성합니다. Claude Code는 이 세션의 기본 패인에서 실행됩니다.
-d 플래그가 핵심입니다 — 분리 모드로 세션을 생성합니다. 세션이 존재하고 실행되는 데 터미널 에뮬레이터가 연결될 필요가 없습니다. Claude Code가 즉시 시작되고, Mac 컴패니언이 백그라운드에서 모니터링합니다.
각 세션은 공유 세션의 패인이 아닌 전용 tmux 세션을 받습니다. 이는 세션별 환경 변수, 독립적인 스크롤 버퍼, 세션 간 간섭 위험 없음 등 깔끔한 격리를 제공합니다.
계층 2: 출력 캡처와 스트리밍
Mac 컴패니언은 tmux 패인에서 터미널 출력을 지속적으로 캡처하고 WebSocket을 통해 연결된 iPhone 클라이언트에 스트리밍합니다. 이것이 iPhone에서 실시간으로 Claude Code의 출력을 볼 수 있게 하는 메커니즘입니다.
출력 캡처는 tmux capture-pane을 사용하며, 가시적인 패인 콘텐츠와 스크롤 버퍼를 커버하는 좌표를 지정합니다. 컴패니언은 다음을 수행하는 캡처 루프를 실행합니다:
- 구성 가능한 간격(기본값: 200ms)으로 현재 패인 콘텐츠를 캡처합니다.
- 캡처된 콘텐츠를 마지막 알려진 상태와 비교합니다.
- 연결된 클라이언트에 변경분만 전송합니다.
- 재연결하여 따라잡아야 하는 클라이언트를 위해 최근 출력의 링 버퍼를 유지합니다.
200ms 캡처 간격은 응답성과 CPU 사용량 사이의 균형을 나타냅니다. 이 속도에서 터미널 업데이트는 iPhone에서 거의 실시간으로 느껴지며, 최신 Mac에서 2% 미만의 CPU를 소비합니다. 더 빠른 업데이트를 선호하는 사용자는 간격을 100ms로 줄일 수 있고, 배터리로 구동하는 사용자는 500ms로 늘릴 수 있습니다.
계층 3: 입력 라우팅
iPhone에서 입력을 보낼 때 — 승인 응답, 입력된 메시지, 세션 제어 명령 등 — Mac 컴패니언은 tmux send-keys를 사용하여 적절한 tmux 패인으로 라우팅합니다. 이는 실제 입력이 WebSocket을 통해 네트워크를 건너 tmux 세션으로 들어가는 동안 직접 터미널 연결의 환상을 유지합니다.
승인 응답은 특수한 경우입니다. 원시 키스트로크로 전송되는 대신, hook 시스템에 의해 처리되고 Claude Code의 권한 시스템이 기대하는 구조화된 응답으로 주입됩니다. 이는 승인이 원자적이며 부분적인 네트워크 전달로 손상되지 않도록 보장합니다.
실제 영속성
tmux 아키텍처는 여러 수준에서 영속성을 제공합니다:
iPhone 앱이 닫힌 경우. tmux 세션은 계속 실행됩니다. Claude Code는 계속 작업하고, 승인은 Mac 컴패니언에서 큐에 들어갑니다. iPhone 앱을 다시 열면 WebSocket에 재연결하고, 버퍼된 출력을 수신하며, 이전 상태로 돌아갑니다.
iPhone이 네트워크를 잃은 경우. 세션의 관점에서 앱이 닫힌 것과 동일합니다. WebSocket이 끊기고, 출력이 Mac에서 버퍼되며, 재연결 시 상태가 복원됩니다.
Mac이 슬립에 들어간 경우. tmux 세션은 슬립과 웨이크 주기에서 살아남습니다. Mac이 깨어나면 tmux 서버 프로세스가 재개되고, 실행 도중이던 Claude Code 프로세스가 계속됩니다. Mac 컴패니언이 WebSocket 리스너를 재설정하고 iPhone에 재연결합니다.
Mac 컴패니언이 크래시한 경우. tmux 세션은 독립적으로 살아남습니다. Mac 컴패니언을 재시작하면 claude-remote- 접두사를 가진 기존 tmux 세션을 감지하고, 재연결하여 스트리밍을 재개합니다. Claude Code 컨텍스트가 손실되지 않습니다.
Mac이 재부팅된 경우. tmux 세션이 살아남지 못하는 유일한 시나리오입니다. tmux 세션은 메모리에만 존재하기 때문입니다. Claude Code를 재시작해야 합니다. 이를 명확히 문서화했으며, 향후 버전을 위해 tmux-resurrect 통합을 탐색하고 있지만, 대화형 AI 세션(단순한 셸 상태가 아닌)을 복원하는 복잡성으로 인해 간단하지 않습니다.
세션 생명주기
Tactic Remote 세션은 다음 상태를 거칩니다:
| 상태 | tmux 상태 | iPhone 상태 |
|---|---|---|
| 시작 중 | 세션 생성됨, Claude Code 실행 중 | 로딩 스피너 |
| 활성 | 프로세스 실행 중, 출력 흐름 중 | 실시간 터미널 뷰 |
| 승인 대기 | 프로세스가 hook에서 차단됨 | 승인 알림 |
| 일시 중지 (클라이언트 연결 끊김) | 실행 중, 출력 버퍼링 중 | 연결 안 됨 |
| 완료 | 프로세스 종료, 세션 유지 중 | 세션 요약 |
| 만료 | 유지 기간 후 세션 삭제 | 보관됨 |
완료된 세션은 구성 가능한 기간(기본값: 30분) 동안 tmux에 유지되므로 Claude Code가 종료된 후에도 최종 출력을 검토할 수 있습니다. 유지 기간이 지나면 tmux 세션이 삭제되고 출력 로그가 보관됩니다.
관측 가능성
tmux는 커스텀 PTY 솔루션으로는 달성하기 어려운 내부 조사 기능을 제공합니다:
- 세션 목록 —
tmux list-sessions가 모든 활성 Tactic Remote 세션의 즉각적인 인벤토리를 제공합니다. - 패인 크기 — iPhone의 디스플레이 특성에 맞게 패인 크기를 조회하고 설정할 수 있어, 출력 줄바꿈이 정확합니다.
- 프로세스 모니터링 —
tmux list-panes에 포맷 플래그를 사용하여 별도의 PID 추적 시스템을 유지하지 않고 Claude Code 프로세스를 모니터링할 수 있습니다. - 스크롤 버퍼 접근 — 전체 스크롤 버퍼를 검색하고 조회할 수 있으며, iPhone의 세션 히스토리 뷰를 지원합니다.
성능 특성
tmux를 통한 실행은 직접 터미널과 비교하여 최소한의 오버헤드를 추가합니다:
| 지표 | 직접 터미널 | tmux 세션 |
|---|---|---|
| 프로세스 시작 | ~800ms | ~850ms |
| 키스트로크 지연 | <1ms | ~2ms |
| 세션당 메모리 오버헤드 | — | ~3MB |
| CPU 오버헤드 (유휴 세션) | — | <0.1% |
오버헤드는 실제로 측정할 수 없습니다. tmux는 거의 20년에 걸친 프로덕션 사용을 통해 최적화되었으며, 도입되는 추가 간접 계층은 사용자에게 보이지 않습니다.
배운 교훈
tmux 위에 구축하는 것은 올바른 선택이었지만, 도전이 없지는 않았습니다:
터미널 인코딩 엣지 케이스. Claude Code의 출력에는 tmux의 터미널 에뮬레이션과 예기치 않게 상호작용하는 ANSI 이스케이프 시퀀스가 포함되는 경우가 있습니다. iPhone에 스트리밍하기 전에 출력을 정리하는 정규화 레이어를 유지하고 있습니다.
macOS에서의 tmux 설치. tmux는 macOS에 사전 설치되어 있지 않습니다. 번들로 포함하는 것을 고려했지만 그러지 않기로 했습니다 — 개발자들은 일반적으로 자체적으로 tmux 설치를 관리하는 것을 선호하며, 번들링은 이미 tmux를 설치한 사용자와 버전 충돌을 일으킬 수 있습니다.
대규모에서의 capture-pane 성능. 많은 동시 세션에서 캡처 루프의 CPU 사용량이 선형적으로 증가합니다. 최근 출력 변경이 없는 세션의 빈도를 줄이는 적응형 캡처 레이트를 구현했습니다.
tmux는 잘 작동할 때 존재를 드러내지 않는 인프라입니다. 대부분의 Tactic Remote 사용자는 tmux가 관여하고 있다는 것을 모르거나 신경 쓰지 않습니다 — 그들이 보는 것은 전화기나 네트워크가 어떻게 되든 안정적으로 작동하는 세션입니다. 그것이 우리가 목표로 한 결과입니다.