gh stack으로 PR 쌓아 올리기

gh stack으로 PR 쌓아 올리기

PR 하나에 파일 30개가 바뀌고 코드가 2,000줄 넘게 추가된 걸 리뷰해달라고 받아본 적 있으신가요? 😅 리뷰어 입장에서는 어디서부터 봐야 할지 막막하고, 작성자 입장에서는 피드백을 기다리는 시간이 길어지죠.

“PR은 작게 만들어라”라는 원칙은 누구나 알고 있지만 실제로 지키기가 쉽지 않습니다. 큰 기능을 구현하다 보면 인증 레이어, API 엔드포인트, 프론트엔드 화면이 서로 물려 있어서 하나만 떼어내기 어렵거든요. 억지로 쪼개봤자 의존하는 브랜치끼리 rebase를 수동으로 맞추느라 시간을 허비하게 됩니다.

이 문제를 해결하기 위해 GitHub이 직접 만든 도구가 바로 gh stack입니다. 큰 변경사항을 여러 개의 작은 PR로 쪼개고, 그 PR들 사이의 의존 관계와 rebase를 자동으로 관리해줍니다.

Stacked PR이란?

Stacked PR은 하나의 큰 변경을 여러 PR로 나눠서 아래에서 위로 쌓아 올리는 방식입니다. 각 PR이 바로 아래 PR의 브랜치를 base로 삼고, 가장 아래 PR만 main을 직접 가리킵니다.

frontend      → PR #3 (base: api-routes)     ← 맨 위
api-routes    → PR #2 (base: auth-layer)
auth-layer    → PR #1 (base: main)            ← 맨 아래
──────────────
main (trunk)

이렇게 쌓으면 각 PR이 하나의 논리적 단위만 담당합니다. 리뷰어는 PR #1에서 인증 코드만, PR #2에서 API 라우트만, PR #3에서 화면 코드만 보면 되죠. PR 하나당 변경 범위가 작으니 리뷰 품질도 올라가고 피드백 주기도 빨라집니다.

문제는 이걸 수동으로 관리하면 고통스럽다는 겁니다. PR #1에 수정이 생기면 PR #2와 #3을 순서대로 rebase해야 하고 브랜치 base도 직접 바꿔줘야 하고 충돌도 하나씩 잡아야 해요. gh stack은 이 과정을 자동화합니다.

gh stack이란?

gh stack은 GitHub이 공식으로 만든 gh CLI 확장(extension)입니다. 터미널에서 gh stack 명령어 하나로 스택을 생성하고, PR을 만들고, 계단식 rebase를 수행하고, 전체 스택을 한눈에 볼 수 있습니다.

기존에도 Graphite, ghstack(Meta) 같은 서드파티 도구가 있었는데요. gh stack이 결정적으로 다른 점은 GitHub 플랫폼 자체가 스택을 이해한다는 것입니다. 서드파티 도구는 GitHub의 기존 PR 모델 위에서 우회적으로 동작하지만, gh stack을 쓰면 GitHub UI에 스택 내비게이터가 나타납니다. 브랜치 보호 규칙도 스택 전체에 제대로 적용되고 GitHub Actions도 각 PR이 main을 대상으로 하는 것처럼 트리거돼요.

현재(2026년 4월 기준) gh stack프라이빗 프리뷰 단계입니다. CLI와 플랫폼 기능 모두 저장소별로 활성화해야 동작하고, 대기 목록에 등록해서 접근 권한을 받아야 합니다. 버전도 아직 v0.0.1이라 빠르게 변화하고 있어요.

설치

gh CLI가 이미 설치되어 있다면 확장을 추가하기만 하면 됩니다.

gh extension install github/gh-stack

gh CLI는 v2.0 이상, Git은 v2.20 이상이 필요합니다. 설치 후 gh stack 명령어가 동작하는지 확인해보세요.

gh stack --help

gh stack은 타이핑할 게 좀 있으니 짧은 별칭을 등록해두면 편합니다.

gh stack alias         # gs라는 별칭 생성
gh stack alias gst     # 원하는 이름으로 지정 가능

이제 gs만 쳐도 gh stack과 동일하게 동작합니다. 이후 예제에서도 gs를 사용하겠습니다.

스택 만들기

새로운 기능 작업을 시작해봅시다. 사용자 인증, API 엔드포인트, 프론트엔드 화면 세 단계로 나눠서 스택을 쌓아볼게요.

먼저 스택을 초기화합니다.

gs init

이 명령은 현재 브랜치를 스택의 첫 번째 레이어로 등록합니다. --base 플래그로 base 브랜치를 지정할 수 있는데, 기본값은 main입니다.

브랜치 이름을 자동으로 번호를 매겨서 만들고 싶다면 --prefix--numbered 옵션을 쓸 수 있습니다.

