git worktree로 브랜치 전환 없이 여러 작업 동시에 하기

기능 개발을 한창 하고 있는데 “결제 페이지에서 500 떨어진다”는 연락이 옵니다. 지금 작업 트리에는 반쯤 고친 파일이 여덟 개쯤 열려 있습니다. 커밋하기엔 아직 동작하지 않고, 버리기엔 아깝습니다. 그래서 git stash로 밀어 넣고, 브랜치를 옮기고, 의존성을 다시 깔고, 고치고, 돌아와서 stash pop을 하고, 충돌을 풉니다. 이 왕복 한 번에 보통 10~15분이 날아갑니다. 하루에 두 번만 끼어들어도 한 달이면 열 시간이 넘습니다. git worktree를 쓰면 이 왕복 자체가 사라집니다. 같은 저장소를 폴더 두 개로 동시에 체크아웃해 두는 기능입니다.

stash로 버티면 무엇이 문제인가

먼저 익숙한 방식부터 봅시다. 대부분 이렇게 합니다.

# 기능 개발 중인데 급한 버그 수정 요청이 들어왔을 때
git stash push -u -m "wip: 결제 화면 작업중"
git switch main
git switch -c hotfix/login-500

# ... 수정하고 커밋하고 푸시 ...

git switch feature/checkout
git stash pop

명령 자체는 어렵지 않습니다. 문제는 명령이 아니라 그 사이에 벌어지는 일들입니다.

  • 빌드 캐시가 통째로 무효화됩니다. 브랜치를 옮기면 소스 파일의 수정 시각이 전부 바뀌므로 웹팩·Vite·Gradle 같은 도구가 증분 빌드를 포기하고 처음부터 다시 돕니다. 규모가 있는 프로젝트라면 이것만으로 수 분입니다.
  • 의존성이 어긋납니다. 두 브랜치의 package-lock.json이나 requirements.txt가 다르면 npm ci를 다시 돌려야 합니다. 안 돌리면 엉뚱한 에러를 버그로 오해하게 됩니다.
  • 에디터 상태가 초기화됩니다. 열어둔 탭, 접어둔 코드, 디버거 중단점이 전부 흐트러집니다.
  • stash를 잊어버립니다. 급한 일이 연달아 들어오면 스택이 쌓이고, 2주 뒤에 stash list를 열었을 때 WIP on feature/checkout 세 개 중 어느 게 무엇이었는지 아무도 모릅니다.
  • 실행 중인 서버가 죽습니다. 개발 서버나 테스트 감시(watch) 프로세스가 돌고 있었다면 파일이 통째로 바뀌는 순간 대부분 비정상 종료합니다.

요약하면, stash는 같은 폴더 하나를 시분할로 나눠 쓰는 방식입니다. 애초에 폴더가 두 개라면 나눌 일이 없습니다.

git worktree가 하는 일

git worktree는 하나의 저장소에 딸린 작업 폴더를 여러 개 만드는 명령입니다. 깃 2.5(2015년)부터 들어 있는 표준 기능이라 따로 설치할 것이 없습니다. 지금 쓰는 깃이면 그냥 됩니다.

핵심은 커밋 이력은 하나를 공유하되, 체크아웃된 파일은 폴더마다 따로 둔다는 점입니다. git clone을 한 번 더 하는 것과 비슷해 보이지만 결정적으로 다릅니다.

비교 항목git clone 한 번 더git worktree add
디스크 사용이력(.git)까지 통째로 복제이력은 공유, 작업 파일만 추가
커밋 공유푸시/풀을 거쳐야 오감같은 저장소라 즉시 보임
원격(remote) 설정새로 붙여야 함기존 설정 그대로 사용
정리폴더 삭제로 끝worktree remove로 등록까지 정리

이력을 공유한다는 건 실무에서 꽤 편합니다. hotfix 폴더에서 만든 커밋이 원래 폴더에서 git log에 바로 보입니다. 푸시했다가 다시 받아올 필요가 없습니다.

기본 사용법 — 세 가지 명령이면 충분하다

1. 새 브랜치로 폴더 만들기

가장 많이 쓰는 형태입니다. 브랜치 생성과 폴더 생성을 한 번에 합니다.

# 현재 저장소(main 체크아웃 상태)에서 실행
git worktree add ../myapp-hotfix -b hotfix/login-500

# 결과: ../myapp-hotfix 폴더가 새로 생기고
#       hotfix/login-500 브랜치가 체크아웃된 상태로 준비됨
#       원래 폴더는 손대지 않았으므로 작업중이던 파일 그대로 남아 있음

경로를 ../로 시작한 것에 이유가 있습니다. 워크트리 폴더를 저장소 안쪽에 만들면 깃이 그 폴더를 추적 대상으로 보게 되어 지저분해집니다. 형제 폴더로 빼는 것이 관례입니다.

2. 이미 있는 브랜치 꺼내기

# 이미 있는 브랜치를 꺼낼 때는 -b 없이
git worktree add ../myapp-review feature/search

# 원격에만 있는 브랜치라면 먼저 가져온 뒤
git fetch origin
git worktree add ../myapp-review origin/feature/search

동료의 PR을 받아서 실제로 돌려보고 싶을 때 이 형태를 씁니다. 내 작업은 원래 폴더에서 그대로 굴러가고, 리뷰는 옆 폴더에서 합니다.

3. 현황 확인과 정리

$ git worktree list
D:/work/myapp          a1b2c3d [feature/checkout]
D:/work/myapp-hotfix   e4f5g6h [hotfix/login-500]
D:/work/myapp-review   i7j8k9l [feature/search]

작업이 끝난 워크트리는 반드시 정리합니다. 폴더만 지우면 깃은 아직 그 워크트리가 있다고 믿고, 해당 브랜치를 다른 곳에서 체크아웃하지 못하게 막습니다.

