전체 검색

글, 태그와 카테고리를 검색하고 바로 이동해요

앱 안에 터미널을 넣고, 워크트리를 1급으로 올렸습니다

워크트리를 쓸 때마다 터미널에서 cd를 다시 치고 있었어요. 기존 git 도구들이 워크트리를 제대로 안 다뤄주더라고요. 그래서 자리를 옮기면 도구가 따라오게 만들었습니다.

2026. 07. 29. 10:12:067분 읽기프로젝트/Git GUI
조회 28
목공방에서 사람이 옮겨간 작업대의 조명과 공구가 함께 따라 켜지는 일러스트

워크트리를 쓸 때마다 터미널에서 cd를 다시 쳤어요. 매번요.

앱에서는 이 워크트리를 보고 있는데, 터미널은 아직 아까 그 폴더에 있는 거죠. 명령 하나 치려면 경로를 다시 찾아야 하고요.

git 도구를 여럿 써봤는데 워크트리를 제대로 다뤄주는 걸 못 찾았습니다. 목록은 보여줘도 거기서 뭘 할 수는 없거나, 아예 없거나요.

그래서 두 개를 같이 만들기로 했어요. 앱 안의 터미널과, 1급 시민이 된 워크트리를요.

워크트리가 뭔지부터

안 써 보신 분을 위해 한 문단만요. 워크트리는 같은 저장소를 여러 폴더에서 동시에 열어 두는 기능이에요.

# 저장소 하나, 작업 폴더 여럿
git worktree add ../review feature/login

# 이제 두 폴더가 같은 저장소를 공유합니다
# ./        → main 브랜치로 작업 중
# ../review → feature/login 을 열어 둔 채

브랜치를 옮길 때마다 빌드가 다시 도는 프로젝트에서 특히 좋아요. 리뷰하려고 브랜치를 바꾸면 하던 일이 통째로 멈추잖아요. 워크트리를 쓰면 옆 폴더에서 열면 됩니다.

좋은 기능인데 도구 지원이 이상하게 약해요. 목록은 보여줘도 거기서 뭘 할 수는 없거나, 아예 없거나였습니다.

먼저 터미널부터

가짜 터미널은 만들기 쉬워요. 명령을 실행하고 출력을 보여주면 되니까요.

그런데 그건 반쪽입니다. 색이 안 나오고, 대화형 명령이 안 되고, 편집기를 못 열어요. 결국 진짜 터미널로 돌아가게 되죠.

그래서 진짜 터미널을 넣었습니다. 사용자의 기본 쉘을 로그인 모드로 띄워서 설정 파일과 경로를 그대로 살렸어요. vim도 열리고 색도 나옵니다.

위치는 고민이 좀 됐어요. 화면 전체 하단에 깔면 왼쪽 관리 패널이 밀립니다. 그래서 가운데와 오른쪽 아래에만 열리게 했어요. 왼쪽은 항상 온전히 남아 있습니다.

(접어도 세션은 안 죽어요. 접기는 숨기기지 종료가 아니거든요.)

가짜 터미널과 진짜 터미널의 차이

"진짜 터미널을 넣었다"는 말이 뭘 의미하는지 표로 보면 명확해요.

기능명령 실행 + 출력 표시의사 터미널(pty)
색이 나오나안 나옴나옴
진행 표시줄줄이 계속 쌓임제자리에서 갱신됨
대화형 입력불가가능
편집기 열기불가vim 도 열림
Ctrl+C프로세스 강제 종료쉘이 신호로 처리
구현 난이도쉬움플랫폼별 처리가 필요

앞쪽으로 만들면 대부분의 경우엔 잘 돌아가요. 그러다 진행 표시줄 하나에 무너집니다. 화면이 줄로 도배되는 순간 사용자는 그냥 진짜 터미널을 켜거든요.

그리고 사용자의 기본 쉘을 로그인 모드로 띄웠어요. 설정 파일과 경로가 그대로 살아 있어야 "내 터미널"이 되니까요. 별칭 하나 안 먹으면 바로 남의 터미널이 됩니다.

그리고 위험한 건 안쪽에 둡니다

터미널 프로세스는 앱의 바깥 계층이 소유해요. 화면을 그리는 쪽은 세션 번호와 글자 흐름만 다룹니다.

