처음 사용해본 Git worktree

Git Git worktree

최근 개인 포트폴리오 서버에 세 가지 이슈가 쌓였다.

  • 블로그 목록 정렬 기준 변경
  • 관리자 Markdown 에디터 개선
  • 코드 블록 하이라이팅, Mermaid, 목차 기능 추가

기존에는 한 브랜치의 작업을 끝낸 뒤 다음 브랜치로 이동했다.

작업 도중 다른 브랜치를 확인해야 한다면 현재 변경 사항을 커밋하거나 git stash로 잠시 치운 뒤 브랜치를 전환했다.

git stash push
git switch main

직접 한 번에 하나의 작업을 처리할 때는 이 방식으로 충분했다. 여러 브랜치를 동시에 펼쳐둘 이유도 없었다.

Claude Code를 이용해 에이전트 중심으로 개발하기 시작하면서 상황이 달라졌다.

세 이슈는 일부 코드가 겹치기는 했지만, 하나가 끝나야 다음 작업을 시작할 수 있는 관계는 아니었다. 각 작업을 별도의 에이전트에 맡길 수 있다면 한 작업이 끝날 때까지 나머지 작업을 기다릴 필요가 없었다.

당시에는 Orca 안에서 Claude Code를 실행하고 있었고, Claude Code에는 Superpowers 플러그인을 적용한 상태였다. Superpowers는 구현 작업을 격리된 작업 공간에서 진행하기 위해 Git worktree를 사용했다.

내 환경에서는 Claude Code가 다음과 같이 세 개의 작업 디렉터리를 만들었다.

chuseok22-home-server/
├── .git/
├── .claude/
│   └── worktrees/
│       ├── blog-admin-editor/
│       ├── blog-list-sort/
│       └── blog-markdown-renderer/
└── ...

명령어를 직접 입력하지 않아도 Claude Code는 worktree를 생성하고 각 폴더에서 작업을 진행할 수 있었다.

그대로 사용해도 기능 구현에는 문제가 없었겠지만, 원리를 모르는 구조에서 여러 에이전트가 코드를 수정하게 두는 방식은 내 성격과 맞지 않았다.

적어도 다음 질문에는 답할 수 있어야 했다.

  • Git worktree는 브랜치와 무엇이 다른가?
  • 왜 같은 저장소에 작업 폴더가 여러 개 생기는가?
  • main 작업 폴더와 다른 작업 폴더를 동시에 수정해도 되는가?
  • 프로젝트 밖의 형제 디렉터리에 만들어도 Git은 어떻게 이를 추적하는가?
  • 작업이 끝나면 worktree와 브랜치는 각각 어떻게 정리해야 하는가?

Google과 YouTube에서 관련 내용을 찾아봤지만, 브랜치나 stash에 비하면 Git worktree를 처음 접하는 개발자를 위한 자료는 생각보다 많지 않았다.

결국 실제 프로젝트에서 생성 과정을 지켜보고, 별도의 저장소에서 직접 실습하면서 구조를 확인해보기로 했다.


브랜치를 세 개 만들었는데 왜 폴더도 세 개가 생겼을까

Git 저장소를 clone하거나 git init으로 생성하면 프로젝트 파일이 펼쳐진 작업 디렉터리가 만들어진다.

Git에서는 이를 working tree라고 부른다.

일반적으로는 하나의 Git 저장소에 하나의 working tree를 두고 사용한다.

chuseok22-home-server/
├── .git/
├── apps/
├── templates/
└── ...

이 폴더에서 브랜치를 변경하면 동일한 작업 디렉터리의 파일이 선택한 브랜치 상태로 바뀐다.

git switch feature/blog-sort

다른 브랜치로 이동하려면 다시 브랜치를 전환한다.

git switch feature/markdown-renderer

브랜치는 여러 개 존재할 수 있지만, 파일이 펼쳐진 작업 디렉터리가 하나라면 한 시점에는 한 브랜치의 파일 상태만 사용할 수 있다.

Git worktree는 이 작업 디렉터리를 추가하는 기능이다.

Git 공식 문서에서는 git init이나 git clone으로 처음 만들어진 작업 디렉터리를 main worktree, 이후 추가한 작업 디렉터리를 linked worktree라고 부른다.

하나의 Git 저장소
├── Main worktree
│   └── main 브랜치
├── Linked worktree A
│   └── feature/blog-sort 브랜치
├── Linked worktree B
│   └── feature/admin-editor 브랜치
└── Linked worktree C
    └── feature/markdown-renderer 브랜치