gs init --prefix feat --numbered
# feat/01 브랜치가 생성됨

첫 번째 레이어에서 인증 관련 코드를 작성한 뒤, 다음 레이어를 추가합니다.

# 인증 코드 작성 후 커밋
git add -A
git commit -m "Add JWT authentication middleware"

# 스택에 새 레이어 추가
gs add api-routes

gs add는 현재 레이어 위에 새 브랜치를 만들어 줍니다. 커밋과 브랜치 생성을 동시에 하고 싶다면 -A(모든 변경사항 스테이징)와 -m(커밋 메시지) 플래그를 함께 쓸 수 있습니다.

# API 라우트 코드 작성 후
gs add frontend -Am "Add REST API endpoints for user management"

이렇게 하면 현재 레이어의 변경사항을 커밋하고, frontend라는 새 브랜치를 만들어 스택 위에 올립니다. 프론트엔드 코드까지 작성하면 스택이 완성됩니다.

스택 확인하기

현재 스택의 상태를 확인하려면 view 명령을 사용합니다.

gs view

스택의 각 레이어가 위에서 아래로 표시되고 현재 체크아웃한 브랜치에 화살표가 붙습니다. PR을 이미 만들었다면 PR 번호와 리뷰 상태도 함께 나와요.

간단한 한 줄 요약이 필요하면 --short 플래그를, CI 도구에서 파싱하려면 --json 플래그를 쓸 수 있습니다.

PR 만들기

스택의 모든 레이어를 한 번에 push하고 PR을 생성하려면 submit 명령을 사용합니다.

gs submit

이 한 줄이면 스택에 포함된 모든 브랜치가 원격에 push되고 각 레이어마다 PR이 만들어집니다. base 브랜치도 스택 구조에 맞게 알아서 설정돼요. PR #3의 base는 PR #2의 브랜치, PR #2의 base는 PR #1의 브랜치가 되는 식이죠.

처음에는 드래프트 PR로 만들고 싶다면 --draft 플래그를 추가하면 됩니다.

gs submit --draft

PR을 만든 뒤 GitHub 웹에 가보면 일반 PR과는 다르게 스택 내비게이터가 PR 상단에 나타납니다. 스택에 포함된 모든 PR이 순서대로 나열되고 각각의 리뷰 상태와 CI 결과를 한눈에 볼 수 있어요. 리뷰어가 클릭 한 번으로 스택의 다른 PR로 이동할 수 있어서 전체 맥락을 파악하기도 쉽습니다.

스택 안에서 이동하기

스택의 레이어 사이를 이동하는 명령어도 있습니다.

gs up        # trunk에서 먼 방향으로 한 칸 위로
gs down      # trunk 방향으로 한 칸 아래로
gs top       # 스택 맨 위로
gs bottom    # 스택 맨 아래로 (trunk 바로 위)

숫자를 붙이면 여러 칸을 한 번에 이동할 수 있습니다.

gs up 2      # 두 칸 위로
gs down 3    # 세 칸 아래로

기존에 git switchgit checkout으로 브랜치 이름을 직접 타이핑하던 것보다 훨씬 편합니다. 스택 구조를 기억하고 있으니 “위로”, “아래로”라는 상대적 개념으로 이동할 수 있는 거죠.

다른 사람이 만든 스택을 받아서 리뷰하려면 checkout 명령을 사용합니다.

gs checkout 42       # PR #42가 속한 스택 전체를 체크아웃
gs checkout feat/01  # 브랜치 이름으로도 가능

계단식 Rebase

Stacked PR의 핵심이자 수동으로 할 때 가장 고통스러운 부분이 바로 rebase입니다. 스택 아래쪽 레이어에 변경이 생기면 위쪽 레이어도 전부 rebase해야 하거든요.

gs rebase는 이걸 한 방에 처리합니다.

gs rebase

현재 레이어부터 위쪽 레이어를 순서대로 rebase합니다. 스택이 세 개 레이어인데 맨 아래 PR에 수정을 했다면 가운데와 맨 위 레이어가 차례로 rebase돼요.

특정 방향만 rebase하고 싶다면 플래그를 쓸 수 있습니다.

gs rebase --downstack    # 현재 레이어 아래쪽만 rebase
gs rebase --upstack      # 현재 레이어 위쪽만 rebase

rebase 중 충돌이 발생하면 일반 Git rebase처럼 충돌을 해결한 뒤 계속할 수 있습니다.

# 충돌 파일 수정 후
git add .
gs rebase --continue

충돌 해결이 복잡해서 포기하고 싶다면 --abort로 원래 상태로 돌아갈 수 있습니다.

gs rebase --abort

