목록으로 돌아가기

블로그

Mac 컴패니언 앱의 내부 구조: 경량 세션 매니저 아키텍처

Mac 컴패니언 앱은 Tactic Remote의 조용한 핵심 기반입니다. tmux 세션을 관리하고, 내장 HTTP/WebSocket 서버를 실행하며, AI 작업 중 Mac이 잠들지 않도록 유지하는 메뉴 바 유틸리티입니다.

게시일 태그: architecture / mac / engineering
블로그

이 카테고리의 제품 및 엔지니어링 업데이트를 확인하세요.

카테고리 보기

Mac 컴패니언 앱은 Tactic Remote에서 가장 눈에 띄지 않는 부분이면서도 가장 중요한 부분일 수 있습니다. 메뉴 바에서 실행되며, 머신의 모든 Claude Code 세션을 관리하고, iPhone이 연결하는 API를 제공하며, 잠자기 주기와 네트워크 변경에 걸쳐 세션을 유지하는 눈에 띄지 않지만 중요한 작업을 처리합니다. 이 글에서는 작동 방식과 각 아키텍처 결정의 이유를 설명합니다.

설계 제약 조건

개발 초기에 세 가지 제약 조건을 설정했습니다:

  1. 유휴 시 앱의 존재를 느낄 수 없어야 합니다. 개발자들은 이미 수십 개의 백그라운드 프로세스를 실행합니다. 유휴 상태에서 눈에 띄는 CPU나 메모리를 소비하는 세션 매니저는 용납할 수 없습니다.
  2. 여러 동시 세션을 처리해야 합니다. 많은 개발자가 Claude Code 작업을 2~3개 동시에 실행합니다. 기능 브랜치용, 테스트용, 문서용으로 나누어서 사용합니다.
  3. Mac 잠자기와 네트워크 변경에 우아하게 대응해야 합니다. 개발자가 노트북을 닫고 카페로 이동한 후 다시 열면, 모든 것이 몇 초 안에 재연결되어야 합니다.

이러한 제약 조건이 아키텍처의 모든 계층을 형성했습니다.

내장 서버 모델

컴패니언 앱은 별도의 서버 프로세스를 실행하는 대신 HTTP와 WebSocket 서버를 직접 내장합니다. 이는 독립 데몬을 별도로 배포하는 방식 대비 의도적으로 선택한 것입니다.

왜 내장 방식인가? 별도의 데몬은 자체 라이프사이클 관리가 필요합니다. launchd 설정, 프로세스 모니터링, 로그 로테이션, 크래시 복구 등 각각이 잠재적 장애 지점이자 지원 부담입니다. 서버를 앱 자체에 내장함으로써 라이프사이클이 단순해집니다: 앱이 실행되면 서버가 실행됩니다. 앱이 종료되면 모든 것이 깨끗하게 중지됩니다.

내장 서버는 설정 가능한 포트(기본값 48736)에 바인딩되며, localhost만(별도의 네트워크 프록시 레이어를 사용하는 로컬 네트워크 모드용) 또는 모든 인터페이스에 바인딩할 수 있습니다. 세션 관리 및 상태 조회를 위한 REST API와 실시간 터미널 스트리밍 및 승인 푸시를 위한 WebSocket 엔드포인트를 제공합니다.

유휴 시 서버는 소켓을 열어두지만 작업을 수행하지 않습니다. 벤치마크에서 활성 세션이 없는 컴패니언 앱은 약 18 MB의 메모리와 무시할 수 있는 수준의 CPU를 소비하며, 일반적인 메뉴 바 유틸리티와 동등합니다.

세션 백본으로서의 tmux

Tactic Remote가 관리하는 모든 Claude Code 세션은 tmux 세션 내에서 실행됩니다. screen이나 직접적인 PTY 관리 대신 tmux를 선택한 이유는 다음과 같습니다:

세션 지속성. tmux 세션은 이를 생성한 프로세스와 독립적으로 존속합니다. 컴패니언 앱이 크래시 후 재시작되어도 tmux 세션과 그 Claude Code 프로세스는 방해 없이 계속 실행됩니다. 앱은 시작 시 기존 세션에 다시 연결하기만 하면 됩니다.

간섭 없는 캡처. tmux의 capture-pane 명령을 사용하면 입력을 주입하거나 실행 중인 프로세스를 수정하지 않고 터미널 내용을 읽을 수 있습니다. 이는 매우 중요합니다. Claude Code의 작동을 방해할 위험 없이 터미널 출력을 iPhone으로 스트리밍해야 하기 때문입니다.

명명된 세션. 각 Claude Code 세션에는 예측 가능한 tmux 세션 이름(예: claude-remote-0, claude-remote-1)이 부여되어, 프로그래밍 방식으로 세션을 열거하고, 연결하고, 관리하기가 간편합니다.