여기서 먼저 바로잡아야 했던 오해가 있었다.

Git worktree는 브랜치를 대체하는 기능이 아니다.

브랜치는 그대로 존재한다.

각 worktree에서는 기존과 동일하게 코드를 수정하고 커밋한다.

git add .
git commit -m "feat: add blog sort options"
git push

GitHub에서 PR을 만들고 다른 브랜치에 병합하는 과정도 달라지지 않는다.

달라지는 것은 브랜치가 체크아웃되는 물리적인 작업 디렉터리다.

하나의 폴더에서 브랜치를 계속 전환하는 대신, 서로 다른 브랜치를 각각 다른 폴더에 동시에 펼쳐둘 수 있다.


브랜치와 worktree는 비교 대상이 아니었다

처음에는 “브랜치와 worktree의 차이”를 알아보려고 했다.

하지만 둘은 서로를 대체하는 개념이 아니었다.

브랜치는 특정 커밋을 가리키는 Git 참조다. worktree는 특정 브랜치나 커밋의 파일을 실제 디렉터리에 펼쳐놓은 작업 공간이다.

따라서 정확히 비교해야 하는 대상은 다음 두 가지였다.

  1. 하나의 working tree에서 브랜치를 전환하는 방식
  2. 여러 worktree에 서로 다른 브랜치를 체크아웃하는 방식
항목 하나의 폴더에서 브랜치 전환 Git worktree
작업 디렉터리 하나 여러 개
동시에 펼쳐둘 수 있는 브랜치 하나 worktree마다 하나
다른 작업으로 이동 switch 또는 checkout 필요 다른 worktree 폴더로 이동
미완성 변경 사항 commit 또는 stash가 필요할 수 있음 다른 worktree와 분리됨
Git 커밋과 브랜치 정보 같은 저장소에서 관리 같은 저장소를 공유
실행 환경 기존 환경을 계속 사용 worktree마다 다시 준비할 수 있음
병합 충돌 발생할 수 있음 동일하게 발생할 수 있음
디스크 사용량 상대적으로 적음 소스와 의존성 공간이 추가됨

나는 기존에 git stash를 자주 사용했다.

git stash push -m "admin editor 작업 중"
git switch main

현재 변경 사항을 잠시 치워두고 다른 브랜치로 이동하는 데는 충분했다.

다만 작업이 여러 개로 늘어나면 다음 상태를 계속 기억해야 했다.

  • 어느 stash가 어떤 작업인지
  • 현재 폴더가 어느 브랜치인지
  • 실행 중인 서버가 어느 브랜치의 코드인지
  • 다시 돌아왔을 때 의존성과 생성 파일이 이전 상태와 맞는지

worktree에서는 작업 A를 멈추기 위해 작업 A의 상태를 치울 필요가 없다.

작업 A의 폴더를 그대로 둔 채 작업 B의 폴더를 열면 된다.

Workspace/
├── project/
│   └── main
├── project-feature-a/
│   └── feature/a
└── project-feature-b/
    └── feature/b

기존 방식이 잘못된 것은 아니었다.

한 사람이 하나의 기능을 처리한다면 브랜치를 전환하는 방식이 더 단순할 수 있다. worktree는 동시에 유지해야 하는 작업 공간이 늘어날 때 의미가 있었다.


폴더가 곧 브랜치는 아니었다

Git worktree를 처음 봤을 때는 다음처럼 생각했다.

blog-sort 폴더 = blog-sort 브랜치

하지만 폴더와 브랜치는 같은 개념이 아니었다.

worktree는 특정 경로에 브랜치나 커밋을 체크아웃해 연결한 것이다.

새 worktree를 만들면서 새 브랜치를 함께 생성할 수 있다.

git worktree add \
  -b feature/blog-sort \
  ../chuseok22-home-server-blog-sort \
  main

명령을 분리해서 보면 다음과 같다.

새 worktree 경로
└── ../chuseok22-home-server-blog-sort

새로 생성할 브랜치
└── feature/blog-sort

브랜치를 시작할 기준
└── main

이미 존재하는 브랜치를 새로운 worktree에 연결할 수도 있다.

git worktree add \
  ../chuseok22-home-server-blog-sort \
  feature/blog-sort

특정 커밋을 detached HEAD 상태로 열 수도 있다.

git worktree add \
  --detach \
  ../chuseok22-home-server-test \
  <commit-sha>