gh stackgit rerere도 알아서 켜줍니다. rerere는 “reuse recorded resolution”의 약자로, 한 번 해결한 충돌 방식을 기억했다가 같은 충돌이 다시 나타나면 자동 적용해주는 기능이에요. 스택 작업에서는 같은 충돌을 반복적으로 만나기 쉬우니 꽤 유용합니다.

동기화

rebase와 push를 한 번에 하고 싶다면 sync 명령이 있습니다.

gs sync

sync는 원격에서 최신 변경을 fetch하고 스택 전체를 rebase한 뒤 모든 브랜치를 push하고 PR 상태까지 맞춰줍니다. 아침에 출근해서 작업 시작 전에, 또는 리뷰 피드백을 반영한 뒤에 돌리면 좋아요.

브랜치만 push하고 PR 상태 동기화는 건너뛰고 싶다면 push 명령을 별도로 쓸 수도 있습니다.

gs push

push는 내부적으로 --force-with-lease --atomic 옵션을 씁니다. --force-with-lease는 누군가 원격 브랜치를 수정했을 때 push를 거부해서 작업 유실을 막아주고, --atomic은 모든 브랜치가 한꺼번에 push되거나 아예 push되지 않도록 보장해줍니다.

머지 전략

Stacked PR을 머지할 때는 반드시 아래에서 위로 순서를 지켜야 합니다. 가운데 PR을 먼저 머지하는 건 불가능합니다.

GitHub이 지원하는 세 가지 머지 방식 모두 사용할 수 있습니다.

  • Merge commit — 머지 커밋을 만들어서 합침
  • Squash merge — 여러 커밋을 하나로 합쳐서 머지
  • Rebase merge — 커밋을 base 위에 재배치

맨 아래 PR을 머지하면 나머지 PR의 base가 알아서 업데이트됩니다. PR #1이 main에 머지되면 PR #2의 base가 main으로 바뀌는 식이에요. 전부 머지하지 않고 아래쪽 일부만 먼저 머지하는 것도 됩니다.

GitHub의 머지 큐(merge queue)와도 연동됩니다. 스택의 모든 PR을 한꺼번에 큐에 넣으면 올바른 순서로 머지되고, 중간에 하나가 실패하면 그 위의 PR도 모두 큐에서 빠져요.

브랜치 보호 규칙과 CI

서드파티 도구를 쓸 때 가장 골치 아픈 부분 중 하나가 브랜치 보호 규칙입니다. 일반적으로 보호 규칙은 main 브랜치를 대상으로 하는 PR에만 적용되는데, Stacked PR은 중간 브랜치를 base로 삼기 때문에 규칙이 우회되는 문제가 있었습니다.

gh stack은 GitHub 플랫폼 수준에서 이 문제를 해결합니다. 스택 내 각 PR이 중간 브랜치를 base로 삼더라도 보호 규칙은 스택의 최종 base(main)를 기준으로 적용돼요. 필수 리뷰어 승인이나 CI 통과 같은 조건이 모든 PR에 똑같이 강제되죠.

GitHub Actions 워크플로우도 마찬가지예요. on: pull_requestbranches: [main]을 지정해두면 스택의 모든 PR에서 워크플로우가 트리거됩니다. 각 PR이 마치 main을 직접 대상으로 하는 것처럼 동작하니 별도 설정이 필요 없어요.

실전 워크플로우 예시

전체 흐름을 처음부터 끝까지 한번 따라가 보겠습니다. 사용자 프로필 기능을 추가하는 시나리오입니다.

# 1. 스택 시작 — 자동 번호 매김으로 깔끔하게
gs init --prefix profile --numbered
# profile/01 브랜치 생성

# 2. 데이터 모델 작성
# ... User, Profile 모델 코드 작성 ...
git add -A && git commit -m "Add user profile data models"

# 3. 다음 레이어 추가 — API 작성
gs add -Am "Add profile API endpoints"
# profile/02 브랜치 생성, 이전 변경사항 자동 커밋
# ... API 라우트 코드 작성 ...
git add -A && git commit -m "Implement GET/PUT /api/profile"

# 4. 마지막 레이어 — 프론트엔드
gs add -Am "Add profile page UI"
# profile/03 브랜치 생성
# ... React 컴포넌트 코드 작성 ...
git add -A && git commit -m "Create ProfilePage component"

# 5. 한 번에 push하고 PR 생성
gs submit --draft

# 6. 스택 확인
gs view

리뷰어가 PR #1에 “유효성 검사를 추가해달라”고 피드백을 남겼다고 해봅시다.

# 7. 맨 아래 레이어로 이동
gs bottom

# 8. 피드백 반영
# ... 유효성 검사 코드 추가 ...
git add -A && git commit -m "Add input validation for profile fields"

# 9. 위쪽 레이어 전부 rebase
gs rebase

# 10. 전체 push
gs push

