엔지니어가 아닌 PM으로서 AI 코딩 도구를 이용하면서 git을 제대로 이해하지 못한 채 작업을 진행했습니다. 브랜치를 생성하고, 한 작업을 진행해서 PR을 생성하고, GitHub에 코드를 배포하는 패턴으로 이용했습니다. 웹앱을 만들 때는 감으로 대략 2~3개 세션을 동시에 실행할 때 서로 겹치지 않을 범위로 작업하고, 소스 코드가 충돌하면 그때그때 클로드 코드가 해결하는 방식으로 진행했습니다. iOS 앱을 개발할 때는 전혀 감이 잡히지 않아서 단일한 작업을 하고, 완료하면 다음 작업으로 넘어가는 식으로 진행했습니다.
그런데 최근에 유튜브 영상 제작 파이프라인 개발을 테스트하면서 여러 작업을 동시에 진행하고 싶다는 생각이 들었습니다. 동시에 많은 세션(예: 5~6개)을 실행하니 클로드 코드가 코드 관리와 관련해 여러 경고를 했고, 워크트리라는 개념을 공부해야겠다고 생각했습니다. 예전에 클로드 코드를 만든 Boris Cherny가 git의 워크트리를 이용해 병렬 작업을 한다는 것은 들었지만, 실제로 이를 제대로 이해하지는 못했습니다.
1. git 상태는 '세션'이 아니라 '폴더' 기반임
클로드 코드 세션을 여러 개 켜도 git은 세션 단위로 구분하지 않습니다. 어차피 클로드 코드는 git과 별도 도구이기 때문에, 같은 폴더를 바라보고 있으면 모든 세션이 같은 작업 공간을 공유합니다. (좀 더 정확히 말하면 작업 파일과 스테이징, 그리고 현재 위치(HEAD)는 워크트리(폴더)마다 따로 갖지만, 브랜치와 커밋 이력은 저장소 하나를 함께 씁니다)
그래서 한 세션이 저장(커밋)할 때 git add로 변경을 통째로 진행하면 같은 폴더에서 다른 세션이 작업 중이던 코드까지 함께 커밋됩니다. 저는 실제로 이런 식으로 커밋할 때 다른 세션의 변경 작업이 섞이기도 했습니다.
사실 더 근본적인 위험은 커밋 단계 이전에도 있습니다. 두 세션이 같은 파일을 동시에 편집하면 커밋을 하기도 전에 서로의 변경작업을 덮어쓸 수 있습니다. 어쨌든 한 폴더를 공유한다는 것 자체가 리스크입니다.
2. 워크트리를 활용한 작업 공간 분리
위의 문제는 결국 여러 세션이 폴더 하나를 같이 본다는 점에서 옵니다. 이에 접근할 수 있는 방법은 세션마다 폴더를 따로 부여하는 것입니다. 이때 git의 워크트리(worktree)를 활용할 수 있습니다. 워크트리는 같은 프로젝트를 별도 폴더로 한 벌 더 만들어 놓은 작업 공간입니다.
여기서 참고할 만한 점은, 워크트리는 작업 기록(git 이력)만 공유하고 실제 파일은 폴더마다 복사되기 때문에, .env 같은 환경 설정 파일은 새 폴더마다 따로 챙겨줘야 한다는 것입니다. 클로드 코드에는 이걸 자동으로 복사해주는 .worktreeinclude라는 기능이 있는데, 이 파일은 .gitignore 문법으로 패턴을 적고, 그 패턴에 맞으면서 동시에 gitignore된 파일만 복사합니다. 자세한 설정 방법은 공식 문서를 참고하면 됩니다.
이런 번거로움이 있긴 해도 저장소를 통째로 여러 번 복제하는 것에 비하면 훨씬 가볍고, 무엇보다 각 폴더에서 서로 다른 브랜치로 독립적으로 작업할 수 있다는 게 장점입니다.
클로드 코드에서 병렬 작업을 할 때 제가 이해한 핵심 규칙은 **"1 세션 = 1 워크트리 = 1 작업 브랜치 = 1 PR"**입니다. 즉 클로드 코드 세션 하나가 자기 전용 폴더에서 자기 전용 작업 브랜치로 하나의 결과물(PR)을 만드는 구조인 셈입니다. 세션을 5~6개 동시에 돌리더라도 각자 다른 폴더에서 작업하면 서로의 수정사항이 같은 작업 공간에서 섞이지 않습니다.
관련해서 클로드 코드에서는 EnterWorktree라는 도구를 지원합니다. 이 도구를 호출하면 별도 폴더와 새 브랜치를 만들어줍니다. 예를 들어 세션에서 "워크트리로 작업해줘"라고 요청하면 클로드 코드가 EnterWorktree 도구를 호출해 새 폴더와 새 브랜치를 만들어줍니다. 이때 폴더와 브랜치 이름에는 기본 규칙이 있습니다. render-fix라는 이름으로 워크트리를 만들면, 클로드 코드는 기본적으로 .claude/worktrees/render-fix/ 폴더를 만들고, 브랜치는 worktree- 접두사가 붙은 worktree-render-fix로 생성합니다. 즉 폴더 이름은 내가 준 이름 그대로지만, 브랜치 이름은 기본적으로 worktree- prefix가 붙는다고 보면 됩니다(이 동작은 설정으로 바꿀 수 있습니다).
예를 들어 세션을 두 개 병렬로 돌린다면 첫 프롬프트를 이렇게 시작할 수 있습니다.
- 세션 A (예: 렌더링 성능 개선): "워크트리를 만들어서 작업할게요. 이름은 render-fix로 하고요. 렌더링 속도가 느린 원인을 찾아서 개선해주세요."
- 세션 B (예: 스톡 이미지 재활용 파이프라인): 다른 세션 창에서 "워크트리 stock-lib 만들어서 시작해주세요. 에피소드 간 스톡 이미지를 재활용하는 구조를 잡아주세요."
핵심은 세션마다 이름을 다르게 주는 것뿐입니다(render-fix, stock-lib처럼). 정확히는 폴더 이름보다 브랜치가 겹치지 않는 게 중요한데(같은 브랜치를 두 워크트리가 동시에 쓰는 건 일반적으로 막혀 있음), 이름을 다르게 주면 자연히 둘 다 해결됩니다.
그리고 보통은 클로드 코드가 원격 저장소의 기본 브랜치(origin/HEAD, 보통 main 또는 master)를 기준으로 워크트리를 만듭니다. 사실 기본 동작 자체가 원격 기준으로 출발하면서 내부적으로 fetch를 시도하고, 원격이 없거나 fetch가 실패하면 현재 로컬 HEAD로 폴백하도록 되어 있습니다.
그래서 일부 사례로 나오는 "fetch 먼저 하고 worktree를 만들어줘"라는 요청은 틀린 건 아니지만 사실상 기본 동작에 이미 포함된 안전장치를 한 번 더 확인하는 것일 수도 있습니다. 반대로 현재 로컬 브랜치의 미완성 작업을 기준으로 워크트리를 만들고 싶다면 worktree.baseRef를 "head"로 설정할 수 있습니다(이 설정은 "fresh"나 "head" 두 값만 받습니다). 다만 일반적인 병렬 작업에서는 기본값처럼 원격 default branch 기준으로 시작하는 편이 더 안전한 것 같습니다.
이밖에 저는 세션 단위로 워크트리를 구분하지만, 한 세션 안에서 서브 에이전트 단위로 워크트리를 나눠 병렬 작업을 진행하는 케이스도 있습니다.
3. 병렬로 세션을 실행할 때 헷갈리는 지점
클로드 코드의 세션 A, B를 동시에 실행하다 보면 몇 가지 지점에서 헷갈릴 때가 있습니다. 하나는 작업을 시작할 때(워크트리를 각각 만들 때)이고, 다른 하나는 작업을 끝낼 때(각자 PR을 머지할 때)입니다. 두 경우 모두 세션 A와 B가 서로 간섭하지 않을까 하는 우려인데요. 대부분 클로드 코드가 자동으로 해결해 줍니다.
(1) 작업을 시작할 때: 워크트리를 각각 만들면 충돌하지 않음
"세션 A에서 워크트리를 만든 상태인데, 세션 B에서 또 EnterWorktree를 하면 충돌하지 않을까?" → 이름만 다르게 주면 충돌하지 않습니다.
세션 A → EnterWorktree(name="render-fix") → 작업공간 1
세션 B → EnterWorktree(name="stock-lib") → 작업공간 2
각 세션이 물리적으로 다른 폴더에서 작업하기 때문에 충돌에서 안전합니다. 대신 주의할 점은 워크트리를 만들지 않고 메인 폴더에서 그대로 작업하는 세션이 리스크라는 것입니다. 따라서 작업하는 세션은 모두 자기 워크트리 안에서 진행하고, 메인 폴더는 보기 전용으로 비워둡니다.
(2) 작업을 끝낼 때: 세션 B가 바라보던 코드가 뒤처졌을 때
이게 가장 자주 있는 상황입니다. 세션 A가 먼저 작업을 끝내고 PR을 머지하면 원격 저장소의 기본 브랜치(보통 main 또는 master)가 갱신됩니다. 그런데 세션 B는 A가 작업하기 이전의 기본 브랜치를 기준으로 작업을 시작했습니다. 저는 이 상태에서 B의 PR을 머지하면 어떻게 되는지 제일 헷갈렸습니다.
결론부터 말하면 B가 과거 소스 코드 기준에서 출발한 것 자체는 대부분 이슈가 아닙니다. 머지가 가능한 상태라면, 머지하는 순간 GitHub가 현재 최신 기본 브랜치 위에 B의 변경사항을 합쳐 줍니다. git이 자동으로 막아주는 건 양쪽이 같은 부분을 동시에 건드렸거나, 한쪽은 파일을 삭제하고 다른 쪽은 같은 파일을 수정한 경우처럼 자동으로 합치기 어려운 상황입니다. 케이스로 구분하면 아래와 같습니다.
case 1. A와 B가 다른 파일을 변경함 (대부분의 경우)
PR 화면에 "no conflicts"가 뜨면 보통은 그대로 Merge하면 됩니다. GitHub가 현재 최신 base branch 위에 A의 변경과 B의 변경을 자동으로 합쳐 줍니다. 다만 한 가지 주의할 점은 no conflict는 git이 텍스트 단위로 충돌하지 않았다는 뜻이지 코드가 논리적으로 맞는다는 보장은 아니라는 점입니다. 그래서 머지 전에 테스트가 통과하는지, 기능이 의도대로 동작하는지 여부를 별도로 확인해야 합니다.
case 2. A와 B가 같은 부분을 변경함 (충돌)
이때는 GitHub가 자동 머지를 막습니다. 세션 B에 "최신 기본 브랜치를 내 작업 브랜치에 반영하고 충돌을 정리해달라"고 요청하면, 클로드 코드가 최신 코드를 받아 합치고 다시 푸시해서 PR을 갱신해 줍니다. 충돌난 코드를 제가 직접 해결할 필요는 없습니다.
다만 엔지니어가 아닌 저 같은 경우에는 초반에 "무조건 양쪽을 다 살려라"라고 시켰다가 오히려 깨진 결과를 받곤 했습니다. 지금은 이렇게 프롬프트를 시도합니다.
"두 변경의 의도를 모두 살려서 자연스럽게 하나로 합쳐줘. 합치는 과정에서 판단이 필요한 부분이 있으면, 어떻게 합쳤는지(또는 합칠 계획인지)를 코드 읽을줄 모르는 사람도 알 수 있게 쉬운 말로 설명해주세요. 다 합친 뒤에는 양쪽 기능이 모두 정상 동작하는지 직접 테스트하고 깨진 게 있으면 수정해주세요."
정리하면, 작업을 시작할 때는 워크트리 이름을 다르게 하면 안전하고, 작업을 마무리할 때는 "코드가 뒤처진 것" 자체는 GitHub가 알아서 처리하며, 사람(또는 AI 도구)이 개입하는 건 충돌이 났을 때인 것 같습니다.
4. 작업 정리
PR이 머지되면 더 이상 쓸 일이 없는 워크트리와 브랜치를 정리합니다. 클로드 코드는 ExitWorktree 도구로 해당 세션이 만든 워크트리를 정리해 줄 수 있습니다. 정리는 세션에 아래와 같이 요청할 수 있습니다.
"PR 머지됐어요. 이 워크트리랑 브랜치 정리해주세요."
ExitWorktree는 워크트리 세션을 끝내고 작업 위치를 원래 메인 폴더로 되돌리는 역할을 합니다. 단, 이 세션의 EnterWorktree로 만든 워크트리에만 동작하며, 수동으로 만들었거나 이전 세션에서 만든 워크트리는 건드리지 않습니다.
정리 방식은 워크트리의 현재 상태에 따라 달라집니다. 커밋하지 않은 변경사항, untracked file, 새 커밋이 하나도 없으면 워크트리와 브랜치가 자동으로 제거됩니다. 반대로 이 중 하나라도 남아 있으면 자동 삭제 대신 항상 keep/remove를 물어봅니다. 또한 세션에 이름을 붙여 둔 경우에는, 변경사항이 없더라도 나중에 다시 쓸 수 있도록 자동 삭제하지 않고 keep/remove를 확인합니다.
remove를 선택하면 로컬 워크트리 디렉터리와 해당 로컬 브랜치가 함께 제거됩니다. 이 과정에서 남아 있던 변경사항이나 커밋도 같이 버려질 수 있으므로 PR이 실제로 머지됐는지 확인한 뒤 정리하는 것이 안전합니다. keep을 선택하면 워크트리와 브랜치를 그대로 남겨 둡니다.
일반 git에서는 "폴더 삭제(git worktree remove)"와 "브랜치 삭제(git branch -d)"를 두 단계로 나눠 실행하지만 클로드 코드에서 remove를 선택하면 이 두 가지가 함께 처리할 수 있습니다. 다만 이것은 내 컴퓨터의 로컬 폴더와 로컬 브랜치를 정리하는 것입니다. GitHub에 올라간 원격 브랜치는 별개라서 PR을 머지할 때 GitHub의 "Delete branch" 버튼을 누르거나 클로드 코드 세션에서 원격 브랜치도 지워달라고 따로 요청해야 합니다.