다만 이미 다른 worktree에서 사용 중인 브랜치를 아무 제한 없이 다시 연결할 수 있는 것은 아니다.

이를 직접 확인하기 위해 작은 실습 저장소를 만들었다.

실습 환경은 다음과 같았다.

Git 2.50.1
Apple Git-155

main worktree에는 feature-base 브랜치를 체크아웃하고, linked worktree에는 feature/worktree-study 브랜치를 연결했다.

linked worktree에서 main worktree가 사용 중인 feature-base 브랜치로 전환해봤다.

git switch feature-base

Git은 다음과 같이 전환을 거부했다.

fatal: 'feature-base' is already used by worktree at '/.../git-worktree-study-main'

이미 main worktree에서 사용 중인 feature-base 브랜치를 linked worktree에서도 사용하려 했기 때문이다.

Git은 기본적으로 하나의 브랜치가 여러 worktree에서 동시에 사용되는 것을 막는다.

한 worktree에서 커밋해 브랜치가 이동했는데 다른 worktree의 파일과 index는 이전 상태로 남는 상황을 방지하기 위한 안전장치로 이해할 수 있었다.

오류 메시지의 세부 문구는 Git 버전에 따라 달라질 수 있다. 위 메시지는 Git 2.50.1에서 직접 확인한 결과다.


main worktree는 main 브랜치를 뜻하지 않는다

main worktree라는 용어도 처음에는 main 브랜치와 관련된 개념으로 보였다.

하지만 둘은 관계가 없다.

main worktree는 git init이나 git clone으로 처음 만들어진 기본 작업 디렉터리를 뜻한다.

해당 디렉터리에는 main뿐 아니라 develop, release, feature-base 같은 다른 브랜치도 체크아웃할 수 있다.

이번 실습에서도 main worktree에는 main 브랜치가 아니라 feature-base 브랜치가 체크아웃돼 있었다.

Main worktree
└── feature-base

Linked worktree
└── feature/worktree-study

즉, 다음 두 개념을 분리해서 봐야 했다.

main worktree
→ 저장소의 최초 작업 디렉터리

main 브랜치
→ 프로젝트가 이름을 main으로 정한 브랜치

형제 디렉터리에 있어도 Git은 어떻게 worktree를 찾을까

Claude Code가 실제 프로젝트에 생성한 worktree는 프로젝트 내부의 .claude/worktrees에 있었다.

chuseok22-home-server/
├── .git/
├── .claude/
│   └── worktrees/
│       ├── blog-admin-editor/
│       ├── blog-list-sort/
│       └── blog-markdown-renderer/
└── ...

프로젝트 내부에 worktree를 생성하면 main worktree 입장에서는 해당 디렉터리가 자신의 하위 경로에 존재한다.

따라서 worktree가 생성되는 경로를 Git 추적 대상에서 제외해야 한다.

내 환경에서는 .claude/worktrees 경로가 .git/info/exclude에 등록돼 있었다.

**/.claude/worktrees/

.gitignore와 달리 .git/info/exclude는 저장소에 커밋되지 않는 로컬 전용 제외 규칙이다.

해당 규칙이 등록돼 있다는 사실은 확인했지만, Claude Code 본체와 Superpowers 중 어느 구성 요소가 직접 추가했는지는 확인하지 못했다.

확인하지 못한 내용을 특정 도구의 기능이라고 단정해서 쓰지는 않기로 했다.

사용자가 직접 worktree를 관리한다면 프로젝트의 형제 디렉터리에 생성할 수도 있다.

Workspace/
├── git-worktree-study-main/
└── git-worktree-study/

형제 디렉터리는 main worktree의 하위 경로가 아니므로 별도의 ignore 설정이 필요하지 않다.

그렇다면 서로 떨어진 두 디렉터리를 Git은 어떻게 같은 저장소로 인식할까?

이를 확인하기 위해 실제로 형제 디렉터리에 worktree를 생성했다.

git worktree add \
  -b feature/worktree-study \
  ../git-worktree-study \
  feature-base

실행 결과는 다음과 같았다.

Preparing worktree (new branch 'feature/worktree-study')
HEAD is now at 1652244 chore: init base repo

생성된 linked worktree의 .git을 확인했다.

cat ../git-worktree-study/.git
gitdir: /.../git-worktree-study-main/.git/worktrees/git-worktree-study

main worktree의 .git은 디렉터리다.

git-worktree-study-main/
└── .git/

반면 linked worktree의 .git은 저장소 전체가 들어 있는 디렉터리가 아니라 텍스트 파일이었다.