리뷰가 통과되면 아래부터 순서대로 머지합니다. 첫 번째 PR이 머지되면 나머지 PR의 base가 자동으로 조정됩니다.

기존 브랜치로 스택 구성하기

이미 의존 관계가 있는 브랜치를 만들어둔 상태라면 --adopt 옵션으로 기존 브랜치를 스택으로 묶을 수 있습니다.

gs init --adopt auth-layer api-routes frontend

이렇게 하면 auth-layerapi-routesfrontend 순서의 스택이 만들어집니다. 이미 PR이 생성되어 있다면 submit 시 새 PR을 만드는 대신 기존 PR에 스택 정보를 연결합니다.

스택 해제와 삭제

스택 구조가 더 이상 필요 없으면 해제할 수 있습니다.

gs unstack          # 스택 구조 해제 (브랜치와 PR은 유지)
gs delete            # 스택 해제 + 로컬 브랜치 삭제
gs delete --local    # 로컬 추적 정보만 삭제

unstack은 GitHub 쪽의 스택 연결을 풀고 로컬 추적 정보를 제거하지만, 브랜치와 PR 자체는 그대로 둡니다. 각 PR이 독립된 일반 PR로 돌아가는 거죠.

대안 도구 비교

gh stack 외에도 Stacked PR을 지원하는 도구가 여럿 있습니다.

Graphite는 가장 성숙한 상용 서비스입니다. 자체 웹 UI에 AI 코드 리뷰까지 갖추고 있어서 기능이 풍부하지만, GitHub 위에 별도 레이어를 올리는 방식이라 조직 전체가 Graphite를 도입해야 제대로 쓸 수 있습니다.

Aviator의 av는 오픈소스 CLI로 Aviator의 머지 큐와 통합됩니다. ghstack(Meta)은 커밋 단위로 PR을 만드는 방식으로 Meta 내부의 Phabricator 워크플로우에서 영향을 받았습니다. Sapling(Meta)과 Jujutsu(jj)는 Git과는 다른 버전 관리 도구인데, 둘 다 스택형 커밋을 기본으로 지원합니다.

gh stack의 가장 큰 장점은 GitHub 플랫폼과 네이티브로 통합된다는 점입니다. 다른 도구가 GitHub PR 모델 위에서 간접적으로 동작하는 반면, gh stack을 쓰면 UI에 스택 내비게이터가 뜨고 보호 규칙과 CI가 스택을 인식하며 머지 큐도 스택 단위로 처리돼요. 별도 서비스 가입이나 팀 전체 도입 없이 gh 확장 하나만 설치하면 되는 것도 장점입니다.

반면 아직 프라이빗 프리뷰이고 v0.0.1이라는 점은 감안해야 합니다. gs merge 명령이 아직 구현되지 않았고 스택 중간에 레이어를 삽입하거나 순서를 바꾸는 것도 안 됩니다. 교차 포크(cross-fork) 스택도 지원하지 않아서 모든 브랜치가 같은 저장소에 있어야 해요.

호환성

흥미로운 점은 gh stack CLI가 아닌 다른 도구로도 GitHub의 Stacked PR 플랫폼 기능을 쓸 수 있다는 것입니다. PR의 base 브랜치만 제대로 설정하면 Jujutsu, Sapling, git-town 같은 도구로 만든 PR도 GitHub이 스택으로 인식해요. gh stack CLI는 편의 도구일 뿐, 플랫폼 기능 자체는 CLI에 종속되지 않습니다.

GitHub 웹 UI에서도 직접 스택을 만들 수 있습니다. PR 생성 시 base 브랜치를 다른 PR의 브랜치로 지정하면 GitHub이 스택 관계를 알아서 인식하고 스택 내비게이터를 띄워줍니다.

마치며

PR이 커지면 리뷰 품질이 떨어지고, 리뷰 품질이 떨어지면 버그가 늘고, 버그가 늘면 또 PR이 커지는 악순환. Stacked PR은 이 고리를 끊는 확실한 방법인데 그동안은 수동 관리가 너무 번거로워서 실제로 적용하기 어려웠죠.

gh stack은 GitHub의 첫 번째 공식 Stacked PR 도구로서 CLI와 플랫폼 양쪽에서 스택을 지원한다는 점에서 의미가 큽니다. 아직 프리뷰 단계라 프로덕션에 바로 도입하기는 이르지만, 대기 목록에 등록해두고 미리 워크플로우에 익숙해져 두면 정식 출시 때 바로 활용할 수 있을 겁니다.

더 자세한 내용은 gh-stack 공식 문서를 참고하세요.

This work is licensed under CC BY 4.0 CC BY

개발자를 위한 뉴스레터

달레가 정리한 AI 개발 트렌드와 직접 만든 콘텐츠를 전해드립니다.

Discord