
Git, 이것만 알면 협업한다 6편 — Pull Request로 함께 일하기
혼자 쓰던 git을 팀의 도구로 바꾸는 마지막 한 걸음, Pull Request. 브랜치를 만들어 push하고, PR을 열고, 리뷰를 주고받고, 세 가지 머지 버튼 중 하나로 main에 합치는 GitHub Flow 한 바퀴를 민지와 도윤의 카페 웹사이트로 따라가 봅니다. 머지 방식마다 역사가 어떻게 달라지는지는 로컬에서 직접 재현해 확인합니다.

혼자 쓰던 git을 팀의 도구로 바꾸는 마지막 한 걸음, Pull Request. 브랜치를 만들어 push하고, PR을 열고, 리뷰를 주고받고, 세 가지 머지 버튼 중 하나로 main에 합치는 GitHub Flow 한 바퀴를 민지와 도윤의 카페 웹사이트로 따라가 봅니다. 머지 방식마다 역사가 어떻게 달라지는지는 로컬에서 직접 재현해 확인합니다.
5편까지 온 민지는 이제 혼자서도 꽤 능숙합니다. 커밋을 쌓고, push·pull로 GitHub와 주고받고, 브랜치를 만들어 실험하고, 충돌이 나도 당황하지 않습니다. 그런데 도윤과 함께 cafe-menu 저장소를 쓰기 시작하자 새로운 고민이 생겼습니다.
"제가 고친 메뉴판을 그냥 main에 push하면 되나요? 혹시 틀린 가격이 그대로 사이트에 올라가면 어떡하죠?"
좋은 질문입니다. 혼자 쓸 때는 main에 바로 올려도 괜찮았지만, 여러 사람이 함께 쓰는 main은 모두가 믿고 기대는 공용 공간입니다. 누군가 실수로 망가진 코드를 올리면 모두의 작업이 멈춥니다. 그래서 대부분의 팀은 이렇게 일합니다.
이 요청이 바로 Pull Request(풀 리퀘스트, 줄여서 PR) 입니다. 이번 편에서는 PR을 중심으로 한 팀의 작업 흐름, GitHub Flow를 한 바퀴 돌아봅니다. 브랜치(4편)와 충돌 해결(5편)을 배웠다면 필요한 재료는 이미 다 갖췄습니다. 새로 배울 것은 명령어보다 약속과 순서입니다.
이번 편의 터미널 출력은 모두 git 2.55에서 직접 실행해 얻은 것입니다. GitHub 대신 내 컴퓨터에 만든 가짜 원격 저장소(git init --bare)로 재현했기 때문에, 출력 속 원격 주소만 읽기 쉽게 https://github.com/minji/cafe-menu.git으로 바꿔 적었습니다. 저장소 주인은 민지(minji)이고, 도윤은 쓰기 권한을 받은 협업자라고 가정합니다.
이름 때문에 헷갈리기 쉽습니다. git pull과 이름이 비슷하지만 Pull Request는 git 명령이 아니라 GitHub(와 GitLab 같은 코드 호스팅 서비스)가 제공하는 기능입니다. GitLab에서는 같은 것을 Merge Request라고 부릅니다.
회사의 결재 서류를 떠올리면 쉽습니다.
그러니까 PR은 "합쳐 주세요"라는 요청과, 그 변경을 두고 이야기하는 토론 공간을 하나로 묶은 것입니다. git이 기록을 남기는 도구라면, PR은 기록을 main에 넣기 전에 사람이 한 번 보는 관문입니다.
PR 하나에는 늘 두 개의 브랜치가 등장합니다. 용어부터 정리해 두겠습니다.
| 용어 | 뜻 | 민지의 예 |
|---|---|---|
| base (받는 쪽) | 변경을 받아들일 브랜치 | main |
| compare / head (보내는 쪽) | 변경이 담긴 브랜치 | feature/latte-menu |
| 리뷰어(reviewer) | 변경을 읽고 의견·승인을 주는 사람 | 도윤 |
| 머지(merge) | PR의 변경을 base에 실제로 합치는 일 | 도윤이 버튼을 누름 |
GitHub Flow는 GitHub가 권하는 가장 단순한 팀 작업 방식입니다. 규칙은 딱 하나에서 출발합니다. "main은 언제나 배포할 수 있는 상태로 둔다." 그래서 main에서 직접 작업하지 않고, 모든 변경은 브랜치 → PR → 리뷰를 거쳐서만 main에 들어옵니다.
git switch main → git pull로 main을 최신 상태로git switch -c feature/latte-menugit add · git commitgit push -u origin feature/latte-menu로 브랜치를 GitHub에gh pr creategit pull → 다시 ①부터①~④는 이미 1~4편에서 배운 명령 그대로입니다. 새로 등장하는 건 ⑤~⑧, 대부분 GitHub 웹 화면에서 일어나는 일입니다. 아래 위젯에서 민지의 PR #1이 main에 들어가기까지를 한 단계씩 넘겨 보세요. 각 단계에서 누가, 터미널과 웹 중 어디서, 무엇을 하는지와 그 순간 GitHub에 있는 브랜치 상태가 함께 바뀝니다.
카페 사장님이 가을 메뉴로 라테 두 가지를 추가해 달라고 했습니다. 민지가 이 일을 맡았습니다.
작업을 시작하기 전에는 늘 main을 최신 상태로 맞춥니다. 도윤이 어제 올린 변경 위에서 시작해야 나중에 충돌이 적습니다.
git switch main
git pull
Already up to date.
git switch -c feature/latte-menu
Switched to a new branch 'feature/latte-menu'
브랜치 이름은 보고 바로 무슨 일인지 알 수 있게 짓습니다. 많은 팀이 feature/(새 기능), fix/(버그 수정), docs/(문서) 같은 머리말을 붙입니다. 규칙 자체보다 팀이 한 가지로 통일하는 것이 중요합니다. (4편의 브랜치 이름 규칙을 참고하세요.)
git add menu.md
git commit -m "메뉴에 카페라테 추가"
[feature/latte-menu a66a068] 메뉴에 카페라테 추가
1 file changed, 1 insertion(+)
git commit -am "메뉴에 바닐라라테 추가"
[feature/latte-menu b8dd6e4] 메뉴에 바닐라라테 추가
1 file changed, 1 insertion(+)
-am은 "이미 git이 추적하고 있는 파일의 변경을 add까지 한 번에 하고 커밋"하는 줄임입니다. 새로 만든 파일에는 통하지 않으니 그때는 git add를 먼저 하세요.
git push -u origin feature/latte-menu
To https://github.com/minji/cafe-menu.git
* [new branch] feature/latte-menu -> feature/latte-menu
branch 'feature/latte-menu' set up to track 'origin/feature/latte-menu'.
* [new branch]는 GitHub에 없던 브랜치가 새로 생겼다는 뜻입니다. -u(--set-upstream)는 "이 로컬 브랜치의 짝은 origin의 feature/latte-menu"라고 기억시키는 옵션이라, 이 브랜치에서는 앞으로 git push, git pull만 쳐도 됩니다. main은 전혀 건드리지 않았다는 점에 주목하세요.
실제 GitHub로 push하면 출력 중간에 remote:로 시작하는 줄이 몇 개 더 나오고, 그 안에 이 브랜치로 PR을 만들 수 있는 링크가 들어 있습니다. 그 링크를 눌러도 곧바로 다음 단계로 갈 수 있습니다.
브랜치를 push한 직후 GitHub 저장소 페이지에 들어가면, 위쪽에 방금 push한 브랜치 이름과 함께 Compare & pull request 버튼이 떠 있습니다. 버튼이 안 보이면 Pull requests 탭 → New pull request를 누르고 두 브랜치를 직접 고르면 됩니다.
PR 작성 화면에서 확인할 것은 다섯 가지입니다.
화면 아래에는 이 PR에 들어갈 커밋 목록과 바뀐 줄(diff)이 미리 보입니다. 여기서 한 번 스스로 훑어보는 것만으로 실수의 절반은 잡힙니다. 디버깅용으로 넣었던 줄이나, 올리면 안 되는 파일이 섞여 있지 않은지 확인하세요.
Draft(초안) PR은 "아직 완성은 아니지만 방향이 맞는지 봐 주세요"라는 표시입니다. 초안 상태에서는 머지할 수 없고, 준비가 되면 PR 페이지의 Ready for review 버튼으로 정식 PR로 바꿉니다. 큰 작업을 몰래 오래 하다가 한꺼번에 내미는 것보다, 초안으로 일찍 보여 주는 편이 서로의 시간을 아낍니다.
3편에서 인증에 썼던 GitHub 공식 명령줄 도구 gh로는 PR도 터미널에서 열 수 있습니다. (아래 옵션은 gh 2.89의 gh pr create --help로 확인했습니다.)
gh pr create --base main --title "메뉴에 라테 2종 추가" --body "가을 메뉴로 카페라테·바닐라라테를 추가합니다." --reviewer doyoon
| 옵션 | 뜻 |
|---|---|
--base main | 합칠 대상 브랜치. 생략하면 저장소의 기본 브랜치(보통 main) |
--title · --body | PR 제목과 설명. 생략하면 대화형으로 물어봅니다 |
--body-file pr.md | 설명이 길면 파일에 써 두고 읽어 오기 |
--fill | 커밋 메시지로 제목·설명을 자동으로 채우기 |
--reviewer doyoon | 리뷰어 지정 |
--draft | 초안 PR로 만들기 |
--web | 터미널 대신 브라우저의 PR 작성 화면을 열기 |
성공하면 새 PR의 주소가 출력됩니다. 브랜치를 아직 push하지 않았다면 gh가 어디에 push할지 먼저 물어봅니다. 처음에는 웹 화면으로 몇 번 열어 보면서 각 칸의 의미를 익히고, 익숙해지면 gh로 옮겨 가는 것을 권합니다. PR 목록은 gh pr list, 지금 브랜치의 PR 상태는 gh pr view, 브라우저로 열기는 gh pr view --web입니다.
리뷰어는 여러분의 머릿속을 볼 수 없습니다. diff는 무엇이 바뀌었는지만 보여 줄 뿐, 왜 바꿨는지와 어떻게 확인하면 되는지는 알려 주지 않습니다. 설명에 이 빈칸을 채워 주면 리뷰가 빠르고 정확해집니다.
| 항목 | 좋은 예 | 아쉬운 예 |
|---|---|---|
| 무엇 | menu.md에 카페라테(4,500원)·바닐라라테(5,500원) 추가 | 메뉴 수정 |
| 왜 | 가을 메뉴 개편 회의에서 라테 2종을 먼저 올리기로 함 | (없음) |
| 확인 방법 | menu.md를 열어 아메리카노 아래 두 줄 확인, 가격이 매장 메뉴판과 같은지 | 잘 됩니다 |
| 스크린샷 | 화면이 바뀌면 전·후 이미지 (설명 칸에 이미지를 끌어다 놓으면 올라감) | "직접 실행해 보세요" |
설명에 Closes #3처럼 이슈 번호를 적어 두면, PR이 머지될 때 그 이슈가 자동으로 닫힙니다. (Fixes #3도 같은 효과입니다.) "할 일 목록(이슈) → 작업(PR) → 완료"가 저절로 이어집니다.
아래 도우미에서 칸을 채워 보세요. 네 가지가 다 들어갔는지 점검해 주고, 복사해서 바로 쓸 수 있는 마크다운 본문과 gh 명령을 만들어 줍니다. "민지의 PR #1 예시로 채우기"를 누르면 완성본을 볼 수 있습니다.
팀에서 매번 같은 틀을 쓰고 싶다면, 저장소에 .github/pull_request_template.md 파일을 만들어 두세요. 새 PR을 열 때마다 설명 칸에 그 내용이 미리 채워집니다.
리뷰 요청 알림을 받은 도윤은 PR 페이지에 들어갑니다. PR 페이지에는 탭이 몇 개 있습니다.
| 탭 | 무엇을 보나 |
|---|---|
| Conversation | 설명, 댓글, 리뷰 기록, 머지 버튼이 있는 메인 화면 |
| Commits | 이 PR에 포함된 커밋 목록 |
| Checks | 자동 검사(CI) 결과 |
| Files changed | 바뀐 줄 전체. 리뷰는 대부분 여기서 합니다 |
Files changed 탭에서 줄 번호 옆에 마우스를 올리면 + 버튼이 나타납니다. 누르면 그 줄에 코멘트를 달 수 있습니다. 도윤은 바닐라라테 줄에 이렇게 적었습니다.
"매장 메뉴판에는 바닐라라테가 5,500원이에요. 한 번 확인 부탁드려요!"
코멘트 입력창에는 제안(suggestion) 버튼도 있습니다. 고쳐 줄 문구를 직접 적어 두면, 작성자가 PR 화면에서 버튼 한 번으로 그 제안을 커밋으로 받아들일 수 있습니다. 오타처럼 작은 수정에 편리합니다.
코멘트를 다 달았으면 오른쪽 위의 리뷰 제출 버튼(Review changes 또는 Submit review, 화면 버전에 따라 이름이 조금 다릅니다)을 누르고 세 가지 중 하나를 고릅니다.
| 선택 | 뜻 | gh 명령 |
|---|---|---|
| Comment | 의견만 남김. 승인도 거절도 아님 | gh pr review --comment -b "..." |
| Approve | "이대로 합쳐도 좋다" 승인 | gh pr review --approve |
| Request changes | "고쳐야 합칠 수 있다" 수정 요청 | gh pr review --request-changes -b "..." |
도윤은 가격이 틀렸으니 Request changes를 골랐습니다. 코멘트를 하나씩 따로 보내지 않고 리뷰 한 번에 모아서 제출하는 이유는, 작성자가 알림 폭탄을 받지 않고 한꺼번에 읽고 고칠 수 있게 하기 위해서입니다.
웹 화면만으로 부족하면, 리뷰어는 PR 브랜치를 자기 컴퓨터로 가져와 직접 실행해 볼 수 있습니다. 브랜치는 GitHub에 올라가 있으니 3편의 fetch와 4편의 switch면 충분합니다.
git fetch
git switch feature/latte-menu
From https://github.com/minji/cafe-menu
* [new branch] feature/latte-menu -> origin/feature/latte-menu
Switched to a new branch 'feature/latte-menu'
branch 'feature/latte-menu' set up to track 'origin/feature/latte-menu'.
도윤의 컴퓨터에는 feature/latte-menu 브랜치가 없었지만, 같은 이름의 원격 브랜치(origin/feature/latte-menu)가 있으니 git이 알아서 추적 브랜치를 만들어 주었습니다. 이제 PR과 똑같은 내용을 터미널에서도 볼 수 있습니다.
git log --oneline main..feature/latte-menu
git diff main...feature/latte-menu
b8dd6e4 메뉴에 바닐라라테 추가
a66a068 메뉴에 카페라테 추가
diff --git a/menu.md b/menu.md
index 3c3560c..79b3b5d 100644
--- a/menu.md
+++ b/menu.md
@@ -1,3 +1,5 @@
# 메뉴
- 아메리카노 4,000원
+- 카페라테 4,500원
+- 바닐라라테 5,000원
main..feature/latte-menu(점 두 개)는 "main에는 없고 feature에만 있는 커밋", main...feature/latte-menu(점 세 개)는 "브랜치가 갈라진 지점부터 feature에서 바뀐 내용"입니다. 점 세 개 쪽이 GitHub의 Files changed 탭과 같은 기준이라고 기억하면 됩니다. gh가 있다면 gh pr checkout 1 한 줄로 PR 번호만 가지고 브랜치를 가져올 수도 있습니다.
수정 요청을 받았다고 PR을 닫고 새로 열 필요는 전혀 없습니다. 같은 브랜치에서 고치고 커밋해서 push하면, 열려 있는 PR에 새 커밋이 자동으로 따라 붙습니다. PR은 "특정 커밋 묶음"이 아니라 "이 브랜치"를 가리키기 때문입니다.
git commit -am "리뷰 반영: 바닐라라테 가격 5,500원으로 수정"
git push
[feature/latte-menu 77d55d4] 리뷰 반영: 바닐라라테 가격 5,500원으로 수정
1 file changed, 1 insertion(+), 1 deletion(-)
To https://github.com/minji/cafe-menu.git
b8dd6e4..77d55d4 feature/latte-menu -> feature/latte-menu
b8dd6e4..77d55d4는 GitHub의 feature 브랜치가 b8dd6e4에서 77d55d4로 한 칸 앞으로 갔다는 뜻입니다. 이미 -u로 짝을 지어 두었으니 그냥 git push면 됩니다.
push한 뒤에는 도윤의 코멘트에 "5,500원으로 고쳤습니다" 하고 답을 달고, 해결된 대화는 Resolve conversation 버튼으로 접어 둡니다. 도윤은 새 커밋만 다시 보고 Approve합니다.
민지가 리뷰를 기다리는 사이, 도윤이 다른 PR로 README에 영업시간을 추가해 main에 합쳤습니다. 이제 main은 민지의 브랜치가 출발했던 지점보다 한 칸 앞서 있습니다. 민지 쪽에서 확인해 봅시다.
git fetch
git log --oneline --graph --all
From https://github.com/minji/cafe-menu
19e2158..7fe3406 main -> origin/main
* 77d55d4 리뷰 반영: 바닐라라테 가격 5,500원으로 수정
* b8dd6e4 메뉴에 바닐라라테 추가
* a66a068 메뉴에 카페라테 추가
| * 7fe3406 README에 영업시간 추가
|/
* 19e2158 카페 웹사이트 첫 페이지
두 갈래로 갈라졌습니다. 서로 다른 파일을 고쳤으니 이대로 머지해도 문제는 없습니다. 하지만 main의 변경이 내 작업과 부딪히는지 머지 전에 내 브랜치에서 먼저 확인하고 싶을 때, 또는 저장소 설정이 "최신 main을 반영한 브랜치만 머지 가능"으로 되어 있을 때는 브랜치를 최신으로 맞춰야 합니다.
가장 쉽고 안전한 방법은 최신 main을 내 브랜치에 merge하는 것입니다.
git fetch
git merge origin/main
Merge made by the 'ort' strategy.
README.md | 2 ++
1 file changed, 2 insertions(+)
git log --oneline --graph
* 6462d1c Merge remote-tracking branch 'origin/main' into feature/latte-menu
|\
| * 7fe3406 README에 영업시간 추가
* | 77d55d4 리뷰 반영: 바닐라라테 가격 5,500원으로 수정
* | b8dd6e4 메뉴에 바닐라라테 추가
* | a66a068 메뉴에 카페라테 추가
|/
* 19e2158 카페 웹사이트 첫 페이지
git merge를 옵션 없이 실행하면 머지 커밋 메시지를 쓰는 편집기가 열릴 수 있습니다. 기본 메시지 그대로 저장하고 닫으면 됩니다. 이제 git push로 올리면 PR에도 반영됩니다. 여기서 충돌이 나면 5편에서 배운 순서(충돌 마커 확인 → 고치기 → git add → git commit)대로 해결하면 됩니다. 충돌을 main이 아니라 내 브랜치에서 푸는 것이 핵심입니다. main은 계속 깨끗하게 유지됩니다.
GitHub의 PR 페이지에 Update branch 버튼이 보이면, 같은 일을 웹에서 버튼 한 번으로 할 수도 있습니다. 그 뒤에는 로컬에서 git pull로 받아오세요.
같은 목적에 rebase(리베이스) 를 쓰는 팀도 있습니다. 이름이 말해 주듯 "base(출발점)를 다시 잡는다"는 뜻입니다. merge가 두 갈래를 머지 커밋으로 묶는 방식이라면, rebase는 내 커밋들을 최신 main 끝으로 떼어다 다시 붙이는 방식입니다.
마지막 줄이 중요합니다. rebase는 커밋을 새로 만들어 바꿔치기합니다. 도윤이 민지의 옛 커밋을 받아서 그 위에 작업하고 있었다면, 민지가 rebase 후 강제로 push하는 순간 두 사람의 역사가 어긋납니다. 그래서 규칙은 하나만 기억하면 됩니다.
git merge origin/main만 써도 전혀 부족하지 않습니다. rebase의 실제 사용법(pull --rebase, 충돌 처리, rebase -i로 커밋 정리)은 8편 한 걸음 더에서 따라 해 봅니다.
도윤이 승인하자 PR 화면 아래의 초록색 머지 버튼이 활성화됐습니다. 버튼 옆의 작은 화살표를 누르면 세 가지 방식이 나옵니다.
셋 다 "PR의 변경을 main에 넣는다"는 결과는 같습니다. 다른 것은 main의 역사에 어떤 모양으로 남느냐입니다. 말로만 보면 헷갈리니, 민지의 PR(커밋 3개, 그사이 main에는 README 커밋 1개)을 로컬에서 세 가지 방식으로 각각 합쳐 보겠습니다. 비교를 깔끔하게 하려고 6장의 main 최신화 머지는 하지 않은 상태에서 시작합니다. 로컬 브랜치 이름은 짧게 feature로 붙였습니다.
GitHub의 이 버튼은 로컬의 git merge --no-ff와 같은 모양을 만듭니다. --no-ff는 4편에서 본 fast-forward가 가능한 상황에서도 반드시 머지 커밋을 만들라는 옵션입니다.
git switch main
git merge --no-ff feature -m "Merge pull request #1 from minji/feature/latte-menu"
git log --oneline --graph
Merge made by the 'ort' strategy.
menu.md | 2 ++
1 file changed, 2 insertions(+)
* 8e9093a Merge pull request #1 from minji/feature/latte-menu
|\
| * 77d55d4 리뷰 반영: 바닐라라테 가격 5,500원으로 수정
| * b8dd6e4 메뉴에 바닐라라테 추가
| * a66a068 메뉴에 카페라테 추가
* | 7fe3406 README에 영업시간 추가
|/
* 19e2158 카페 웹사이트 첫 페이지
민지의 커밋 세 개가 원래 해시 그대로 main 역사에 들어오고, 그 위에 "PR #1을 합쳤다"는 머지 커밋이 생겼습니다. 메시지는 GitHub가 실제로 쓰는 형식을 흉내 냈습니다.
"squash"는 "찌그러뜨리다"라는 뜻입니다. 로컬에서는 git merge --squash가 비슷한 일을 합니다. 이 명령은 변경 내용만 스테이징에 올려 두고 커밋은 하지 않고 멈춥니다.
git switch main
git merge --squash feature
Automatic merge went well; stopped before committing as requested
Squash commit -- not updating HEAD
git status
On branch main
Your branch is up to date with 'origin/main'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: menu.md
세 커밋의 변경이 합쳐져 menu.md 하나의 "커밋할 준비된 변경"이 됐습니다. 이제 한 번에 커밋합니다.
git commit -m "메뉴에 라테 2종 추가 (#1)"
git log --oneline --graph
[main 12f2878] 메뉴에 라테 2종 추가 (#1)
1 file changed, 2 insertions(+)
* 12f2878 메뉴에 라테 2종 추가 (#1)
* 7fe3406 README에 영업시간 추가
* 19e2158 카페 웹사이트 첫 페이지
main에는 새 커밋 딱 하나만 남았습니다. "리뷰 반영" 같은 자잘한 커밋은 main에서 사라지고(PR 페이지에는 계속 남습니다), PR 하나 = 커밋 하나라는 깔끔한 역사가 됩니다. GitHub의 Squash and merge는 보통 PR 제목과 번호를 커밋 제목으로 제안하며, 저장소 설정에 따라 기본 문구는 달라질 수 있습니다.
그런데 이 방식에는 입문자가 자주 놀라는 부작용이 하나 있습니다.
git branch -d feature
error: the branch 'feature' is not fully merged
hint: If you are sure you want to delete it, run 'git branch -D feature'
hint: Disable this message with "git config set advice.forceDeleteBranch false"
분명 합쳤는데 "다 합쳐지지 않았다"고 합니다. git은 커밋 해시로 합쳐졌는지 판단하는데, squash는 내용만 옮겨 담은 새 커밋을 만들었기 때문에 a66a068·b8dd6e4·77d55d4는 main 어디에도 없습니다. PR이 정말 머지된 것을 확인했다면 대문자 -D로 지워도 됩니다. 같은 이유로, squash 머지된 브랜치에서 작업을 계속 이어 가면 다음 머지 때 같은 충돌을 또 풀어야 할 수 있습니다. GitHub 문서도 squash는 짧게 쓰고 버리는 브랜치에 가장 잘 맞는다고 설명합니다. 머지된 브랜치는 지우고, 다음 일은 새 브랜치에서 시작하세요.
로컬에서는 브랜치를 main 위로 rebase한 뒤 fast-forward로 합치는 것과 비슷합니다.
git switch feature
git rebase main
Switched to branch 'feature'
Successfully rebased and updated refs/heads/feature.
git switch main
git merge --ff-only feature
git log --oneline --graph
Switched to branch 'main'
Your branch is up to date with 'origin/main'.
Updating 7fe3406..fbc94a9
Fast-forward
menu.md | 2 ++
1 file changed, 2 insertions(+)
* fbc94a9 리뷰 반영: 바닐라라테 가격 5,500원으로 수정
* cbf69d6 메뉴에 바닐라라테 추가
* a9ebb3d 메뉴에 카페라테 추가
* 7fe3406 README에 영업시간 추가
* 19e2158 카페 웹사이트 첫 페이지
커밋 세 개가 머지 커밋 없이 한 줄로 붙었습니다. 그런데 해시를 보세요. a66a068이 a9ebb3d로, 77d55d4가 fbc94a9로 바뀌었습니다. 내용과 메시지는 같지만 새로 만들어진 커밋입니다. 커밋 정보를 확인하면 흥미로운 점이 하나 더 보입니다.
git log --format='%h %an / %cn %s' -3
fbc94a9 민지 / 도윤 리뷰 반영: 바닐라라테 가격 5,500원으로 수정
cbf69d6 민지 / 도윤 메뉴에 바닐라라테 추가
a9ebb3d 민지 / 도윤 메뉴에 카페라테 추가
%an은 작성자(author), %cn은 커밋을 실제로 만든 사람(committer)입니다. 내용을 쓴 사람은 민지 그대로지만, 다시 붙이는 작업을 한 도윤이 커미터로 찍혔습니다. GitHub 문서에 따르면 Rebase and merge 버튼도 항상 커미터 정보를 갱신하고 새 커밋 해시를 만듭니다. 그래서 이 방식으로 머지한 뒤에도 민지의 로컬 브랜치(옛 해시)는 git branch -d로 지워지지 않습니다.
아래 위젯에서 버튼을 바꿔 가며 main의 모양이 어떻게 달라지는지 보세요. 그림의 해시는 위에서 직접 재현한 값입니다.
| 항목 | Create a merge commit | Squash and merge | Rebase and merge |
|---|---|---|---|
| main에 들어가는 커밋 | 원래 커밋 전부 + 머지 커밋 1개 | 새 커밋 1개 | 원래 커밋 수만큼 (새 해시) |
| 원래 커밋 해시 | 유지 | main에 없음 | 바뀜 |
| 역사 모양 | 갈라졌다 합쳐짐 | 한 줄, PR당 1커밋 | 한 줄, 커밋별로 |
| PR 단위로 되돌리기(revert) | 머지 커밋 하나를 revert (-m 1 필요) | 커밋 하나만 revert | 커밋 여러 개를 각각 revert |
로컬 브랜치 git branch -d | 바로 됨 | 거절 → -D | 거절 → -D |
| 잘 맞는 경우 | 작업 과정을 그대로 남기고 싶을 때 | 커밋이 자잘한 짧은 브랜치 (입문 팀에 무난) | 커밋이 이미 잘 정리돼 있을 때 |
| gh 명령 | gh pr merge --merge | gh pr merge --squash | gh pr merge --rebase |
어느 쪽이 정답이라기보다 팀이 하나로 정하는 것이 중요합니다. 섞어 쓰면 main의 역사가 뒤죽박죽이 됩니다. 저장소 주인은 Settings → General의 Pull Requests 항목에서 세 버튼 중 허용할 방식만 켜 둘 수 있습니다. 결정하기 어렵다면, 자잘한 커밋이 많은 입문 팀에는 Squash and merge 하나만 켜 두는 것이 가장 무난한 출발점입니다.
도윤은 Create a merge commit으로 PR #1을 머지했습니다. 머지가 끝나면 PR 화면에 Delete branch 버튼이 나타납니다. 누르면 GitHub에 있던 feature/latte-menu 브랜치가 지워집니다. 커밋은 이미 main 역사 안에 들어가 있으니 사라지지 않고, 혹시 필요하면 같은 자리의 Restore branch 버튼으로 되살릴 수도 있습니다. 매번 누르기 귀찮다면 저장소 설정의 Automatically delete head branches를 켜 두면 머지 때 자동으로 지워집니다. gh로 머지한다면 gh pr merge --merge --delete-branch처럼 --delete-branch를 붙이면 로컬과 원격 브랜치를 함께 지워 줍니다.
GitHub에서 일이 끝났다고 내 컴퓨터가 저절로 바뀌지는 않습니다. 민지는 main으로 돌아가 최신 상태를 받아옵니다.
git switch main
git pull
Switched to branch 'main'
Your branch is behind 'origin/main' by 1 commit, and can be fast-forwarded.
(use "git pull" to update your local branch)
From https://github.com/minji/cafe-menu
7fe3406..4da4283 main -> origin/main
Updating 19e2158..4da4283
Fast-forward
README.md | 2 ++
menu.md | 2 ++
2 files changed, 4 insertions(+)
이제 다 쓴 로컬 브랜치를 지우고, GitHub에서 이미 지워진 브랜치의 흔적(원격 추적 브랜치)도 정리합니다.
git branch -d feature/latte-menu
git fetch --prune
git branch -a
Deleted branch feature/latte-menu (was 6462d1c).
From https://github.com/minji/cafe-menu
- [deleted] (none) -> origin/feature/latte-menu
* main
remotes/origin/HEAD -> origin/main
remotes/origin/main
머지 커밋 방식으로 합쳤기 때문에 git branch -d가 한 번에 통과했습니다. --prune은 "원격에서 사라진 브랜치의 흔적을 내 쪽에서도 지워라"라는 뜻입니다. 이것까지 하면 GitHub Flow 한 바퀴가 끝납니다. 도윤도 자기 컴퓨터에서 git switch main, git pull을 하면 두 사람의 main이 같아집니다. 다음 일은 다시 ①부터입니다.
git log --oneline --graph -8
* 4da4283 Merge pull request #1 from minji/feature/latte-menu
|\
| * 6462d1c Merge remote-tracking branch 'origin/main' into feature/latte-menu
| |\
| |/
|/|
* | 7fe3406 README에 영업시간 추가
| * 77d55d4 리뷰 반영: 바닐라라테 가격 5,500원으로 수정
| * b8dd6e4 메뉴에 바닐라라테 추가
| * a66a068 메뉴에 카페라테 추가
|/
* 19e2158 카페 웹사이트 첫 페이지
6장에서 main을 브랜치에 merge해 최신화한 커밋(6462d1c)까지 모두 남아 있습니다. 머지 커밋 방식은 이렇게 일어난 일을 전부 기록합니다. 같은 PR을 squash로 합쳤다면 이 모든 것이 커밋 하나로 보였을 겁니다.
"모든 변경은 PR로"는 좋은 약속이지만, 약속만으로는 실수를 다 막지 못합니다. 누군가 깜빡하고 main에 바로 push할 수도 있으니까요. 그래서 GitHub는 브랜치 보호(branch protection) 기능으로 이 약속을 강제할 수 있게 해 줍니다. 저장소의 Settings → Branches에서 main에 규칙을 걸고, 자주 쓰는 규칙은 다음과 같습니다.
| 규칙 | 무엇을 막나 |
|---|---|
| PR을 거쳐야만 머지 (Require a pull request before merging) | main에 직접 push. 모든 변경이 PR을 통해서만 들어옵니다 |
| 승인 필수 (required approvals) | 리뷰 없이 머지. "최소 1명 승인" 같은 조건을 걸 수 있습니다 |
| 새 커밋이 오면 기존 승인 무효 (dismiss stale approvals) | 승인받은 뒤 몰래 다른 내용을 추가해 머지 |
| 자동 검사 통과 필수 (Require status checks) | 테스트·빌드가 실패한 코드의 머지 |
| 대화 해결 필수 (conversation resolution) | 답하지 않은 리뷰 코멘트가 남은 채 머지 |
| 강제 push·삭제 금지 | main의 역사를 덮어쓰거나 main 자체를 지우는 사고 |
보호 규칙이 걸린 main에 직접 push하면 GitHub가 push를 거절합니다. 에러 메시지가 무섭게 보여도, 규칙이 제대로 일하고 있다는 뜻입니다. 당황하지 말고 브랜치를 만들어 PR로 올리면 됩니다. 최근 GitHub에는 비슷한 규칙을 더 유연하게 묶어 거는 룰셋(Rulesets) 기능도 있습니다. 화면 이름과 쓸 수 있는 규칙은 요금제나 저장소 공개 여부에 따라 조금씩 다를 수 있으니, 실제 Settings 화면에서 확인하세요.
CI(지속적 통합, Continuous Integration) 는 PR이 열리거나 새 커밋이 올라올 때마다 테스트·빌드·맞춤법 검사 같은 작업을 자동으로 돌리는 것입니다. GitHub에서는 보통 GitHub Actions라는 기능으로 설정합니다. 결과는 PR 화면 아래에 초록 체크(통과)나 빨간 X(실패)로 표시되고, 터미널에서는 gh pr checks로 볼 수 있습니다.
사람은 "이 변경이 맞는 방향인가"를, 기계는 "빌드가 깨지지 않았나"를 봅니다. 보호 규칙의 자동 검사 통과 필수와 함께 쓰면 빨간 X가 있는 PR은 아예 머지할 수 없게 됩니다. 이번 편에서는 CI가 있다는 것만 알아 두면 충분합니다.
지금까지는 도윤이 민지의 저장소에 쓰기 권한이 있었습니다. 그런데 내가 쓰기 권한이 없는 저장소, 예를 들어 즐겨 쓰는 오픈소스 프로젝트의 문서에서 오타를 발견했다면 어떻게 할까요? 브랜치를 push할 권한이 없으니 앞의 흐름을 그대로 쓸 수 없습니다.
이때 쓰는 것이 fork(포크) 입니다. 남의 저장소를 내 계정으로 통째로 복사해 오는 GitHub 기능입니다. 내 복사본에는 마음껏 push할 수 있으니, 거기서 작업한 브랜치로 원래 저장소에 PR을 보냅니다.
upstream이라는 이름의 원격으로 추가fork로 작업할 때는 금고가 두 개입니다. 3편의 원격 관리 명령으로 주소록에 둘 다 등록해 두는 것이 핵심입니다.
| 별명 | 가리키는 곳 | 내 권한 | 주로 하는 일 |
|---|---|---|---|
origin | 내 계정의 복사본 (fork) | 읽기·쓰기 | 내 브랜치를 push |
upstream | 원래 저장소 | 읽기만 | 최신 내용을 fetch, PR의 base |
예를 들어 민지가 cafe-tools/menu-kit이라는 오픈소스를 fork했다고 합시다. 웹에서 Fork 버튼을 누르면 minji/menu-kit이 생깁니다. 이 내 복사본을 clone하면 origin은 자동으로 등록됩니다.
git clone https://github.com/minji/menu-kit.git
cd menu-kit
git remote -v
origin https://github.com/minji/menu-kit.git (fetch)
origin https://github.com/minji/menu-kit.git (push)
원래 저장소는 아직 주소록에 없습니다. upstream이라는 별명으로 직접 추가합니다. (이름은 무엇이든 되지만, upstream이 사실상 모두가 쓰는 관례입니다.)
git remote add upstream https://github.com/cafe-tools/menu-kit.git
git remote -v
origin https://github.com/minji/menu-kit.git (fetch)
origin https://github.com/minji/menu-kit.git (push)
upstream https://github.com/cafe-tools/menu-kit.git (fetch)
upstream https://github.com/cafe-tools/menu-kit.git (push)
네 줄이 보이면 준비 끝입니다. 이제 원래 저장소에 새 커밋이 올라오면, 내 복사본을 이렇게 따라잡습니다.
git switch main
git fetch upstream
git merge upstream/main
git push origin main
$ git fetch upstream
From https://github.com/cafe-tools/menu-kit
* [new branch] main -> upstream/main
$ git merge upstream/main
Updating 871af1e..6195e0b
Fast-forward
README.md | 1 +
1 file changed, 1 insertion(+)
$ git push origin main
To https://github.com/minji/menu-kit.git
871af1e..6195e0b main -> main
원격 이름만 다를 뿐, 3편의 fetch·merge와 똑같습니다. 받아 오는 곳은 upstream, 올리는 곳은 origin이라는 것만 기억하세요. 새 작업은 이렇게 최신으로 맞춘 main에서 브랜치를 따서 시작하면 PR에서 충돌이 적습니다.
git status는 upstream을 보지 않습니다. 위에서 git fetch upstream 직후에도 git status는 Your branch is up to date with 'origin/main'.이라고 말했습니다. 내 main의 짝꿍은 origin/main이기 때문입니다. 원래 저장소가 앞서 나갔는지는 git log --oneline main..upstream/main으로 확인하세요(아무것도 안 나오면 따라잡은 상태).
gh를 쓰면 fork·clone·upstream 등록을 한 번에 할 수 있습니다. gh repo fork는 fork를 만든 뒤 내 복사본을 origin으로, 원래 저장소를 upstream으로 설정해 줍니다. 실행한 뒤 git remote -v로 네 줄이 보이는지 확인하세요.
gh repo fork 원래주인/저장소이름 --clone
GitHub 웹의 내 fork 페이지에 있는 Sync fork 버튼도 같은 일(원래 저장소의 main을 내 fork로 가져오기)을 해 줍니다. 버튼을 쓴 뒤에는 내 컴퓨터에서 git pull로 받아 오면 됩니다.
오픈소스 PR을 보내기 전에는 저장소의 README나 CONTRIBUTING.md를 먼저 읽으세요. 큰 변경이라면 PR부터 보내지 말고 이슈로 먼저 의견을 묻는 것이 예의입니다. 처음에는 오타 수정이나 문서 개선 같은 작은 PR이 좋은 출발점입니다.
도구보다 약속이 협업을 결정합니다. 민지와 도윤이 첫 PR 뒤에 정한 약속을 소개합니다.
| 약속 | 이유 |
|---|---|
| PR은 작게 — 한 PR에 한 가지 일 | 작은 PR은 빨리, 꼼꼼히 리뷰됩니다. 수천 줄짜리 PR은 결국 대충 보고 승인하게 됩니다 |
PR 제목 규칙 — 예: feat: 새 기능, fix: 버그 수정, docs: 문서 | 목록만 봐도 성격이 보이고, squash 머지 시 그대로 main의 커밋 제목이 됩니다 |
| 설명은 무엇·왜·확인 방법 | 리뷰어가 질문하고 답을 기다리는 왕복을 줄입니다 |
| 리뷰는 하루 안에 첫 응답 | PR이 오래 열려 있을수록 main과 멀어지고 충돌이 늘어납니다 |
| 머지 방식은 하나로 | main의 역사가 일관되게 유지됩니다 |
| 머지한 브랜치는 바로 삭제 | 브랜치 목록이 "지금 진행 중인 일"만 보여 주게 됩니다 |
| main에 직접 push 금지 (보호 규칙으로 강제) | 실수가 아니라 구조로 막습니다 |
feat:·fix: 같은 머리말은 Conventional Commits라는 널리 쓰이는 관례에서 왔습니다. 꼭 따라야 하는 표준은 아니니, 팀에 맞게 한국어 머리말([기능], [수정])을 써도 괜찮습니다.
gh pr create)를 직접 해낼 수 있습니다. 명령을 실행하기 전에 허락을 구하도록 설정해 두면, 어떤 명령을 치려는지 하나하나 보고 승인할 수 있습니다. Claude Code 2.1.283에는 PR에 연결된 이전 작업 세션을 다시 여는 --from-pr 옵션도 있습니다.Q1. PR을 열었는데 오타를 발견했어요. PR을 닫고 새로 열어야 하나요?
아니요. 같은 브랜치에서 고쳐서 커밋하고 git push만 하면 열려 있는 PR에 자동으로 반영됩니다. PR은 브랜치를 가리키기 때문입니다. 제목과 설명도 PR 페이지에서 언제든 수정할 수 있습니다.
Q2. 혼자 하는 프로젝트에도 PR이 필요한가요?
필수는 아닙니다. 하지만 혼자서도 브랜치 → PR → 스스로 Files changed 훑어보기 → 머지 순서로 일하면, 변경 이유가 PR 설명으로 남고 CI 결과도 확인할 수 있어 좋습니다. 특히 AI 에이전트에게 코드를 맡긴다면 "에이전트는 PR까지, 머지는 내가" 흐름이 가장 안전한 검토 장치가 됩니다.
Q3. 머지 방식은 무엇을 고르면 되나요?
팀이 하나로 정하는 것이 가장 중요합니다. 자잘한 커밋이 많은 입문 팀이라면 Squash and merge가 무난합니다. main이 "PR 하나 = 커밋 하나"로 깔끔해지고 되돌리기도 쉽습니다. 작업 과정을 그대로 남기고 싶다면 Create a merge commit, 커밋을 잘 정리하는 습관이 있는 팀이라면 Rebase and merge도 좋습니다.
Q4. squash로 머지했더니 git branch -d가 "not fully merged"라며 거절해요. 뭔가 잘못된 건가요?
정상입니다. squash는 원래 커밋이 아닌 새 커밋을 main에 만들기 때문에, git은 원래 커밋들이 합쳐지지 않았다고 판단합니다. PR이 GitHub에서 머지된 것을 확인했다면 git branch -D 브랜치이름으로 지우세요. Rebase and merge도 새 해시를 만들기 때문에 같은 일이 생깁니다.
Q5. 리뷰를 기다리는 동안 다음 일을 시작해도 되나요?
됩니다. git switch main → git pull → git switch -c 새-브랜치로 main에서 새 브랜치를 만들어 시작하세요. 리뷰 코멘트가 오면 git switch로 원래 브랜치에 돌아가 고치면 됩니다. 이때 아직 커밋하지 않은 변경이 있다면 5편의 git stash로 잠시 치워 두세요. 다만 두 번째 작업이 첫 번째 작업의 코드를 꼭 필요로 한다면, 첫 PR이 머지될 때까지 기다리는 편이 간단합니다.
Q6. git pull과 Pull Request는 무슨 관계인가요?
이름만 비슷할 뿐 다른 것입니다. git pull은 원격의 변경을 내 컴퓨터로 받아오는 git 명령이고, Pull Request는 "내 브랜치를 당겨 가서(pull) 합쳐 주세요"라고 요청하는 GitHub 기능입니다. 저장소 주인 입장에서 내 변경을 "끌어오는" 것이라 이런 이름이 붙었습니다.
| 하고 싶은 일 | 명령 / 화면 |
|---|---|
| 작업 시작 전 최신화 | git switch main → git pull |
| 작업 브랜치 만들기 | git switch -c feature/이름 |
| 브랜치 처음 올리기 | git push -u origin feature/이름 |
| PR 열기 (웹) | Compare & pull request → base: main 확인 → Reviewers 지정 → Create |
| PR 열기 (터미널) | gh pr create --base main --title "..." --body "..." |
| 초안 PR | gh pr create --draft / 웹의 Draft pull request |
| 남의 PR 브랜치 가져와 보기 | git fetch → git switch 브랜치이름 (또는 gh pr checkout 번호) |
| PR의 변경 보기 | git diff main...브랜치 / Files changed 탭 |
| 리뷰 제출 | Comment · Approve · Request changes (gh pr review --approve) |
| 리뷰 반영 | 같은 브랜치에서 커밋 → git push |
| 브랜치를 최신 main으로 | git fetch → git merge origin/main → git push |
| 머지 | Create a merge commit · Squash and merge · Rebase and merge (gh pr merge --merge / --squash / --rebase) |
| 머지 후 정리 | Delete branch → git switch main → git pull → git branch -d 브랜치 → git fetch --prune |
| squash·rebase 머지 후 로컬 브랜치 삭제 | git branch -D 브랜치 |
| 권한 없는 저장소에 기여 | gh repo fork 주인/저장소 --clone → 브랜치 → push → PR |
| fork에 원래 저장소 연결 | git remote add upstream <원래 저장소 주소> → git remote -v로 네 줄 확인 |
| fork를 원래 저장소에 맞추기 | git fetch upstream → git merge upstream/main → git push origin main |
| main 보호 | Settings → Branches: PR 필수 · 승인 필수 · 자동 검사 통과 필수 |
1편부터 6편까지 민지는 add·commit·push·pull로 시작해 브랜치, 충돌, 되돌리기, 그리고 Pull Request까지 왔습니다. 이제 도윤 없이도 팀의 일원으로 일할 수 있습니다.
마지막 7편에서는 민지가 Claude Code에게 일을 맡기는 장면으로 넘어갑니다. 에이전트가 작업 전에 git status와 git diff로 상황을 파악하는 방식, 브랜치·커밋·PR을 만드는 과정, 여러 작업을 동시에 돌리는 worktree, 그리고 reset --hard나 push --force 같은 위험한 명령 앞에서 사람이 어디를 지켜봐야 하는지를 1~6편의 개념과 연결해 정리합니다.
RELATED