git-worktree-study/
└── .git

이 파일은 main worktree의 .git/worktrees 아래에 만들어진 전용 관리 디렉터리를 가리키고 있었다.

git-worktree-study-main/
└── .git/
    └── worktrees/
        └── git-worktree-study/
            ├── HEAD
            ├── index
            ├── gitdir
            └── ...

이 연결 정보가 있기 때문에 linked worktree가 main worktree의 하위 경로가 아니라 형제 디렉터리에 있어도 Git은 두 작업 공간을 같은 저장소에 연결할 수 있었다.


무엇을 공유하고 무엇을 분리할까

Git worktree는 저장소 전체를 복제하지 않는다.

커밋 객체와 브랜치 같은 공통 저장소 데이터는 공유하면서, 체크아웃된 파일 상태는 worktree별로 분리한다.

flowchart TB
    COMMON["공통 Git 저장소<br/>objects · 대부분의 refs · 기본 repository config"]

    MAIN["Main worktree<br/>feature-base<br/>개별 HEAD · index"]
    WORKTREE_A["Linked worktree A<br/>feature/blog-sort<br/>개별 HEAD · index"]
    WORKTREE_B["Linked worktree B<br/>feature/admin-editor<br/>개별 HEAD · index"]
    WORKTREE_C["Linked worktree C<br/>feature/markdown-renderer<br/>개별 HEAD · index"]

    COMMON --- MAIN
    COMMON --- WORKTREE_A
    COMMON --- WORKTREE_B
    COMMON --- WORKTREE_C

단순화하면 다음과 같이 나눌 수 있다.

구분 내용
공통으로 공유 Git object database, 일반 브랜치와 태그, 기본 저장소 설정
worktree별로 분리 작업 디렉터리, HEAD, index, commit되지 않은 변경 사항

Git의 ref 공유 규칙은 조금 더 세부적이다.

일반적으로 refs/ 아래의 ref는 공유하지만 refs/bisect, refs/worktree, refs/rewritten 같은 일부 ref는 worktree별로 관리한다. HEAD처럼 $GIT_DIR 바로 아래에 있는 pseudo ref도 worktree별로 분리된다.

일상적인 사용에서는 다음 정도로 이해해도 충분했다.

커밋과 일반 브랜치 정보는 공유하지만, 현재 어떤 브랜치를 보고 있는지와 스테이징 상태는 worktree마다 다르다.

이 차이도 직접 확인했다.

먼저 feature-base 브랜치가 체크아웃된 main worktree에서 다음 명령을 실행했다.

git rev-parse --show-toplevel
git rev-parse --git-dir
git rev-parse --git-common-dir

결과는 다음과 같았다.

/.../git-worktree-study-main
.git
.git

이번에는 feature/worktree-study 브랜치가 체크아웃된 linked worktree에서 같은 명령을 실행했다.

git rev-parse --show-toplevel
git rev-parse --git-dir
git rev-parse --git-common-dir
/.../git-worktree-study
/.../git-worktree-study-main/.git/worktrees/git-worktree-study
/.../git-worktree-study-main/.git

결과를 비교하면 역할이 분명해졌다.

명령 Main worktree Linked worktree
--show-toplevel main worktree 경로 linked worktree 경로
--git-dir .git .git/worktrees/<worktree-id>
--git-common-dir .git main worktree의 .git

linked worktree는 자신의 HEAD와 index 등을 관리하는 전용 Git 디렉터리를 가졌다.

반면 공통 Git 디렉터리는 main worktree의 .git을 가리켰다.

각 worktree의 체크아웃 상태는 분리되지만, 브랜치와 커밋 객체를 보관하는 저장소는 공유한다는 설명을 실제 출력으로 확인할 수 있었다.


실제로 세 작업을 병렬 진행했다

이번에 worktree로 분리한 작업은 Django 기반 개인 포트폴리오 서버의 세 이슈였다.

작업 PR
블로그 목록에 게시일과 최신순·조회순 정렬 추가 #120
관리자 에디터를 EasyMDE 기반으로 개선 #121
코드 블록 하이라이팅, Mermaid, TOC 추가 #122

세 작업은 각각 구현과 테스트를 거쳐 최종적으로 main에 병합됐다.

실제 작업 흐름은 다음과 같았다.

Orca 내부에서 Claude Code 실행
→ Claude Code에서 Superpowers 사용
→ 작업별 linked worktree 생성
→ 세 작업을 동시에 구현
→ 각 worktree에서 테스트와 서버 실행
→ 커스텀 스킬로 commit, push, PR 생성
→ 검토 후 main에 병합
→ linked worktree 정리