# 작업이 끝났으면 폴더째 정리
git worktree remove ../myapp-hotfix

# 폴더를 탐색기에서 이미 지워버렸다면 등록 정보만 정리
git worktree prune

# 커밋 안 한 변경이 남아 있으면 remove가 거부됩니다.
# 정말 버릴 생각이면 --force
git worktree remove --force ../myapp-hotfix

실제로 이득이 큰 세 가지 상황

  1. 핫픽스 — 앞에서 본 그 상황입니다. 원래 폴더의 개발 서버는 계속 떠 있고, 새 폴더에서 수정해 배포합니다. 끝나면 폴더를 지웁니다. 원래 폴더는 처음부터 아무 일도 없었던 것처럼 그대로입니다.
  2. 코드 리뷰 — PR을 눈으로만 읽는 것과 실제로 띄워보는 것은 리뷰 품질이 다릅니다. 하지만 “띄워보려면 내 작업을 접어야 한다”는 마찰 때문에 대개 눈으로만 읽고 넘어갑니다. 워크트리를 쓰면 이 마찰이 없어집니다.
  3. 버전 비교 — “이 기능 예전 버전에서는 어떻게 동작했더라”를 확인할 때, 태그를 체크아웃한 워크트리를 하나 만들어 두 창을 나란히 띄우면 됩니다. 성능 회귀를 추적할 때 특히 유용합니다.

반대로 이득이 적은 경우도 분명히 있습니다. 브랜치 전환이 하루 한 번도 안 되는 소규모 프로젝트, 빌드가 몇 초 만에 끝나는 정적 사이트, 의존성이 거의 없는 스크립트 저장소라면 그냥 stash가 간편합니다. 폴더가 늘어나는 관리 비용이 절약되는 시간보다 클 수 있습니다.

리뷰용 워크트리를 한 줄로 만들기

깃허브 PR을 자주 리뷰한다면 매번 fetch와 worktree add를 치는 것도 일입니다. 스크립트로 묶어두면 ./review.sh 142 한 줄로 끝납니다.

# review.sh — PR 번호만 주면 리뷰용 워크트리를 만들어 여는 스크립트
#!/usr/bin/env bash
set -e

PR=$1
if [ -z "$PR" ]; then
  echo "사용법: ./review.sh <PR번호>"
  exit 1
fi

DIR="../review-pr$PR"

git fetch origin "pull/$PR/head:pr-$PR"
git worktree add "$DIR" "pr-$PR"

echo "준비 완료: $DIR"
code "$DIR"   # VS Code로 새 창 열기

pull/<번호>/head는 깃허브가 제공하는 특수 참조로, 포크에서 올라온 PR도 이 경로로 받을 수 있습니다. 마지막 줄의 code는 VS Code CLI이고, 다른 에디터를 쓴다면 그 자리를 바꾸면 됩니다. 리뷰가 끝나면 git worktree remove ../review-pr142와 git branch -D pr-142로 정리합니다.

주의할 점

여기서부터가 실제로 발이 걸리는 지점들입니다.

  • 같은 브랜치를 두 워크트리에서 체크아웃할 수 없습니다. fatal: ... is already checked out at ...가 뜨면 이미 다른 폴더가 그 브랜치를 쓰고 있다는 뜻입니다. git worktree list로 어디인지 확인하세요. 이 제약은 버그가 아니라, 같은 브랜치를 두 곳에서 동시에 커밋해 이력이 꼬이는 것을 막는 안전장치입니다.
  • 무시된(ignored) 파일은 따라오지 않습니다. .env, node_modules, .venv는 새 폴더에 없습니다. 이것 때문에 “워크트리가 고장났다”고 오해하기 쉬운데 정상 동작입니다.
  • 절대 경로를 박아둔 설정은 깨집니다. IDE의 실행 구성이나 도커 볼륨 마운트에 폴더 경로가 하드코딩돼 있으면 새 폴더에서 동작하지 않습니다. 상대 경로로 바꿔두는 편이 안전합니다.
  • 디스크는 생각보다 먹습니다. 이력은 공유하지만 node_modules는 폴더마다 생깁니다. 워크트리 세 개면 그만큼 세 벌입니다.
  • 지운 뒤엔 prune을 잊지 마세요. 탐색기에서 폴더를 통째로 지웠다면 깃은 아직 모릅니다. git worktree prune으로 등록 정보를 정리해야 브랜치가 다시 풀립니다.
# 워크트리는 .gitignore된 파일을 복사하지 않습니다.
# 새 폴더에는 .env도 node_modules도 없습니다.

cd ../myapp-hotfix
cp ../myapp/.env .env
npm ci

# 파이썬 프로젝트라면
python -m venv .venv
.venv/Scripts/activate      # 윈도우
pip install -r requirements.txt

워크트리 폴더를 실수로 저장소 안쪽에 만들면 깃 상태 창이 수천 개의 미추적 파일로 뒤덮입니다. 이때 당황해서 git clean -fd를 치면 워크트리 폴더가 통째로 날아갑니다. 경로는 반드시 저장소 바깥, ../로 시작하세요.

마무리

다음에 작업 도중 급한 요청이 들어오면, stash를 치기 전에 git worktree add ../프로젝트명-hotfix -b hotfix/이름 한 줄을 먼저 쳐보세요. 그 한 번으로 감이 옵니다. 익숙해지면 핫픽스 폴더 하나는 상시로 열어두고 필요할 때 브랜치만 갈아 끼우는 식으로 쓰게 됩니다.

다음 글에서는 협업 중 가장 자주 마주치는 골칫거리인 깃 머지 충돌(merge conflict)을 겁내지 않고 푸는 순서를 다룹니다.

댓글 남기기