화면엔 AI만 남았는데, 커밋은 어디서 하지 — Git GUI를 직접 만듭니다
IntelliJ를 끄고 나니 제일 아쉬운 건 코드 편집기가 아니라 git 도구였어요. 대안은 불친절하거나 유료라서 직접 만들기로 했습니다. 개발자 모드와 쉬운 모드를 함께 담은 데스크톱 Git 클라이언트, 그 시작 편이에요.

요즘 제 모니터에는 코드 편집기가 없어요. Claude 하나, Codex 하나. 코드는 AI가 고치고, 저는 지시하고 검토합니다.
그런데 커밋할 때가 되면 잠깐 멈칫해요. 여태 커밋은 IntelliJ의 git 도구로 해왔거든요. diff 보고, 스테이징하고, 커밋하는 그 흐름이 정말 편했는데 — 에디터를 안 쓰게 되니 그 도구 하나 쓰자고 IntelliJ를 켜는 게 이상해진 거죠.
대안을 찾다가 그만뒀어요
git GUI만 따로 주는 툴이 없는 건 아니에요. 그런데 써보면 하나씩 걸렸습니다.
| 도구 | 걸린 것 |
|---|---|
| SourceTree | UI/UX가 불친절해서 손이 안 감 |
| GitKraken, Fork 같은 유료 계열 | 무료 구간은 기능 제한이 많음 |
| IntelliJ | git 도구는 최고인데, 이제 에디터를 안 씀 |
(커밋 하나 하려고 IDE를 켜는 건 좀 아니잖아요.)
그리고 직접 만들기로 마음먹은 두 번째 이유가 있어요. AI로 개발하는 사람이 늘면서, Git을 잘 모른 채 코드를 쌓아가는 사람도 늘었다는 것. 이 사람들의 가장 큰 니즈는 명확해요 — "코드가 잘 되던 시점으로 돌아가고 싶다". 그런데 기존 Git GUI조차 commit, branch, merge라는 용어를 전제하니 진입 장벽이 높죠. 이런 분들도 쓸 수 있는 도구면 좋겠다고 생각했어요.
"돌아가고 싶다"가 왜 어려운 요구인가
AI로 개발하는 사람이 늘면서 생긴 요구를 한 문장으로 줄이면 이거예요. "잘 되던 시점으로 돌아가고 싶어요."
쉬운 요구처럼 들리는데, git에서는 이게 여러 갈래로 갈립니다.
| "돌아가고 싶다"의 실제 의미 | 필요한 명령 | 되돌릴 수 있나 |
|---|---|---|
| 방금 고친 파일을 버리고 싶다 | 작업 트리 복원 | 저장 안 했으면 못 되돌림 |
| 방금 한 커밋을 취소하고 싶다 | 커밋만 취소 · 변경은 남김 | 됨 |
| 어제 상태로 통째로 가고 싶다 | 과거 커밋으로 이동 | 됨 |
| 올린 것까지 없던 일로 | 히스토리 재작성 + 강제 푸시 | 남에게 영향 감 |
| 합치기를 중단하고 싶다 | 병합 중단 | 됨 |
사용자는 이 다섯을 구분하지 않고 "돌아가기"라고 말해요. 그런데 첫 줄만 되돌릴 수 없고, 네 번째는 남한테 영향이 갑니다.
그래서 이 앱이 할 일이 정해졌어요. 어느 갈래인지 먼저 파악하고, 그 결과와 되돌리는 방법을 말한 다음에 실행하는 것. "돌아가기 버튼 하나"를 만드는 게 아니라요.
어떤 앱인가
한 문장으로는 이거예요. Git의 복잡한 상태를 정확히 해석해 보여 주고, 사용자가 작업의 결과와 복구 방법을 이해한 상태에서 안전하게 명령을 실행하도록 돕는 데스크톱 클라이언트.
기존 도구들은 정상적인 commit/push 흐름에는 편리해요. 문제는 rebase 도중 충돌이 났을 때, detached HEAD에 서 있을 때, force push를 잘못했을 때예요. 갑자기 사용자가 명령어와 내부 상태를 직접 해석해야 하죠.
그래서 이 앱은 Git을 단순화한다는 이유로 상태를 감추지 않아요. 대신 현재 상태, 가능한 행동, 행동의 영향, 되돌리는 방법을 하나의 흐름으로 보여줍니다.
그래서 상태를 감추지 않는다는 게 화면에서 어떤 모양이냐면, 항상 네 가지를 같이 보여주는 거예요.
| 보여주는 것 | 예시 |
|---|---|
| 지금 상태 | "병합 중이에요 · 충돌 3곳 중 1곳 해결" |
| 가능한 행동 | "충돌 계속 해결하기 / 병합 취소하기" |
| 행동의 영향 | "취소하면 병합 전으로 돌아가요" |
| 되돌리는 방법 | "지금 해결한 것도 함께 사라져요" |
네 번째가 대부분의 도구에 없는 칸이에요. 그리고 초보자가 제일 무서워하는 칸이고요. 무서우면 아무것도 안 누르게 되거든요.
여기에 원칙을 하나 더 뒀어요. 화면이 git보다 똑똑한 척하지 않는다. git이 모르는 걸 앱이 아는 척하면, 그 순간 앱의 말이 틀리기 시작합니다.
듀얼 모드 — 개발자 모드와 쉬운 모드
하나의 앱에 두 모드가 공존하고, 토글로 즉시 전환해요. 쉬운 모드는 별도의 Git 로직이 아니라 같은 엔진 위의 다른 프레젠테이션입니다. 쉬운 모드에서 한 작업은 개발자 모드와 CLI에서 표준 Git 상태로 그대로 보여요.
쉬운 모드의 문구 전략은 "일상어 주 표기 + Git 용어 병기"예요.
| 쉬운 모드 표기 | Git 개념 |
|---|---|
| 저장하기 | commit |
| 백업 | push |
| 실험 공간 | branch |
| 보관함 | shelf (stash 기반) |
| 그만두고 원래대로 | abort |
(용어를 숨기지 않고 병기하는 이유 — 나중에 AI한테 질문하거나 검색할 때 용어가 끊기면 안 되거든요.)
그리고 쉬운 모드의 핵심 보장 하나. 어떤 버튼을 눌러도 작업물이 영구 삭제되지 않는다. 되돌리기처럼 현재 작업을 덮을 수 있는 동작 전에는 저장 안 된 변경을 자동으로 보관함에 넣고 진행해요. 보관함 항목은 기본 30일 뒤 자동 정리되고, 정리 전에 알려줍니다.
그리고 쉬운 모드에 대한 오해를 하나 풀고 갈게요. 별도 로직이 아니에요.
// 두 모드가 같은 엔진 위에 있습니다
engine.commit(message) // 실제로 하는 일은 하나
// 다른 건 표현뿐
mode === "easy"
? "저장하기"
: "커밋"
// 그래서 쉬운 모드에서 한 작업이
// CLI 에서 표준 git 상태로 그대로 보입니다| 접근 | 장점 | 치명적 단점 |
|---|---|---|
| 쉬운 모드용 별도 로직 | 자유롭게 단순화 가능 | CLI 와 상태가 갈라짐 — 도움을 받을 수 없게 됨 |
| 같은 엔진 + 다른 표현 | 언제든 CLI 로 넘어갈 수 있음 | 표현으로 숨길 수 있는 복잡도에 한계 |
두 번째를 고른 이유는 명확해요. 쉬운 모드를 쓰던 사람이 막혔을 때, 검색을 하든 AI한테 묻든 CLI를 켜든 밖에서 도움을 받을 수 있어야 하거든요.
용어를 숨기지 않고 병기한 것도 같은 이유예요. 앱 안에서만 통하는 말로 앱을 만들면, 그 앱은 사용자를 가둡니다.
기술스택
| 항목 | 결정 | 근거 |
|---|---|---|
| 데스크톱 런타임 | Electron | GitHub Desktop·VS Code로 검증된 경로. Node에서 git 프로세스 실행·취소·스트리밍 제어가 용이 |
| UI | React 19 + TypeScript strict | 늘 쓰는 표준 스택 |
| 컴포넌트 기반 | React Aria Components + 자체 디자인 토큰 | 접근성·상호작용 품질은 가져오되 디자인 자유도 확보 |
| Git 실행 | git CLI 번들(dugite 방식) + 출력 파싱 | LFS·sparse checkout·credential helper까지 전체 기능 호환 |
git을 라이브러리로 다시 구현하지 않고 CLI를 실행해서 기계용 출력(porcelain v2, -z 구분자)을 파싱하는 이유는 호환성이에요. 실제 git이 실행되니 CLI와 상태가 어긋날 일이 없고, "실행한 명령을 로그로 보여준다"는 요구와도 맞아떨어집니다. libgit2는 성능 병목이 확인되면 읽기 경로에 한해 하이브리드로 검토해요.
git을 다시 구현하지 않고 CLI를 실행하기로 한 것도 같은 태도예요.
| 방식 | 호환성 | 장점 | 문제 |
|---|---|---|---|
| git 라이브러리 재구현 | 일부 | 빠름 · 파싱 불필요 | 자격 증명 · 훅 · 대용량 확장이 다 따로 |
| CLI 실행 + 출력 파싱 | 완전 | 실제 git 이 도니까 상태가 안 갈라짐 | 출력을 파싱해야 함 |
| 하이브리드 | 완전 | 성능 병목만 라이브러리로 | 두 경로를 다 유지해야 함 |
# 사람용 출력 — 파싱하면 안 됩니다
$ git status
On branch main
Changes not staged for commit:
modified: notes.txt
# 기계용 출력 — 이걸 씁니다
$ git status --porcelain=v2 -z
1 .M N... 100644 100644 100644 <oid> <oid> notes.txt사람용 출력은 git 버전이나 언어 설정에 따라 바뀌어요. 기계용 출력은 형식이 고정돼 있고, 널 문자로 구분해서 파일명에 공백이나 줄바꿈이 있어도 안 깨집니다.
그리고 실행한 명령을 로그로 보여주겠다는 요구와도 맞아떨어져요. 실제로 그 명령을 실행하니까요. 로그가 진짜인 겁니다.
어떻게 만드나
UI가 git 프로세스를 직접 실행하거나 원시 CLI 출력을 직접 해석하기 시작하면 이런 앱은 금방 엉켜요. 그래서 계층을 pnpm 모노레포 패키지로 물리적으로 분리했습니다.
apps/desktop # Electron main + renderer 진입점
packages/domain # 상태 모델, 작업 정책, 위험도·복구 정책 (순수 TS)
packages/git-adapter # CLI 출력 → 도메인 모델 변환
packages/git-process # git 실행, 취소, 스트리밍
packages/ui # 디자인 토큰, 공통 컴포넌트
packages/ipc-contract # renderer ↔ main 타입 계약renderer는 Node API에 접근하지 않고, 타입이 정의된 IPC 계약으로만 main 프로세스와 통신해요. domain 패키지는 Electron조차 몰라서 단위 테스트가 그냥 돌아갑니다.
로드맵은 개발자 모드 엔진(0~3단계) 위에 쉬운 모드(E0~E3)를 얹는 구조예요.
| 단계 | 범위 | 얹히는 쉬운 모드 |
|---|---|---|
| 0단계 · 기반 | 저장소 열기, 변경 파일·diff, stage, commit, push/pull | E0 — 2패널 메인, 저장·백업 |
| 1단계 · 일상 작업 | stash, commit graph, merge·rebase, 충돌 해결 | E1·E2 — 되돌리기·보관함, 충돌 선택형 |
| 2단계 · 복잡한 저장소 | worktree, submodule, LFS, 대형 저장소 성능 | — |
| 3단계 · 복구와 호스팅 | reflog 복구, GitLab·Bitbucket, 다중 계정 | E3 — 리뷰 요청부터 병합까지 |
기능의 완료 기준도 미리 박아뒀어요. 버튼이 있다고 완료가 아니라, 정상 흐름·실패 흐름·중단 후 재개 흐름의 자동화 테스트가 전부 통과해야 완료입니다. merge 충돌, detached HEAD, 앱 강제 종료 같은 상태를 fixture 저장소로 재현하는 시나리오 카탈로그를 코드보다 먼저 썼어요.
(앱보다 설계 문서가 먼저 네 개 나왔어요. 코드는 그다음입니다.)
"완료"의 정의를 먼저 적어 둔 이유
앱보다 설계 문서가 먼저 네 개 나왔다고 썼는데, 그중 제일 도움이 된 게 완료 기준이었어요.
| 흐름 | 확인하는 것 |
|---|---|
| 정상 흐름 | 의도한 대로 동작하는가 |
| 실패 흐름 | 실패했을 때 상태가 깨지지 않는가 |
| 중단 후 재개 | 앱을 강제 종료했다 켜도 이어지는가 |
| 외부 변경 뒤 | 터미널에서 뭔가 한 뒤에도 맞는가 |
세 번째 줄이 특히 중요했어요. git 작업은 중간 상태가 파일로 남거든요. 병합하다 앱이 죽으면 저장소는 병합 중인 채로 남습니다. 그걸 켰을 때 앱이 "아무 일 없음"이라고 하면 그게 제일 나쁜 거죠.
그래서 그런 상태를 만들어 두는 픽스처 저장소를 카탈로그로 관리했어요. 병합 충돌 중, 리베이스 중, 분리된 HEAD, 업스트림 없음 — 손으로 만들면 매번 다르니까 스크립트로 찍어냅니다.
버튼이 있다고 완료가 아니에요. 이 네 흐름이 전부 자동으로 통과해야 완료입니다.
지금은 0단계를 구현하는 중이에요. 모노레포를 부트스트랩했고, domain 패키지의 저장소 상태 감지(지금 merge 중인지, rebase 중인지)부터 테스트를 먼저 쓰면서 올라가고 있습니다.
다음 편부터는 만들면서 부딪힌 것들을 하나씩 풀어볼게요. porcelain v2 파싱, Electron IPC 경계, 그리고 쉬운 모드의 픽토그램 언어까지 — 이 시리즈는 앱이 완성될 때까지 이어집니다.