이번 작업에서 .claude/worktrees 아래에 worktree를 만든 주체는 Orca 자체가 아니라 Orca 안에서 실행한 Claude Code와 Superpowers였다.

Orca는 Claude Code를 실행하고 여러 작업 공간을 확인하는 환경으로 사용했다.

GitHub Desktop에서 확인한 worktree

GitHub Desktop에서도 worktree 구조를 확인할 수 있었다.

저장소가 서로 다른 세 개의 저장소로 표시되지는 않았다.

하나의 Current Repository 아래에 main worktree와 세 개의 linked worktree가 구분되어 나타났다.

업로드 이미지# Claude Code로 세 작업을 동시에 돌리며 Git worktree를 배웠다

GitHub Desktop의 Current Worktree 목록. 기존 main worktree 아래에 세 작업 브랜치가 각각 linked worktree로 표시됐다.

GitHub Desktop은 현재 worktree 생성, 전환, 이름 변경, 삭제를 지원한다.

linked worktree를 선택하면 해당 작업 디렉터리를 기준으로 Changes와 History가 표시됐다.

Current Repository
└── chuseok22-home-server

Current Worktree
├── Main Worktree
│   └── chuseok22-home-server / main
└── Linked Worktrees
    ├── blog-admin-editor / feature branch
    ├── blog-list-sort / feature branch
    └── blog-markdown-renderer / feature branch

repository와 worktree는 별도 개념이었다.

  • 저장소는 하나였다.
  • worktree는 네 개였다.
  • 각 worktree에는 서로 다른 브랜치가 연결돼 있었다.

이번 작업에서는 main worktree에서 별도의 코드를 수정하지 않았다.

main은 병합 결과를 받는 기준 브랜치로 두고, 세 linked worktree에서 각각 에이전트가 작업하도록 했다.


코드가 분리돼도 실행 환경까지 만들어지지는 않았다

worktree를 사용하면서 가장 먼저 마주친 현실적인 문제는 실행 환경이었다.

새 worktree에는 해당 브랜치에서 Git이 추적하는 파일이 체크아웃된다.

.gitignore에 포함된 .env 같은 로컬 파일은 Git이 관리하지 않으므로 새 worktree에 자동으로 생성되지 않는다.

내 프로젝트에서도 각 worktree에 필요한 환경 변수 파일을 별도로 복사해야 했다.

Main worktree
└── .env

Linked worktree A
└── .env 필요

Linked worktree B
└── .env 필요

Linked worktree C
└── .env 필요

Python 가상환경도 worktree마다 새로 만들었다.

기존 .venv를 통째로 복사하지 않았다. 프로젝트에 의존성 정의 파일이 있으므로 각 worktree에서 새로운 가상환경을 만들고 의존성을 설치했다.

python -m venv .venv
source .venv/bin/activate

pip install -r requirements/base.txt

worktree가 분리하는 대상과 그렇지 않은 대상을 구분할 필요가 있었다.

worktree가 분리하는 것

  • 체크아웃된 코드
  • 현재 브랜치 또는 detached HEAD 상태
  • HEAD
  • index
  • commit되지 않은 변경 사항
  • 작업 디렉터리 안에서 생성한 파일

worktree가 자동으로 분리하지 않는 것

  • .env
  • Python 가상환경
  • 서버 포트
  • 데이터베이스
  • Docker 컨테이너
  • Redis 같은 외부 상태
  • 외부 API 계정과 인증 정보

세 worktree에서 서버가 동시에 실행되기도 했다.

동일한 포트를 사용할 수는 없으므로 각 서버의 실행 포트를 다르게 설정해 충돌을 피했다.

코드 작업 공간이 분리됐다고 해서 애플리케이션 실행 환경까지 완전히 격리되는 것은 아니었다.

여러 worktree가 같은 데이터베이스를 사용한다면 더 주의해야 한다.

한 브랜치에서 마이그레이션을 실행하거나 스키마를 변경하면 다른 worktree의 코드와 데이터베이스 상태가 맞지 않을 수 있다.

이번 작업에서는 데이터베이스 충돌을 직접 겪지 않았다. 다만 worktree를 완전히 독립된 샌드박스로 보는 것은 잘못된 해석이었다.


병렬 작업은 가능해졌지만 충돌은 남아 있었다

세 작업을 병렬로 진행하면서 한 작업이 끝날 때까지 다른 작업을 기다릴 필요는 줄었다.

