큰 AI 생성 PR을 스택 PR로 나누기: git rebase --update-refs와 gh로 계층별 리뷰하는 방법

별도 도구 없이 git과 GitHub CLI만으로 계층 브랜치를 만들고, 리뷰 반영과 squash 병합을 처리하는 순서

큰 AI PR을 스택으로 쪼개기 썸네일

핵심 요약

  1. 큰 AI 생성 변경은 데이터 모델·API·UI처럼 혼자 읽어도 이해되는 계층으로 나누고, 각 PR의 base를 바로 아래 계층으로 둔다.
  2. 리뷰 반영 후에는 맨 위 브랜치에서 git rebase --update-refs를 한 번 실행하면 중간 브랜치도 함께 옮겨진다.
  3. 체크아웃된 브랜치(다른 워크트리 포함)는 --update-refs 대상에서 빠진다.
  4. 아래 PR을 squash 병합했다면 rebase --onto main <옛 base>로 옛 커밋을 건너뛴다.

AI 에이전트에 기능 하나를 통째로 맡기면 데이터 모델, API, 화면이 섞인 PR 하나가 올라온다. 리뷰어는 영역을 오가며 맥락을 다시 세워야 하고, 결국 꼼꼼히 보지 못한 채 승인하는 일이 생긴다. 스택 PR은 변경을 계층별 작은 PR로 나눠 아래에서부터 차례로 쌓는 방식이다. 전용 도구도 있지만, 이 글은 git과 GitHub CLI만으로 처리하는 순서를 정리한다. 아래 출력은 2026-10-07에 로컬 저장소에서 Git 2.43으로 실행한 결과다.

계층을 나누는 기준

계층포함할 변경PR의 base
1. 데이터 모델타입, 스키마, 검증 로직, 시드 데이터main
2. API엔드포인트, 서비스 로직, API 테스트stack/1-model
3. UI화면 컴포넌트, 화면 테스트stack/2-api

기준은 줄 수가 아니라 "이 PR만 봐도 무엇을 하는지 이해되는가"다. 에이전트에 작업을 맡길 때부터 "데이터 모델, API, UI 세 단계로 나눠 각 단계를 별도 브랜치의 커밋으로 만들 것"을 요청하면, 결과가 나온 뒤 쪼개는 것보다 훨씬 쉽다.

스택 만들기와 PR 올리기

  1. main에서 stack/1-model을 만들고 1계층 커밋을 쌓는다.
  2. stack/1-model에서 stack/2-api를, 그 위에 stack/3-ui를 만든다.
  3. 각 브랜치를 푸시하고, PR의 base를 바로 아래 계층 브랜치로 지정한다.
  4. 리뷰는 아래 계층부터 받는다. 기반 계층이 늦게 확정될수록 위 계층을 다시 고칠 일이 커진다.
git push -u origin stack/1-model stack/2-api stack/3-ui

gh pr create --head stack/1-model --base main          --title "1/3 상품 모델과 검증"
gh pr create --head stack/2-api   --base stack/1-model --title "2/3 검색 API" --draft
gh pr create --head stack/3-ui    --base stack/2-api   --title "3/3 검색 UI"  --draft

--base는 병합 대상 브랜치, --head는 변경이 담긴 브랜치다. 위 계층 PR은 --draft로 올려 아래 계층이 승인되기 전에 병합되지 않게 한다. base를 아래 계층으로 두면 각 PR의 diff에는 그 계층의 변경만 보인다.

리뷰 반영: 맨 위에서 rebase 한 번으로 스택 전체 갱신

1계층 리뷰에서 "가격 음수 검증을 추가하라"는 지적을 받아 stack/1-model에 커밋을 추가했다. 이 상태에서 2·3계층은 아직 옛 1계층 위에 있다.

set -e
git init -q -b main && git config user.name demo && git config user.email [email protected]
echo "base" > README.md && git add . && git commit -qm "init"
# 1계층: 데이터 모델
git switch -qc stack/1-model && echo "type Product" > model.ts && git add . && git commit -qm "1: 상품 타입과 검증"
# 2계층: API (1계층 위에)
git switch -qc stack/2-api && echo "GET /search" > api.ts && git add . && git commit -qm "2: 검색 API"
# 3계층: UI (2계층 위에)
git switch -qc stack/3-ui && echo "<SearchBox/>" > ui.tsx && git add . && git commit -qm "3: 검색 UI"
# 리뷰 피드백: 1계층에 수정 커밋 추가
git switch -q stack/1-model && echo "price >= 0" >> model.ts && git commit -qam "1: 가격 음수 검증 추가(리뷰 반영)"
echo "== 수정 직후: 2·3계층은 아직 옛 1계층 위에 있다"
git log --oneline --graph --all --format='%s%d'
# 맨 위 브랜치에서 한 번만 rebase → 중간 브랜치(2-api)도 함께 옮겨진다
git switch -q stack/3-ui && git rebase -q --update-refs stack/1-model
echo "== rebase --update-refs 후"
git log --oneline --graph --all --format='%s%d'
$ bash demo.sh
== 수정 직후: 2·3계층은 아직 옛 1계층 위에 있다
* 1: 가격 음수 검증 추가(리뷰 반영) (HEAD -> stack/1-model)
| * 3: 검색 UI (stack/3-ui)
| * 2: 검색 API (stack/2-api)
|/
* 1: 상품 타입과 검증
* init (main)
== rebase --update-refs 후
* 3: 검색 UI (HEAD -> stack/3-ui)
* 2: 검색 API (stack/2-api)
* 1: 가격 음수 검증 추가(리뷰 반영) (stack/1-model)
* 1: 상품 타입과 검증
* init (main)