세션 번호는 안쪽에서 발급한 난수고, 모르는 번호로 뭔가 하려 하면 조용히 무시하는 게 아니라 에러를 냅니다. 그리고 화면 쪽이 임의의 경로로 터미널을 열 수는 없어요.

터미널은 결국 아무 명령이나 실행할 수 있는 구멍이잖아요. 그 구멍의 열쇠를 어디에 두느냐가 중요했습니다.

구멍의 열쇠를 어디에 두느냐는 이런 뜻이에요.

// 화면 쪽이 할 수 있는 것 — 이게 전부입니다
terminal.write(sessionId, data)
terminal.resize(sessionId, cols, rows)
terminal.close(sessionId)

// 세션을 여는 건 화면이 경로를 정하지 않습니다
terminal.openForWorktree(worktreeId)
// 앱 안쪽이 worktreeId → 실제 경로를 해석합니다

// 모르는 세션 번호로 오면
if (!sessions.has(sessionId)) {
  throw new Error("알 수 없는 터미널 세션입니다");
  // 조용히 무시하지 않습니다
}
설계위험우리 선택
화면이 경로 문자열로 터미널을 엶임의 경로에서 쉘이 열림워크트리 식별자로만 열게 함
세션 번호가 순번다른 세션에 입력을 보낼 수 있음난수 발급
모르는 세션이면 무시실패가 안 보임에러를 냄

마지막 줄이 이 앱 전체의 태도예요. 조용한 실패를 안 만듭니다. 충돌 편에서 자동 해결을 안 넣은 것과 같은 이유고요.

터미널을 넣으니 생긴 문제

예상 못 한 일이 있었어요. 터미널에서 커밋을 하면 앱 화면이 거짓말을 하기 시작합니다.

변경 목록은 아직 아까 그대로고, 히스토리에도 새 커밋이 없어요. 앱 안에서 한 일이 아니니까요.

그래서 저장소를 계속 지켜보게 했습니다. 터미널이든 외부 도구든 저장소가 바뀌면 화면이 알아서 따라와요.

여기서 원칙을 하나 세웠어요. 갱신은 최신화지 닫기가 아니다. 외부에서 커밋이 들어와도 보고 있던 diff나 커밋 상세는 그대로 열려 있습니다. 내용만 최신이 되고요.

(보던 화면이 툭 닫히는 앱, 다들 하나쯤 아시죠. 그거 안 하려고요.)

앱이 거짓말을 하기 시작한 이야기

터미널을 넣고 나서 처음 며칠은 뿌듯했어요. 그러다 이상한 걸 발견했습니다. 터미널에서 커밋했는데 앱 화면이 그대로인 거예요.

당연하죠. 앱은 자기가 실행한 명령만 아니까요. 그런데 사용자 입장에서는 같은 창 안에서 벌어진 일이라, 앱이 거짓말하는 것처럼 보입니다.

외부에서 벌어진 일앱이 모르면지켜보면
터미널에서 커밋변경 목록이 그대로 남음목록이 비고 히스토리에 새 커밋
에디터가 파일 저장변경 감지가 늦음바로 반영
다른 도구에서 브랜치 전환헤더가 옛 브랜치를 가리킴헤더가 따라감
외부에서 리베이스 중단앱은 아직 진행 중이라고 믿음상태 배지가 사라짐

그래서 저장소를 계속 지켜보게 했어요. 여기서 원칙을 하나 세웠는데, 이게 은근히 중요합니다.

갱신은 최신화지 닫기가 아니다. 외부에서 커밋이 들어와도 보고 있던 diff나 커밋 상세는 그대로 열려 있어요. 내용만 최신이 되고요.

보던 화면이 툭 닫히는 앱, 다들 하나쯤 아시죠. 그거 안 하려고요. 화면이 닫히면 사용자는 자기가 뭘 보고 있었는지부터 다시 찾아야 하잖아요.

워크트리를 어떻게 다룰까

여기서 설계 질문이 하나 나왔어요. 워크트리 행을 클릭하면 무슨 일이 일어나야 하나?

두 가지 답이 다 말이 됩니다. 터미널만 그리로 따라가거나, 앱 전체가 그 워크트리로 전환되거나요.

매번 물어보면 성가시고, 하나로 고정하면 반대쪽 사람이 불편하죠.