다만 worktree가 해결하는 문제는 작업 디렉터리의 분리다.

브랜치를 병합할 때 발생하는 Git conflict까지 제거하는 기능은 아니다.

실제로 관리자 에디터 개선 작업과 Markdown 렌더링 개선 작업은 모두 다음 파일을 수정했다.

apps/blog/services/markdown_renderer.py
apps/blog/tests/test_markdown_renderer.py

관리자 에디터 작업에서는 이미지 크기 지정에 필요한 width 속성을 sanitizer 허용 목록에 추가했다.

Markdown 렌더링 개선 작업에서는 같은 렌더러에 Pygments 코드 하이라이팅, Mermaid 처리, fenced code block 전처리 로직을 추가했다.

두 브랜치가 같은 파일을 수정하면서 conflict가 한 번 발생했다.

충돌 자체는 크지 않았고 해결 후 정상적으로 병합했다.

이 경험으로 worktree가 보장하는 범위를 구분할 수 있었다.

worktree가 줄여주는 문제

- 한 에이전트의 미완성 파일을 다른 에이전트가 직접 덮어쓰는 상황
- 브랜치를 전환하면서 작업 디렉터리 전체가 바뀌는 상황
- 한 폴더에서 여러 작업 프로세스가 서로 다른 코드를 수정하는 상황
worktree가 해결하지 않는 문제

- 같은 파일을 수정한 브랜치 사이의 merge conflict
- 서로 중복되는 구현
- 호환되지 않는 데이터 모델 변경
- 잘못된 설계
- 병렬 작업 결과에 대한 통합 검토

따라서 “worktree를 사용해 개발 속도가 세 배가 됐다”고 말할 수는 없다.

시간을 별도로 측정하지 않았고, 병렬 작업 이후에는 세 결과를 검토하고 병합하는 과정도 필요했다.

확인할 수 있었던 사실은 세 작업을 순차적으로 기다리지 않고 동시에 진행할 수 있었다는 점이다.

서로 독립적인 이슈라면 병렬 작업의 장점이 컸다.

반대로 같은 핵심 파일을 수정하는 이슈라면 병렬 구현으로 줄인 대기 시간 일부를 충돌 해결과 통합 검토에 다시 사용하게 됐다.


CLI에서 직접 worktree를 관리하는 방법

Claude Code나 GitHub Desktop 없이 CLI에서 직접 worktree를 관리할 수도 있다.

현재 저장소에 연결된 worktree는 다음 명령으로 확인한다.

git worktree list

스크립트에서 처리하기 쉬운 안정적인 출력이 필요하다면 --porcelain을 사용한다.

git worktree list --porcelain

새 브랜치와 함께 생성

git worktree add \
  -b feature/blog-sort \
  ../chuseok22-home-server-blog-sort \
  main

-b 뒤에 새 브랜치 이름을 지정하고, 마지막 인수에는 브랜치를 시작할 기준 커밋이나 브랜치를 전달한다.

기준을 생략하면 현재 HEAD를 기준으로 새 브랜치가 만들어진다.

기존 브랜치 연결

git worktree add \
  ../chuseok22-home-server-blog-sort \
  feature/blog-sort

해당 브랜치가 이미 다른 worktree에서 사용 중이라면 기본적으로 생성이 거부된다.

특정 커밋을 detached HEAD로 연결

git worktree add \
  --detach \
  ../chuseok22-home-server-test \
  <commit-sha>

브랜치를 만들 필요가 없는 임시 테스트나 과거 커밋 확인에 사용할 수 있다.

worktree 이동

git worktree move \
  ../chuseok22-home-server-blog-sort \
  ../worktrees/blog-sort

git worktree move는 Git이 관리하는 연결 정보를 유지하면서 linked worktree의 위치를 변경한다.

main worktree는 이 명령으로 이동할 수 없다. submodule이 포함된 linked worktree에도 제한이 있다.

Finder나 일반 mv 명령으로 폴더만 이동하면 Git이 기억하는 경로와 실제 경로가 달라질 수 있다.

특별한 이유가 없다면 worktree 이동에는 git worktree move를 사용하는 편이 안전하다.


작업이 끝난 worktree는 따로 정리해야 했다

작업이 병합된 뒤에는 linked worktree를 제거할 수 있다.

git worktree remove ../chuseok22-home-server-blog-sort

remove는 다음 두 영역을 함께 정리한다.

  • linked worktree의 작업 디렉터리
  • 공통 저장소의 .git/worktrees 아래 관리 정보

