Electron을 떠난 이유
GitSquid v1은 Electron으로 구축되었습니다. 잘 작동했습니다. 사용자들은 저장소를 탐색하고, 변경사항을 스테이징하고, 브랜치를 관리하고, 리모트를 다루고, 통합 터미널을 사용할 수 있었습니다 -- 모두 하나의 데스크톱 앱에서. 내부적으로는 Git 작업에 simple-git, 터미널에 node-pty와 xterm.js, UI에는 일반적인 React 스택을 사용했습니다.
그러나 앱이 성숙해지면서 Electron의 트레이드오프를 무시하기 어려워졌습니다. 기능적인 제품을 출시했지만, 런타임 자체가 우리에게 불리하게 작용하고 있었습니다. 그래서 Tauri 2.x를 사용하여 백엔드 전체를 Rust로 다시 작성했습니다 -- React 프론트엔드의 95%는 그대로 유지하면서.
이것은 왜 그렇게 했는지, 어떻게 했는지, 그리고 그 과정에서 무엇이 깨졌는지에 대한 이야기입니다.
Electron의 문제점
Electron은 검증된 프레임워크입니다. VS Code, Slack, Discord -- 주요 제품들이 그 위에서 돌아갑니다. 하지만 Git 클라이언트에게 그 오버헤드는 작업에 비해 과도합니다.
- 앱 크기: Electron은 전체 Chromium 브라우저와 Node.js 런타임을 번들합니다. 패키징된 앱의 크기가 150 MB를 넘었습니다. Git 클라이언트치고는 정당화하기 어려운 크기입니다.
- 메모리 사용량: 각 Electron 창은 여러 Chromium 프로세스를 생성합니다. 유휴 메모리 소비량은 네이티브 앱이 같은 작업에 필요로 하는 것보다 훨씬 높았습니다.
- 시작 시간: Chromium 실행은 즉각적이지 않습니다. 특히 오래된 하드웨어에서 앱이 인터랙티브해지기 전에 체감되는 지연이 있었습니다.
- 네이티브 느낌: 네이티브 타이틀 바와 메뉴에 대한 Electron의 노력에도 불구하고, 앱은 OS에 속한 느낌을 주지 못했습니다. 창 관리, 포커스 동작, 키보드 단축키 같은 작은 것들에 항상 약간의 웹 앱 느낌이 남아 있었습니다.
이 중 어느 것도 단독으로는 치명적이지 않습니다. 하지만 결합되면, 개발자 도구가 주어야 할 느낌 -- 빠르고, 가볍고, 보이지 않는 -- 과 맞지 않는 마찰을 만들어냈습니다.
왜 Tauri 2.x인가
Tauri로 결정하기 전에 여러 대안을 평가했습니다. 결정적인 요인은 명확했습니다:
- 번들된 Chromium 대신 시스템 WebView. Tauri는 OS가 제공하는 WebView(macOS의 WebKit, Windows의 WebView2, Linux의 WebKitGTK)를 사용합니다. 이것만으로도 Electron 바이너리 크기의 대부분을 제거합니다.
- Rust 백엔드. 백엔드는 컴파일된 Rust 바이너리로 실행됩니다. 메모리 안전성, 가비지 컬렉터 없음, 예측 가능한 성능. 인터랙션당 수십 개의 Git 작업을 실행하는 도구에게 이것은 중요합니다.
- 극적으로 작은 바이너리. Tauri 앱은 10 MB 미만으로 배포할 수 있습니다. 150 MB 이상과 비교해 보세요.
- 네이티브 통합을 갖춘 크로스 플랫폼. Tauri 2.x는 네이티브 메뉴, 시스템 트레이, 알림, 파일 대화상자를 위한 견고한 API를 제공합니다. 앱이 OS에 속한 느낌을 줍니다.
- 활발한 에코시스템과 좋은 개발자 경험. Tauri의 플러그인 시스템, IPC 모델, 문서는 2.x 릴리스에서 크게 성숙했습니다. 프로덕션 사용 준비가 되어 있었습니다.
마이그레이션 전략
전체 재작성은 위험합니다. 명확한 관심사 분리로 그 위험을 완화했습니다.
프론트엔드는 유지, 백엔드는 재작성
React 프론트엔드는 이미 IPC 경계를 중심으로 구조화되어 있었습니다. 컴포넌트는 Electron 메인 프로세스와 통신하는 함수를 호출했고, 이 호출들을 Tauri의 invoke API로 리디렉션하기만 하면 되었습니다. React 코드의 약 95%가 변경 없이 이전되었습니다.
백엔드는 다른 이야기였습니다. 모든 Node.js 서비스 -- Git 작업, 파일 감시, 터미널 관리, 인증, 설정 -- 를 Rust로 재구현해야 했습니다. 총 약 7,200줄의 Rust를 작성하고 120개 이상의 IPC 명령을 마이그레이션했습니다.
libgit2 대신 Git CLI
흔한 질문: 왜 git2-rs(libgit2의 Rust 바인딩)를 사용하지 않았나?
Git CLI를 직접 호출하기 위해 std::process::Command를 선택했습니다. 이유는 다음과 같습니다:
libgit2는 모든 Git 기능을 지원하지 않습니다. 인터랙티브 rebase, 일부 merge 전략, 특정 설정 옵션 같은 작업이 누락되어 있거나 다르게 동작합니다.- 사용자들은 이미 Git을 설치하고 있습니다. 로컬 Git 바이너리를 호출함으로써 명령줄과의 동작 일치를 보장합니다. 터미널에서 작동하면 GitSquid에서도 작동합니다.
- 디버깅이 더 간단합니다. 우리가 실행하는 명령은 사용자가 입력하는 것과 같습니다. 문제가 생기면 에러 메시지가 익숙합니다.
Node.js 에코시스템 교체
각 Node.js 의존성을 Rust 동등물로 교체했습니다:
simple-git는std::process::Command를 통한 직접 Git CLI 호출로node-pty는 터미널 에뮬레이션용portable-pty로- 토큰 저장은 OS 키체인(macOS Keychain, Windows Credential Manager, Linux Secret Service)을 사용하는
keyringcrate로 - 파일 감시는
notifycrate로 - HTTP 요청은
reqwest로
원활한 설정 마이그레이션
동일한 설정 디렉토리 구조를 유지했습니다. v1에서 v2로 업그레이드하는 사용자는 설정, 저장된 저장소, 환경설정을 잃지 않습니다. Rust 백엔드는 Node.js 백엔드가 작성한 것과 동일한 설정 파일을 읽습니다.
직면한 도전 과제
마이그레이션은 순탄하지 않았습니다. 여러 문제가 상당한 디버깅 시간을 소모했습니다.
Git 로그 파싱의 널 바이트
커밋 데이터를 파싱하기 위해 git log --format에 커스텀 포맷 문자열을 사용합니다. Electron 버전에서는 필드 간에 특정 구분자 문자를 사용했습니다. Rust로 포팅할 때 처음에는 널 바이트(\x00)를 필드 구분자로 사용하려 했습니다 -- 커밋 메시지에 나타나지 않으므로 안전한 선택처럼 보였습니다.
틀렸습니다. Rust 문자열은 널 바이트를 문제없이 처리하지만, 스택의 여러 계층이 이를 C 스타일 문자열 종결자로 취급합니다. 로그 출력이 첫 번째 널 바이트에서 조용히 잘리고 있었습니다. Git 출력에서 자연적으로 발생하지 않는 다중 문자 구분자로 전환했습니다.
키체인 토큰 저장
keyring crate는 OS 키체인에 자격 증명을 저장하며, 이는 올바른 접근법입니다. 하지만 유사한 널 바이트 문제에 부딪혔습니다: 저장 전에 토큰 데이터를 널 바이트 구분자로 연결하고 있었습니다. 검색 시 일부 OS 키체인 구현이 첫 번째 널 바이트에서 값을 잘라내어 불완전한 자격 증명을 반환했습니다. 해결책은 동일했습니다 -- 가시적이고 명확한 구분자를 사용하는 것입니다.
BitBucket 인증의 복잡성
GitHub과 GitLab은 비교적 간단한 토큰 기반 인증 플로우를 가지고 있습니다. BitBucket은 더 복잡합니다. 작업과 계정 유형에 따라 사용자 이름, 이메일, 앱 비밀번호 또는 OAuth 토큰이 필요할 수 있으며 -- 어디에서 무엇을 사용해야 하는지의 규칙이 항상 명확하지는 않습니다. clone, fetch, push, API 호출 전반에서 BitBucket 인증을 안정적으로 작동시키려면 여러 차례의 반복이 필요했습니다.
줄 단위 스테이징
GitSquid는 전체 hunk뿐만 아니라 diff에서 개별 줄의 스테이징을 지원합니다. Electron 버전에서는 패치 생성이 JavaScript에서 이루어졌습니다. Tauri 버전에서는 이 로직을 Rust 백엔드로 옮겼습니다. 임의의 줄 선택에서 유효한 Git 패치 형식을 생성하고, 컨텍스트 줄의 엣지 케이스를 처리하고, git apply가 결과를 수락하도록 보장하는 것 -- 이것은 재작성에서 가장 복잡한 부분 중 하나였습니다. Rust 구현은 결국 원래 JavaScript 버전보다 더 견고해졌습니다.
프랑스어 아포스트로피가 빌드를 깨뜨리다
이건 기억에 남는 문제였습니다. 사용자에게 표시되는 일부 문자열에 직선 ASCII 아포스트로피 대신 프랑스어 인쇄용 아포스트로피(곡선 형태, U+2019)가 포함되어 있었습니다. 이것이 특정 CI 설정에서 빌드 프로세스 중 인코딩 문제를 일으켰습니다. 문제가 식별되면 수정은 간단했지만, 에러 메시지는 실제 문제 근처를 가리키지 않았습니다.
결과
마이그레이션을 완료하고 완전한 기능 패리티에 도달한 후의 결과입니다:
- 앱 크기: Electron 빌드 대비 크게 감소. Tauri 번들은 이전에 배포하던 150 MB 이상의 극히 일부입니다.
- 메모리 사용량: 기준선이 낮고 부하 시 더 예측 가능합니다. 백그라운드에서 RAM을 소비하는 Chromium 프로세스 트리가 없습니다.
- 시작 시간: 눈에 띄게 빨라졌습니다. 앱이 거의 즉시 인터랙티브해집니다.
- 기능 패리티: 100%. v1의 모든 기능이 v2에 존재합니다. 통합 터미널, 줄 단위 스테이징, 다중 리모트 지원, 브랜치 관리 -- 전부.
- 프론트엔드 재사용: React 코드베이스의 95%가 이전되어, 깨끗한 IPC 경계를 유지하려는 아키텍처 결정이 올바랐음을 입증했습니다.
그만한 가치가 있었나
네.
재작성에는 상당한 노력이 들었습니다. 약 7,200줄의 Rust, 120개 이상의 IPC 명령 재구현, 그리고 Rust와 Node.js가 엣지 케이스를 다르게 처리하기 때문에 비로소 드러난 여러 비자명한 버그. 주말 프로젝트가 아니었습니다.
하지만 결과는 근본적으로 더 나은 애플리케이션입니다. GitSquid v2는 더 빠르게 시작하고, 메모리를 적게 사용하고, 더 작은 바이너리로 배포되며, 모든 플랫폼에서 더 네이티브하게 느껴집니다. Rust 백엔드는 Node.js 동등물보다 더 예측 가능하고 이해하기 쉽습니다. 그리고 같은 React 프론트엔드를 유지했기 때문에 사용자들은 이미 익숙한 동일한 인터페이스를 봅니다.
Electron 앱을 유지보수하고 있고 런타임 오버헤드가 신경 쓰인다면, Tauri는 진지한 선택지입니다. 에코시스템은 프로덕션 사용에 충분히 성숙했으며, 마이그레이션 경로 -- 특히 이미 깨끗한 프론트엔드/백엔드 분리가 있다면 -- 는 전체 재작성이 암시하는 것보다 더 관리 가능합니다.
핵심 통찰: IPC 경계를 API 계약으로 다루세요. 프론트엔드가 invoke('get_commits', { path, branch })를 호출하는 방법만 알고 응답이 Node.js에서 오는지 Rust에서 오는지 신경 쓰지 않는다면, 백엔드 교체는 환상이 아닌 현실적인 프로젝트가 됩니다.