git-rebase 문서에 따르면 --update-refs는 rebase되는 커밋을 가리키는 브랜치를 자동으로 강제 갱신한다. 맨 위 stack/3-ui만 rebase했는데 중간의 stack/2-api도 함께 옮겨졌다. 매번 옵션을 붙이기 싫다면 git config rebase.updateRefs true로 기본값을 켠다. 갱신 뒤에는 git push --force-with-lease origin stack/2-api stack/3-ui로 원격도 맞춘다.

아래 PR을 squash 병합한 뒤의 정리

1계층 PR을 squash로 병합하면 main에는 새 커밋 하나가 생기고, 2계층 브랜치에는 원래의 1계층 커밋들이 그대로 남는다. 이때 2계층을 main 위로 그냥 rebase하면 같은 내용을 두 번 적용하려다 충돌한다.

$ git rebase main   # stack/2-api에서
CONFLICT (add/add): Merge conflict in model.ts
Rebasing (1/3)error: could not apply cd2a2f5... 1: 상품 타입과 검증

--onto로 "옛 1계층 이후의 커밋만" 옮기면 충돌 없이 정리된다. 이때 맨 위 브랜치를 체크아웃한 상태에서 실행해야 한다. 처음에는 stack/2-api가 체크아웃된 상태에서 git rebase --update-refs --onto main stack/1-model stack/3-ui를 실행했는데, 3계층은 옮겨졌지만 2계층 브랜치는 옛 위치에 남았다. 문서대로 작업 트리에 체크아웃된 브랜치는 --update-refs 대상에서 빠지기 때문이다.

$ git switch stack/3-ui && git rebase --update-refs --onto main stack/1-model
* 3: 검색 UI (HEAD -> stack/3-ui)
* 2: 검색 API (stack/2-api)
* 1: 상품 모델 (#101 squash) (main)

GitHub 쪽에서는 병합된 PR의 head 브랜치를 삭제하면, 그 브랜치를 base로 쓰던 열린 PR의 base가 병합된 PR의 base(여기서는 main)로 자동으로 바뀐다. 브랜치를 삭제하지 않는 저장소라면 gh pr edit <번호> --base main으로 직접 바꾼다. base를 바꾸면 일부 리뷰 코멘트가 outdated로 표시될 수 있다.

운영 규칙

  • 스택은 3~4계층을 넘기지 않는다. 길어질수록 아래 계층 수정이 위로 전파되는 비용이 커진다.
  • PR 제목에 1/3, 2/3처럼 순서를 붙이고, 본문에 이전·다음 PR 링크를 적는다.
  • 각 계층에서 CI가 독립적으로 통과해야 한다. 위 계층 없이는 빌드가 깨지는 계층 분할은 다시 나눈다.
  • 리뷰 규칙(AI 사용 범위 표시 등)은 계층마다 동일하게 적용한다. 템플릿과 자동 검사는 AI 생성 코드 리뷰 규칙을 참고한다.

에이전트 작업을 브랜치별로 격리하는 방법은 Git Worktree로 에이전트 작업 격리에서 이어진다. 워크트리를 쓰면 계층 브랜치가 다른 작업 트리에 체크아웃되어 있을 수 있는데, 이 경우에도 해당 브랜치는 --update-refs로 갱신되지 않는다.

초보자가 자주 실수하는 포인트

  • 결과물이 나온 뒤에 큰 PR을 수동으로 쪼개려다 시간을 더 쓴다.
  • 위 계층 PR을 draft로 두지 않아 아래 계층보다 먼저 병합된다.
  • 중간 브랜치를 체크아웃한 채 --update-refs를 실행해 그 브랜치가 갱신되지 않는다.
  • squash 병합 뒤 위 계층을 main 위로 그냥 rebase해 충돌을 만든다.

체크리스트

  • 계층마다 혼자 읽어도 이해되는 범위로 나눴다.
  • 각 PR의 base가 바로 아래 계층 브랜치다.
  • 위 계층 PR은 draft 상태다.
  • rebase.updateRefs를 켰거나 --update-refs를 붙여 rebase한다.
  • 아래 PR 병합 후 위 PR의 base가 main으로 바뀌었는지 확인했다.

자주 묻는 질문

Graphite 같은 전용 도구가 꼭 필요한가요?

필수는 아닙니다. 이 글처럼 git의 --update-refs와 gh만으로도 운영할 수 있습니다. 스택이 자주 길어지거나 여러 사람이 같은 스택을 다룬다면 동기화와 병합 순서를 자동화하는 도구가 편할 수 있습니다.

계층 PR에 force push를 해도 리뷰 코멘트가 유지되나요?

변경된 줄에 달린 코멘트는 outdated로 표시될 수 있습니다. 리뷰 반영 커밋은 새 커밋으로 추가하고, 병합 직전에만 정리하면 리뷰어가 무엇이 바뀌었는지 보기 쉽습니다.

참고 자료 · 검증 기준

위 자료와 내용을 대조한 날짜: . 도구·서비스 정책은 이후 바뀔 수 있으므로 적용 전 공식 문서를 다시 확인하세요.

이 글은 실제 실무 환경 테스트 및 검증된 기술 자료를 바탕으로 작성되었으며, 환경 변화에 따라 지속적으로 갱신됩니다.