성숙하고 보편적. tmux는 macOS에 기본 포함되어 있거나 Homebrew를 통해 간단히 설치할 수 있습니다. 수십 년의 안정성을 갖추고 있으며, 그렇지 않으면 우리가 직접 해결해야 할 엣지 케이스(터미널 크기 조정, Unicode, 이스케이프 시퀀스)를 처리합니다.

컴패니언 앱은 설정 가능한 간격(기본값 200ms)으로 활성 tmux 세션을 폴링하여 최신 터미널 내용을 캡처합니다. 내용이 변경되면 WebSocket을 통해 연결된 클라이언트에 차이점을 푸시합니다. 이 폴링 방식은 의도적으로 단순한 접근법입니다. 파일 시스템 기반 알림 메커니즘을 평가했지만, macOS 버전 간에 이 간격에서의 직접적인 폴링보다 덜 안정적이었습니다.

세션 라이프사이클 관리

Tactic Remote에서 Claude Code 세션은 정의된 라이프사이클을 거칩니다:

  1. 생성 — iPhone이 "세션 시작" 요청을 전송합니다. 컴패니언 앱이 새 tmux 세션을 생성하고 지정된 작업 디렉토리와 작업 설명으로 Claude Code를 실행합니다.
  2. 활성 모니터링 — 터미널 내용이 캡처되고 스트리밍됩니다. Claude Code의 hook 시스템에서 오는 승인 요청이 가로채져 WebSocket과 푸시 알림을 통해 iPhone으로 전달됩니다.
  3. 유휴 감지 — 세션의 터미널 출력이 설정 가능한 기간 동안 변경되지 않으면, 리소스 사용을 줄이기 위해 폴링 빈도를 낮춥니다.
  4. 완료 — Claude Code가 종료됩니다. 컴패니언 앱이 종료를 감지하고, 최종 출력을 캡처하며, iPhone에 알립니다.
  5. 정리 — tmux 세션은 설정 가능한 보존 기간(기본값 30분) 동안 유지되어 사용자가 출력을 검토할 수 있으며, 이후 자동으로 정리됩니다.

여러 동시 세션을 관리한다는 것은 이러한 라이프사이클을 독립적으로 다중화하는 것을 의미합니다. 각 세션은 자체 캡처 루프, 자체 WebSocket 채널, 자체 상태 머신을 갖습니다. 컴패니언 앱은 현재 최대 8개의 동시 세션을 지원하며, 이 제한은 아키텍처 제약이 아닌 실제 리소스 테스트에 기반합니다.

Mac 잠자기 방지

이것은 덜 명확한 엔지니어링 과제 중 하나였습니다. macOS는 비활동을 감지하면 적극적으로 잠자기에 들어가며, tmux에서 실행 중인 Claude Code 작업은 macOS가 "활동"으로 인식하는 종류의 사용자 입력을 생성하지 않습니다.

컴패니언 앱은 적어도 하나의 Claude Code 세션이 활발히 실행 중일 때 IOPMAssertionCreateWithName API를 사용하여 전원 어서션을 유지합니다. 어서션 유형은 kIOPMAssertPreventUserIdleSystemSleep으로, 디스플레이가 꺼지는 것은 허용하면서 시스템 잠자기를 방지합니다. 모든 세션이 완료되거나 종료되면 어서션은 즉시 해제됩니다.

외부 프로세스로 caffeinate를 사용하는 것도 고려했지만, 독립 데몬을 거부한 것과 같은 이유로 거부했습니다. 라이프사이클 복잡성이 증가하기 때문입니다. IOKit 어서션 API는 정밀한 제어를 제공하며 앱 자체의 세션 추적과 깔끔하게 통합됩니다.

메뉴 바 통합

컴패니언 앱은 메뉴 바 아이콘과 최소한의 드롭다운 인터페이스로 표시됩니다. 메뉴에는 다음이 표시됩니다:

  • 연결 상태 — 서버 실행 여부, 바인딩된 포트, Cloudflare Tunnel 활성 여부.
  • 활성 세션 — 실행 중인 Claude Code 세션 수와 간단한 상태 표시.
  • QR 코드 접근 — iPhone 페어링을 위한 연결 URL과 인증 토큰이 포함된 QR 코드를 원클릭으로 표시.
  • 빠른 작업 — 서버 시작/중지, 설정 열기, 연결 URL 복사.

전체 창 인터페이스 구축은 의도적으로 피했습니다. 컴패니언 앱의 역할은 인프라이지 사용자가 찾아가는 목적지가 아닙니다. 개발자는 iPhone을 통해 Tactic Remote와 상호작용합니다. Mac 앱은 초기 설정과 가끔의 설정 변경 시에만 관심이 필요합니다.

