블로그
30초 연결: `npx tacticremote` 배포 경로를 만든 방법
`npx tacticremote` 뒤에 있는 엔지니어링 이야기. Bun으로 컴파일한 바이너리와 얇은 npm 런처, QR 자동 저장, 더 빠른 첫 실행 온보딩까지 포함합니다.
이 카테고리의 제품 및 엔지니어링 업데이트를 확인하세요.
카테고리 보기목표는 말로는 단순했지만, 실제 구현은 꽤 어려웠습니다. 새 사용자가 한 번 명령을 실행하고, QR 코드를 스캔해서, 30초 안에 휴대폰에서 실시간 터미널을 볼 수 있어야 했습니다.
이 글은 그 과정을 어떻게 만들었는지 설명합니다.
문제
npx tacticremote가 나오기 전에는 서버 시작 과정이 플랫폼마다 달랐고, 여러 단계를 거쳐야 했습니다.
- macOS: DMG를 내려받고, 마운트하고, 앱을 Applications로 옮기고, 실행한 뒤 Gatekeeper를 처리해야 했습니다.
- Linux / CI: 저장소를 복제하고,
npm install을 실행하고, 그 환경에서 서버를 어떻게 실행하고 유지할지 스스로 정리해야 했습니다.
원격 Linux VM이나 Docker 컨테이너 안에서 작업하는 개발자에게는 이 과정이 특히 불편했습니다. 저장소 경로는 git, Node.js, 그리고 registry에 접근 가능한 npm 환경이 필요했습니다. 손에 쥐여 줄 수 있는 단일 명령이 없었습니다.
우리가 원한 것은 npx tacticremote → QR 스캔 → 완료였습니다.
왜 그냥 npm install이 아니었나
서버 런타임 의존성은 많지 않지만, npm install 자체가 여전히 마찰을 만듭니다. 현재 런타임 패키지에는 ws, bonjour-service, qrcode-terminal이 포함됩니다.
npm 의존성 해석 자체가 시간이 걸리고, 그동안 서버는 아무 일도 하지 않습니다. 기업 네트워크나 컨테이너화된 CI 같은 제한된 환경에서는 proxy 설정, registry mirror, 권한 문제 때문에 npm install이 실패할 수도 있습니다.
더 중요한 점은 CLI 경로가 가벼워야 한다는 것입니다. 만약 npm에 배포된 패키지가 전체 런타임 트리를 그대로 포함했다면, 첫 실행 경험은 여전히 "JavaScript를 먼저 내려받고 나서 서버를 시작"하는 수준에 머물렀을 것입니다. 우리가 원한 온보딩 기준과는 거리가 있었습니다.
그래서 npm에 올라가는 계층은 최대한 얇아야 했습니다.
Bun 컴파일 방식
우리는 Bun으로 서버를 자급자족형 단일 바이너리로 컴파일합니다. 결과물은 gzip 압축 전 기준 대략 20 MB 정도의 실행 파일이며, 다음을 포함합니다.
- Bun 런타임
- 모든 런타임 의존성
- 전체 서버 소스
node_modules도 없고, 별도의 Bun 설치도 필요 없고, 시작할 때 의존성 해석도 하지 않습니다. 바이너리를 받아서 실행하면 됩니다.
GitHub Actions(.github/workflows/build-cli.yml)는 릴리스마다 네 개의 타깃을 빌드합니다.
bun-darwin-arm64- Apple Silicon Macbun-darwin-x64- Intel Macbun-linux-x64- x86_64 Linuxbun-linux-arm64- ARM Linux(Raspberry Pi, AWS Graviton 등)
각 바이너리는 gzip 압축된 GitHub Release 자산으로 업로드됩니다.
얇은 래퍼
npmjs.com에 배포되는 npm 패키지는 서버 본체가 아닙니다. bin/tacticremote.js에 있는 124줄짜리 런처 스크립트입니다. 실제로 하는 일은 다음과 같습니다.
process.platform과process.arch로 현재 OS와 CPU 아키텍처를 감지합니다.- 이 플랫폼에 맞는 바이너리 이름을 결정합니다.
~/.tacticremote/bin에서 현재 버전의 캐시된 바이너리를 찾습니다.- 캐시가 없으면 GitHub Releases에서 gzip 압축 바이너리를 내려받아 압축을 풀고 실행 권한을 주어 디스크에 씁니다.
- 바이너리를 실행하고, stdio를 그대로 넘기며,
SIGINT와SIGTERM같은 신호도 전달합니다.
첫 실행에서는 해당 릴리스 자산을 내려받아 로컬에 캐시합니다. 이후에는 해당 npm 패키지 버전에 대응하는 캐시된 바이너리를 재사용합니다.
이 패턴이 새롭지는 않습니다. esbuild가 먼저 보여줬고, turbo도 사용하며, prisma도 비슷한 방식을 씁니다. 핵심은 npm을 컴파일 환경이 아니라 배포 채널로 보는 것입니다. dependency tree 대신 다운로더를 배포하는 셈입니다.
package.json 분리
우리는 두 개의 패키지 매니페스트를 나란히 유지합니다.
package.json- 개발용 설정입니다. 모든 devDependencies(Jest, ESLint, Prettier), 테스트 스크립트, lint 스크립트, 그리고 로컬 개발에 필요한 전체 의존성을 담습니다.package.npm.json- 가벼운 배포 매니페스트입니다.bin을tacticremote.js에 연결하고, 런타임 의존성 트리를 제외합니다.
이 분리는 개발 워크플로와 배포된 패키지를 각각 명확하게 유지해 줍니다. 대신 하나의 분명한 유지보수 비용이 있습니다. 두 매니페스트에서 겹치는 메타데이터는 계속 맞아야 합니다.
지금은 그 동기화가 아직 취약한 지점입니다. 저장소에는 배포 시점에 이 전달 과정이 항상 올바른지 검증하는 단계가 없기 때문에, CI 자동화가 가장 직접적인 다음 개선 사항입니다.
30초 온보딩 흐름
배포 문제가 해결된 뒤에는 연결 흐름의 각 단계를 점검하면서 마찰을 줄였습니다.
0-5초. 사용자가 터미널에서 npx tacticremote를 실행합니다. 첫 실행에서는 shim이 캐시된 바이너리가 없음을 감지하고 다운로드합니다. 이후에는 캐시 확인이 밀리초 단위로 끝납니다.
5-10초. 서버 바이너리가 시작됩니다. 로컬 IP 주소를 감지하고, 랜덤 API key를 생성한 뒤, 유니코드 블록 문자로 QR 코드를 터미널에 바로 출력합니다. 브라우저도 필요 없고, 따로 들어갈 웹 UI도 없습니다.
10-15초. 사용자가 휴대폰에서 Tactic Remote를 엽니다. 홈 화면에는 npx tacticremote 명령과 "Scan QR" 버튼이 있는 "Connect Your Computer" 카드가 보입니다. 버튼을 누르면 카메라가 열립니다.
15-25초. 사용자가 터미널의 QR 코드를 스캔합니다. QR에는 서버 IP, 포트, API key가 들어 있는 JSON payload가 인코딩되어 있습니다. 앱은 이 payload를 디코딩해 서버 설정을 자동 저장합니다. "Add Server" 화면도 없고, 수동 IP 입력도 없고, API key 복사-붙여넣기도 없습니다.
25-30초. WebSocket 핸드셰이크가 완료됩니다. 터미널 출력이 휴대폰으로 스트리밍되기 시작하고, 연결 표시가 초록색으로 바뀝니다.
QR 자동 저장 단계가 가장 큰 마찰을 없앴습니다. 이전 온보딩은 "서버 추가"를 누르고, IP를 입력하고, 포트를 입력하고, API key를 입력하고, 저장을 누르고, 연결을 눌러야 했습니다. 다섯 번의 탭과 세 번의 수동 입력이었고, 각각에서 실수가 날 수 있었습니다. QR 스캔은 이 모든 과정을 하나의 제스처로 압축합니다.
다시 한다면
아직 고치지 못한 부분도 몇 가지 있습니다.
아티팩트 서명 검증이 아직 없습니다. 런처는 HTTPS로 다운로드하지만, 현재는 바이너리를 공개된 서명이나 checksum으로 검증하지 않습니다. 지금 방식에서 가장 중요한 무결성 격차입니다.
매니페스트 전달은 여전히 자동화가 필요합니다. 개발 매니페스트와 배포 매니페스트를 분리해 두었지만, 서로 동기화되었는지 더 엄격하게 보장하는 배포 시 보호 장치가 아직 없습니다.
대체 미러가 없습니다. GitHub Releases가 잠시 불가하면 런처가 가져올 다른 경로가 없습니다. 보조 미러가 있으면 배포 경로의 복원력이 좋아질 것입니다.
큰 그림
이 배포 경로는 한 가지 원칙을 따릅니다. 사용자가 인프라를 고민하지 않게 하는 것입니다. Bun이 뭔지 알 필요도 없고, Linux 머신이 어떤 CPU 아키텍처인지 알 필요도 없고, API key를 손으로 복사할 필요도 없습니다. 명령은 시작점을 맡고, 런처는 올바른 바이너리를 가져오며, QR 코드는 연결 정보를 전달합니다.
서버 자체의 구조, 단일 의존성 설계, 보안 모델은 짝글인 One Dependency: Building a Production Server with Just the ws Package에서 설명합니다. 이 글은 그 서버를 사용자의 손에 빠르게 전달하는 방법에 관한 것입니다.
아직 30초 플로우를 직접 써보지 않았다면 /download로 가서 확인해 보세요.
서버 아키텍처 이야기는 One Dependency: Building a Production Server with Just the ws Package를 참고하세요. 설정 방법은 Getting Started guide를 참고하세요.