그래서 이렇게 했습니다. 클릭의 기본 동작은 설정에서 고르고, 우클릭에는 둘 다 항상 있어요. 설정이 정하는 건 기본값이지 가능 여부가 아닙니다.

이 앱 최초의 설정 화면이 이때 생겼는데, 여기서도 규칙을 하나 지켰어요. 나중에 채울 카테고리를 흐리게 미리 보여주지 않습니다. 죽은 UI를 만들지 않는다는 거죠.

설정으로 푼 이 방식이 마음에 드는 이유가 있어요.

방식장점문제
매번 물어보기명확함두 번째부터 성가심
하나로 고정단순함반대 취향인 사람이 계속 불편함
설정으로 기본값 + 우클릭에 둘 다취향과 예외를 같이 해결설정 화면이 필요해짐

세 번째의 핵심은 "설정이 정하는 건 기본값이지 가능 여부가 아니다"예요. 설정을 바꾸지 않아도 두 동작 다 항상 할 수 있습니다.

설정이 기능을 켜고 끄기 시작하면, 사용자는 어떤 기능이 있는지조차 알 수 없게 돼요. 문서를 읽어야 알 수 있는 기능은 없는 기능이고요.

만들기와 지우기까지

목록만 보여주는 건 의미가 없었어요. 요청의 핵심이 "기존 도구들이 제대로 안 해준다"였으니까요.

그래서 새로 만들기도 넣었습니다. 브랜치를 고르고 경로를 정하면 되는데, 여기서 git 제약이 하나 있어요. 같은 브랜치를 두 워크트리가 동시에 쓸 수 없습니다.

그래서 이미 쓰이는 브랜치는 목록에서 사유와 함께 비활성돼요. 안 보이게 숨기지 않고요. 없는 것과 못 고르는 건 다르니까요.

지우기도 마찬가지예요. 저장소 본체와 지금 앱이 열고 있는 워크트리는 못 지웁니다. 대신 왜 못 지우는지 적혀 있어요.

못 하는 일을 어떻게 보여줄까

워크트리를 만들고 지우는 데는 git 제약이 몇 개 붙어요. 이걸 화면에 어떻게 표현할지가 생각보다 큰 결정이었습니다.

제약숨기면비활성 + 사유를 적으면
같은 브랜치를 두 워크트리가 못 씀그 브랜치가 왜 없는지 모름"이미 ../review 에서 쓰는 중"
저장소 본체는 못 지움지우기 버튼이 없어서 헤맴"본체 폴더는 지울 수 없어요"
지금 열고 있는 워크트리는 못 지움왜 안 되는지 모름"지금 이 폴더를 보고 있어요"

없는 것과 못 고르는 건 다릅니다. 숨기면 화면은 깔끔해지는데, 사용자는 자기가 뭘 잘못했는지 영원히 모르게 돼요.

그리고 사유 문장에 대상을 같이 적었어요. "이미 사용 중"보다 "이미 ../review 에서 쓰는 중"이 낫거든요. 앞쪽은 상태 설명이고 뒤쪽은 다음 행동을 알려주니까요.

그리고 자리를 옮기면 도구가 따라옵니다

완성된 그림은 이래요. 왼쪽에서 워크트리를 고르면, 오른쪽 아래 터미널이 그 폴더에서 열립니다. 탭 이름에도 폴더 이름이 붙고요.

이미 열려 있던 터미널은 안 건드려요. 터미널의 위치는 만들어진 시점에 정해지는 거니까요.

나중에는 워크트리별로 터미널 탭이 묶여서, 워크트리를 바꾸면 탭 묶음도 같이 전환되게 했습니다.

결국 만들고 싶었던 건 "지금 어디에 있는지"를 앱과 터미널이 같이 아는 상태였어요. cd를 다시 치지 않아도 되는 건 그 결과일 뿐이고요.


다음 편은 이 앱의 정체성을 뒤집은 이야기예요. 쉬운 말을 버리고 개발자 어휘로 돌아온, 조금 이상한 결정이요.

시리즈 이어읽기

Git GUI, 도구가 되기까지

2 / 5

이야기 나누기

반응을 남기고 새 글 소식을 받아보세요.

댓글0
서로를 존중하는 대화를 남겨주세요.

첫 댓글을 남겨 대화를 시작해보세요.

새 글 구독
새 글이 공개되면 이메일로 알려드려요.