추적 중인 파일의 변경이나 untracked 파일이 남아 있는 worktree는 기본적으로 제거되지 않는다.

먼저 상태를 확인해야 한다.

git status

정말 버려도 되는 작업이라면 강제로 제거할 수도 있다.

git worktree remove --force <worktree-path>

다만 --force는 작업 디렉터리의 변경 사항을 잃을 수 있으므로 기본 정리 방법으로 사용하지 않는 편이 안전하다.

worktree를 삭제해도 브랜치는 남는다

worktree와 브랜치의 생명 주기는 분리돼 있다.

git worktree remove ../chuseok22-home-server-blog-sort

위 명령을 실행해도 연결됐던 로컬 브랜치는 남는다.

git branch --list

브랜치까지 삭제하려면 별도의 명령이 필요하다.

git branch -d feature/blog-sort

이번 프로젝트에서는 PR 병합 후 worktree만 제거하고 로컬 브랜치는 삭제하지 않았다.

실제 정리 순서는 다음과 같았다.

PR 병합 확인
→ linked worktree 제거
→ 로컬 브랜치는 유지

브랜치를 언제 삭제할지는 worktree의 규칙이라기보다 프로젝트의 브랜치 관리 정책에 가까웠다.

폴더를 먼저 수동으로 삭제했다면

linked worktree 폴더를 Finder나 rm -rf로 먼저 삭제하면 공통 저장소에 worktree 관리 정보가 남을 수 있다.

이때 사용할 수 있는 명령이 prune이다.

git worktree prune

prune은 실제 작업 디렉터리는 사라졌지만 .git/worktrees에 남아 있는 오래된 관리 정보를 정리한다.

Git의 garbage collection이 오래된 정보를 나중에 정리할 수도 있지만, 필요하다면 직접 prune을 실행할 수 있다.

나는 이번 작업에서 폴더를 직접 삭제하지 않았다.

Claude Code, GitHub Desktop 또는 git worktree remove를 통해 정리했다. 따라서 stale metadata가 남아 prune이 필요했던 상황은 직접 겪지 않았다.


기본 명령 외에 알아둔 명령

일반적인 작업에서는 list, add, move, remove 정도면 충분했다.

Git에는 그 외에도 다음 명령이 있다.

git worktree lock

이동식 디스크나 네트워크 드라이브처럼 일시적으로 접근할 수 없는 위치의 worktree가 자동 정리 대상으로 판단되지 않도록 잠근다.

git worktree lock <worktree-path>

잠긴 worktree는 이동하거나 삭제할 수도 없다.

git worktree unlock

잠긴 worktree를 다시 이동하거나 삭제할 수 있도록 해제한다.

git worktree unlock <worktree-path>

git worktree repair

main worktree나 linked worktree를 Git 명령이 아닌 방식으로 이동해 연결 경로가 깨졌을 때 관리 정보를 복구한다.

git worktree repair <worktree-path>

git worktree prune

실제 폴더가 사라진 worktree의 오래된 관리 정보를 제거한다.

git worktree prune

lock, unlock, repair는 이번 프로젝트에서 직접 사용하지 않았다.

현재로서는 문제가 발생했을 때 찾아볼 수 있도록 역할만 알아두는 정도로 충분했다.

bare repository를 중심으로 여러 worktree를 관리하는 방식도 있지만, 이번 글에서는 다루지 않았다. 일반적인 저장소와 에이전트 병렬 작업에서 필요한 범위를 먼저 이해하는 것이 목적이었기 때문이다.


앞으로 어떤 작업에 사용할 것인가

이번 경험 이후 worktree를 모든 브랜치 작업의 기본 방식으로 사용하지는 않을 생각이다.

직접 하나의 기능만 개발한다면 기존 방식이 더 단순하다.

브랜치 생성
→ 구현
→ 테스트
→ PR
→ 병합

하나의 작업을 위해 worktree를 만들고, 환경 변수를 복사하고, 가상환경을 만들고, 마지막에 작업 디렉터리를 정리하는 과정은 오히려 추가 비용이 될 수 있다.

반면 다음 상황에서는 다시 사용할 가능성이 높다.

여러 에이전트가 독립적인 이슈를 동시에 구현할 때

한 에이전트가 목록 정렬을 수정하는 동안 다른 에이전트는 관리자 에디터를 변경할 수 있다.

서로 다른 working tree를 사용하므로 작업 중인 파일을 직접 덮어쓰지 않는다.