Cloudflare Tunnel 통합

Cloudflare Tunnel 모드가 활성화되면 컴패니언 앱은 cloudflared 프로세스를 자식 프로세스로 관리합니다. 설정된 터널 자격 증명으로 cloudflared를 시작하고, 로컬 메트릭 엔드포인트를 통해 상태를 모니터링하며, 터널이 끊어지면 자동으로 재시작합니다.

이 통합은 미묘한 타이밍 문제를 처리합니다: 터널이 완전히 확립되어야 iPhone이 이를 통해 연결할 수 있습니다. 컴패니언 앱은 cloudflared가 정상적인 연결을 보고할 때까지(일반적으로 시작 후 1~3초) 기다린 후 터널 URL을 사용 가능으로 표시합니다. 이 동안 메뉴 바 아이콘은 고유한 "연결 중" 상태를 표시합니다.

터널 프로세스가 크래시하면 컴패니언 앱은 지수 백오프(2초, 4초, 8초)로 최대 3회 재시작을 시도한 후 터널 모드를 실패로 표시하고, 연결된 클라이언트에 로컬 모드로 폴백하도록 알립니다(사용 가능한 경우).

인증

모든 API 요청과 WebSocket 연결에는 Bearer Token이 필요합니다. 토큰은 처음 실행 시 생성되어 macOS 키체인에 저장됩니다. 설정 시 표시되는 QR 코드에 포함되므로 페어링은 한 번의 스캔으로 완료되며, 수동으로 토큰을 복사할 필요가 없습니다.

토큰은 SecRandomCopyBytes를 사용하여 생성된 256비트 임의 값입니다. 세션 기반 인증 대신 정적 토큰 모델을 선택한 이유는 주요 위협 모델이 무단 네트워크 접근이지 다중 사용자 접근 제어가 아니기 때문입니다. 사용자별 인증이 필요한 팀에는 터널 앞에 Cloudflare Access를 배치하는 것을 권장합니다.

검토했지만 채택하지 않은 방안

풀 Electron 앱. 메모리 오버헤드만으로도 이 방안을 탈락시켰습니다. Electron의 기본 사용량은 80150 MB로, 네이티브 Swift 앱의 유휴 시 사용량의 약 58배입니다.

독립형 CLI 데몬. 초기 빌드는 더 간단하지만 기술에 익숙하지 않은 사용자가 관리하기 어렵고, macOS 메뉴 바 규칙과의 통합이 불가능합니다.

tmux 대신 직접 PTY 관리. 더 많은 제어가 가능하지만, tmux가 이미 안정적으로 처리하는 세션 지속성, 터미널 에뮬레이션 엣지 케이스, 프로세스 라이프사이클 관리를 재구현해야 했습니다.

자동 시작을 위한 LaunchAgent. 이 기능을 구현했지만 기본값이 아닌 선택 사항으로 만들었습니다. 개발자들은 로그인 시 무엇을 실행할지에 대해 강한 의견이 있으며, 우리는 이를 존중합니다.

성능 프로파일

일반적인 부하(활성 세션 2개, 연결된 iPhone 1대)에서의 컴패니언 앱 리소스 사용량:

지표값
메모리 (RSS)22-35 MB
CPU (유휴, 출력 변경 없음)< 0.1%
CPU (활성 스트리밍, 세션 2개)1-3%
디스크 I/O무시할 수 있는 수준 (기본적으로 디스크 로깅 없음)
네트워크 (로컬 모드, 활성 스트리밍)10-50 KB/s

이 수치는 macOS 14이 설치된 MacBook Pro M2에서 측정되었습니다. Intel Mac에서의 성능도 유사하며, 활성 스트리밍 시 CPU 사용률이 약간 높습니다.

향후 전망

컴패니언 앱의 아키텍처는 현재 출시된 기능 이상의 역량을 지원하도록 설계되었습니다. 세션 관리 계층은 세션 공유(여러 iPhone이 같은 세션 모니터링), 세션 녹화(재생을 위한 전체 터미널 히스토리 캡처), 그리고 Claude Code의 확장성 API가 발전함에 따른 더 풍부한 통합을 수용할 수 있습니다.

설정 방법은 시작하기를 참조하세요. Mac 컴패니언 앱의 문제 해결은 자주 발생하는 문제를 참조하세요.

Tactic Remote 사용해 보기

휴대전화로 코딩 에이전트를 원격 제어하세요

Mac, Windows, Linux 컴퓨터의 Claude Code, Codex 등에 연결하세요. iPhone이나 iPad에서 진행 상황을 확인하고 다음 요청을 보낼 수 있습니다.