긴 기능 개발 중 긴급 수정이 필요할 때

현재 변경 사항을 억지로 커밋하거나 stash하지 않고 별도의 hotfix worktree를 만들 수 있다.

git worktree add \
  -b hotfix/login \
  ../chuseok22-home-server-hotfix-login \
  main

서로 다른 브랜치의 서버를 동시에 비교해야 할 때

포트와 필요한 실행 설정을 분리하면 두 브랜치의 애플리케이션을 동시에 실행할 수 있다.

현재 작업을 유지하면서 다른 PR을 검토할 때

현재 작업 폴더를 그대로 둔 채 검토 대상 브랜치를 별도 worktree에 체크아웃할 수 있다.

반대로 다음 상황에서는 신중하게 사용할 생각이다.

  • 여러 이슈가 같은 핵심 파일을 수정한다.
  • 각 worktree의 의존성 설치 비용이 크다.
  • 여러 worktree가 동일한 데이터베이스 스키마를 공유한다.
  • 로컬 디스크 공간이 부족하다.
  • 생성과 정리 비용이 실제 수정 비용보다 크다.
  • 병렬로 나온 결과를 충분히 검토할 시간이 없다.

worktree는 병렬 작업을 가능하게 하는 기반이었다

Git worktree를 사용하기 전에는 브랜치와 작업 폴더를 거의 같은 단위로 생각했다.

하나의 저장소 폴더가 있고, 그 안에서 브랜치를 바꿔가며 작업했다.

worktree를 사용한 뒤에는 브랜치와 작업 디렉터리를 분리해서 보게 됐다.

브랜치
→ 특정 커밋을 가리키는 Git 참조

worktree
→ 특정 브랜치나 커밋을 실제 파일로 펼쳐둔 작업 공간

Claude Code와 Superpowers가 worktree를 만들지 않았다면 이 기능을 별도로 찾아보지 않았을 가능성이 크다.

직접 코드를 작성할 때는 브랜치를 옮겨 다니는 방식만으로도 대부분의 작업을 처리할 수 있었기 때문이다.

에이전틱 코딩에서는 한 사람이 하나의 작업만 진행하는 것이 아니다.

여러 에이전트가 같은 저장소의 서로 다른 작업을 동시에 수정한다.

하나의 작업 디렉터리를 공유하면 각 에이전트의 변경을 안전하게 분리하기 어렵다. Git worktree는 각 에이전트에 별도의 작업 공간을 제공하는 기반이 됐다.

하지만 worktree가 모든 병렬 작업 문제를 해결하지는 않았다.

  • 실행 환경은 자동으로 분리되지 않았다.
  • 같은 파일을 수정하면 merge conflict가 발생했다.
  • 각 작업 결과를 사람이 검토하고 통합해야 했다.
  • 작업이 끝난 worktree를 별도로 정리해야 했다.
  • 병렬 실행이 항상 더 효율적인 것은 아니었다.

앞으로도 모든 작업에 worktree를 사용하지는 않을 것이다.

다만 서로 독립적인 이슈를 여러 에이전트가 동시에 처리해야 한다면, 브랜치 전환과 stash만으로 관리하기보다 worktree를 먼저 고려할 생각이다.

원리를 모른 채 자동화가 만든 폴더를 사용하는 단계에서는 벗어났다.

적어도 이제는 무엇이 공유되고, 무엇이 분리되며, 작업이 끝난 뒤 무엇을 정리해야 하는지는 알고 사용할 수 있게 됐다.


명령어 정리

# 현재 worktree 목록
git worktree list

# 스크립트 처리를 위한 상세 목록
git worktree list --porcelain

# 새 브랜치와 함께 linked worktree 생성
git worktree add -b <new-branch> <path> <start-point>

# 기존 브랜치를 linked worktree에 연결
git worktree add <path> <existing-branch>

# detached HEAD worktree 생성
git worktree add --detach <path> <commit>

# linked worktree 이동
git worktree move <worktree> <new-path>

# linked worktree 제거
git worktree remove <worktree>

# 사라진 worktree의 오래된 관리 정보 정리
git worktree prune

# 손상되거나 오래된 연결 정보 복구
git worktree repair <path>

# worktree 잠금과 해제
git worktree lock <worktree>
git worktree unlock <worktree>

참고 자료

56 ❤️ 0

댓글

댓글을 작성하려면 GitHub 로그인이 필요합니다.

첫 댓글을 남겨보세요.

AI 챗봇
...