🔀
Git·GitHub 심화
fork · PR · Issue · Review · 브랜치 보호
5단원 — GitHub 협업
오픈 소스 프로젝트에 기여하거나 팀 프로젝트를 진행할 때 fork·PR(Pull Request)·Issue·Code Review가 협업의 핵심 흐름입니다. 이 단원에서는 GitHub의 UI 중심 기능들을 이해하고, 브랜치 보호 규칙과 CODEOWNERS 설정으로 코드 품질을 지키는 방법을 배웁니다.
forkPRissuereview
소요 시간
⏱ 1~2시간
난이도
📊 초급~중급
선수 조건
🎯 4단원
결과물
GitHub PR/Review 기반 협업 흐름
이 강의에서 배우는 것
- 1fork → 브랜치 → PR 흐름을 설명하고 직접 수행할 수 있다.
- 2Issue를 활용해 작업을 계획하고 PR과 연결할 수 있다.
- 3Code Review에서 댓글을 달고 요청한 변경을 반영할 수 있다.
- 4브랜치 보호 규칙으로 main 브랜치를 직접 push로부터 보호할 수 있다.
- 5CODEOWNERS 파일로 특정 파일/디렉토리의 리뷰어를 자동 지정할 수 있다.
핵심 개념
1) Fork & PR 흐름
text
원본 저장소 (upstream)
↓ fork
내 저장소 (origin)
↓ clone
로컬 작업
↓ git switch -c feature/xxx
↓ 커밋
↓ git push origin feature/xxx
GitHub UI에서 PR 생성
↓ upstream main ← feature/xxx
코드 리뷰 → 승인 → Merge2) Issue 작성 및 PR 연결
Issue 번호(예: #42)를 PR 설명이나 커밋 메시지에 포함하면 자동 연결됩니다.
text
# PR 설명 예시
Closes #42
# 커밋 메시지 예시
fix: resolve login error (#42)`Closes`, `Fixes`, `Resolves` 키워드 사용 시 PR 병합 시 Issue 자동 닫힘.
3) Code Review 단어 모음
| 약어 | 의미 |
|---|---|
| LGTM | Looks Good To Me (승인) |
| nit | 사소한 개선 제안 |
| RFC | Request for Comments |
| WIP | Work In Progress |
| NB | Nota Bene (주목) |
4) 브랜치 보호 규칙
GitHub 저장소 → Settings → Branches → Add rule
text
Branch name pattern: main
✅ Require a pull request before merging
✅ Require approvals: 1
✅ Dismiss stale pull request approvals
✅ Require status checks to pass
✅ Restrict who can push to matching branches5) CODEOWNERS
`.github/CODEOWNERS` 파일:
text
# 전체 저장소 기본 소유자
* @username
# docs 디렉토리는 technical-writers 팀
docs/ @org/technical-writers
# 보안 관련 파일
security.md @security-team예제로 보기
예제 1 — `ex01_fork.sh` : fork 워크플로우 시뮬레이션 (bare 저장소 사용)
bash
#!/usr/bin/env bash
# GitHub fork 는 UI 작업이므로 로컬 bare 저장소로 흐름을 시뮬레이션합니다.
WORKDIR=$(mktemp -d)
git init --bare "$WORKDIR/upstream.git" -q
git init --bare "$WORKDIR/fork.git" -q # 내 fork 역할
# upstream 초기화
git clone "$WORKDIR/upstream.git" "$WORKDIR/upstream_local" -q
cd "$WORKDIR/upstream_local"
git config user.name "메인테이너" && git config user.email "main@example.com"
echo "# 프로젝트" > README.md
git add . && git commit -q -m "init"
git push origin main -q
# fork 로 동기화 (GitHub의 fork = upstream clone push)
git clone "$WORKDIR/upstream.git" "$WORKDIR/my_fork" -q
cd "$WORKDIR/my_fork"
git config user.name "기여자" && git config user.email "contrib@example.com"
git remote add upstream "$WORKDIR/upstream.git"
# 기능 브랜치에서 작업
git switch -c feature/add-docs
echo "## 사용법" >> README.md
git add . && git commit -q -m "docs: add usage section"
echo "=== feature 브랜치 로그 ==="
git log --oneline --graph --all
rm -rf "$WORKDIR"**실행 결과**
text
=== feature 브랜치 로그 ===
* a1b2c3d (HEAD -> feature/add-docs) docs: add usage section
* b2c3d4e (origin/main, main) init핵심: fork 후 기능 브랜치에서 작업하고 PR로 upstream에 기여하는 것이 오픈 소스 흐름이다.
예제 2 — `ex02_pr.sh` : PR 시뮬레이션 (로컬 병합으로 대체)
bash
#!/usr/bin/env bash
WORKDIR=$(mktemp -d)
git init --bare "$WORKDIR/origin.git" -q
git clone "$WORKDIR/origin.git" "$WORKDIR/repo" -q
cd "$WORKDIR/repo"
git config user.name "개발자" && git config user.email "dev@example.com"
echo "메인 기능" > main.py && git add . && git commit -q -m "init"
git push origin main -q
git switch -c feature/login
echo "def login(): pass" >> main.py
git add . && git commit -q -m "feat: add login function"
git push origin feature/login -q
echo "=== PR 시뮬레이션: feature/login → main 병합 ==="
git switch main
git merge --no-ff feature/login -m "merge: PR#1 add login function"
git push origin main -q
echo ""
echo "=== 병합 후 로그 ==="
git log --oneline --graph
rm -rf "$WORKDIR"**실행 결과**
text
=== PR 시뮬레이션: feature/login → main 병합 ===
=== 병합 후 로그 ===
* c3d4e5f merge: PR#1 add login function
|\
| * b2c3d4e feat: add login function
|/
* a1b2c3d init핵심: PR은 브랜치를 직접 push하지 않고 리뷰 후 병합하는 안전 장치다.
예제 3 — `ex03_issue.sh` : Issue 번호를 커밋 메시지에 연결
bash
#!/usr/bin/env bash
REPO=$(mktemp -d)
cd "$REPO"
git init -q
git config user.name "개발자" && git config user.email "dev@example.com"
echo "앱 코드" > app.py && git add . && git commit -q -m "init"
# Issue #7: 로그인 버그 수정
git switch -c fix/issue-7
echo "# 버그 수정됨" >> app.py
git add .
git commit -m "fix: resolve login error
Closes #7"
echo "=== 커밋 메시지 확인 ==="
git log --oneline -n 1
git show HEAD --format="%B" --no-patch
rm -rf "$REPO"**실행 결과**
text
=== 커밋 메시지 확인 ===
a1b2c3d fix: resolve login error
fix: resolve login error
Closes #7핵심: `Closes #N` 을 PR 설명에 쓰면 병합 시 Issue가 자동으로 닫힌다.
예제 4 — `ex04_codeowners.sh` : CODEOWNERS 파일 생성
bash
#!/usr/bin/env bash
REPO=$(mktemp -d)
cd "$REPO"
git init -q
git config user.name "관리자" && git config user.email "admin@example.com"
mkdir -p .github docs src
cat > .github/CODEOWNERS << 'EOF'
# 기본 소유자: 모든 파일
* @admin-user
# 문서 디렉토리
docs/ @tech-writer
# 소스 코드
src/ @backend-team
# CI/CD 설정
.github/workflows/ @devops-team
EOF
echo "앱 코드" > src/app.py
echo "문서" > docs/guide.md
git add .
git commit -m "chore: add CODEOWNERS and project structure"
echo "=== CODEOWNERS 내용 ==="
cat .github/CODEOWNERS
rm -rf "$REPO"**실행 결과**
text
=== CODEOWNERS 내용 ===
# 기본 소유자: 모든 파일
* @admin-user
...핵심: CODEOWNERS는 PR 생성 시 지정된 팀/개인을 리뷰어로 자동 요청한다.
다른 시각으로 보기
| GitHub 기능 | 비유 |
|---|---|
| Issue | 할 일 메모 |
| PR | 상사에게 결재 요청 |
| Code Review | 결재 검토 |
| LGTM / Approve | 결재 도장 |
| Merge | 공식 반영 |
| CODEOWNERS | 부서별 담당자 목록 |
자주 하는 실수
- **PR 없이 main에 직접 push** — 브랜치 보호 규칙이 없으면 팀원 리뷰를 건너뛰게 된다.
- **Issue 없이 PR 생성** — 왜 이 작업이 필요한지 맥락이 없어 리뷰가 어렵다.
- **PR 설명에 `Closes #N` 생략** — Issue가 자동으로 닫히지 않아 관리가 번거로워진다.
- **fork 저장소에서 upstream 동기화 생략** — upstream 변경이 없어 충돌이 커진다.
- **리뷰 댓글을 모두 Resolve 안 하고 Merge** — 해결되지 않은 피드백이 코드에 남는다.
정리
- fork → 기능 브랜치 → PR → 리뷰 → Merge 가 팀 협업의 표준 흐름이다.
- Issue와 PR을 `Closes #N`으로 연결하면 작업 추적이 자동화된다.
- 브랜치 보호 규칙으로 main 직접 push를 막고, 최소 리뷰어 수를 강제한다.
- CODEOWNERS 로 파일/디렉토리별 책임자를 지정해 자동 리뷰 요청을 받는다.
직접 해 보기
bash
cd 05_GitHub_협업/src
chmod +x *.sh
./ex01_fork.sh
./ex02_pr.sh
./ex03_issue.sh
./ex04_codeowners.sh응용:
- 실제 GitHub 저장소를 fork해서 오타 수정 PR을 올려 보세요.
- Issue 템플릿(`.github/ISSUE_TEMPLATE/`)을 만들어 버그 리포트 양식을 만들어 보세요.
💻 예제 (examples)
실제로 실행해 결과를 확인할 수 있는 예제입니다.
ex01_fork.sh— fork 워크플로우 시뮬레이션 (bare 저장소 사용)
CODE
#!/usr/bin/env bash
set -euo pipefail
WORKDIR=$(mktemp -d)
echo "작업 디렉토리: $WORKDIR"
# upstream 역할 bare 저장소
git init --bare -b main "$WORKDIR/upstream.git" -q
# upstream 초기화
git clone "$WORKDIR/upstream.git" "$WORKDIR/upstream_local" -q
cd "$WORKDIR/upstream_local"
git config user.name "메인테이너" && git config user.email "main@example.com"
git config commit.gpgsign false
echo "# 오픈소스 프로젝트" > README.md
echo "기능 A" > feature_a.py
git add . && git commit -q -m "init: initial commit"
git push origin main -q
echo "upstream 초기화 완료"
# 기여자가 fork (= upstream 복제)
git clone "$WORKDIR/upstream.git" "$WORKDIR/my_fork" -q
cd "$WORKDIR/my_fork"
git config user.name "기여자" && git config user.email "contributor@example.com"
git remote add upstream "$WORKDIR/upstream.git"
echo ""
echo "=== fork 저장소의 remote 목록 ==="
git remote -v
# 기능 브랜치 생성 후 작업
git switch -c feature/improve-docs
echo "## 사용법" >> README.md
echo "1. 설치" >> README.md
git add . && git commit -q -m "docs: add usage section (#1)"
echo ""
echo "=== 기여자 브랜치 로그 ==="
git log --oneline --graph --all
echo ""
echo "=== (GitHub UI에서는 여기서 PR을 생성합니다) ==="
echo "git push origin feature/improve-docs"
echo "→ GitHub에서 upstream:main ← origin:feature/improve-docs 로 PR 생성"
rm -rf "$WORKDIR"
▶ 실행 결과
=== feature 브랜치 로그 ===
* a1b2c3d (HEAD -> feature/add-docs) docs: add usage section
* b2c3d4e (origin/main, main) initex02_pr.sh— PR 시뮬레이션 (로컬 병합으로 대체)
CODE
#!/usr/bin/env bash
set -euo pipefail
WORKDIR=$(mktemp -d)
git init --bare -b main "$WORKDIR/origin.git" -q
git clone "$WORKDIR/origin.git" "$WORKDIR/repo" -q
cd "$WORKDIR/repo"
git config user.name "개발자" && git config user.email "dev@example.com"
git config commit.gpgsign false
echo "# 앱" > README.md
echo "def main(): pass" > app.py
git add . && git commit -q -m "init: project setup"
git push origin main -q
# PR 흐름: feature 브랜치 작성 → push → 병합
git switch -c feature/login
cat >> app.py << 'EOF'
def login(user, password):
# 로그인 처리
return user == "admin"
EOF
git add . && git commit -q -m "feat: implement login function
Closes #3"
git push origin feature/login -q
echo "=== PR 대상 브랜치 상태 ==="
git log --oneline --graph --all
# 리뷰 후 main 으로 병합 (--no-ff 로 병합 커밋 생성)
git switch main
git merge --no-ff feature/login -m "merge: PR#1 — feat: implement login"
git push origin main -q
echo ""
echo "=== PR 병합 후 로그 ==="
git log --oneline --graph
# 병합된 브랜치 정리
git branch -d feature/login
git push origin --delete feature/login -q
echo ""
echo "feature/login 브랜치 삭제 완료"
rm -rf "$WORKDIR"
▶ 실행 결과
=== PR 시뮬레이션: feature/login → main 병합 ===
=== 병합 후 로그 ===
* c3d4e5f merge: PR#1 add login function
|\
| * b2c3d4e feat: add login function
|/
* a1b2c3d initex03_issue.sh— Issue 번호를 커밋 메시지에 연결
CODE
#!/usr/bin/env bash
set -euo pipefail
REPO=$(mktemp -d)
cd "$REPO"
git init -q -b main
git config user.name "개발자" && git config user.email "dev@example.com"
git config commit.gpgsign false
echo "def app(): pass" > app.py
git add . && git commit -q -m "init"
echo "=== Issue 연동 커밋 메시지 예시 ==="
echo ""
echo "Issue #7: 로그인 시 빈 비밀번호 허용 버그"
echo ""
# fix/issue-7 브랜치에서 버그 수정
git switch -c fix/issue-7
cat > app.py << 'EOF'
def login(user, password):
if not password:
raise ValueError("Password cannot be empty")
return True
EOF
git add .
git commit -m "fix: prevent empty password login
- 빈 비밀번호 입력 시 ValueError 발생
- 관련 테스트 케이스 추가 예정
Closes #7"
echo "=== 커밋 로그 ==="
git log --oneline
echo ""
echo "=== 최신 커밋 전체 메시지 ==="
git log -1 --format="%B"
echo ""
echo "=== GitHub UI에서의 자동 연동 ==="
echo "PR 설명 또는 커밋에 'Closes #7' 이 있으면"
echo "PR 병합 시 Issue #7 이 자동으로 닫힙니다."
rm -rf "$REPO"
▶ 실행 결과
=== 커밋 메시지 확인 ===
a1b2c3d fix: resolve login error
fix: resolve login error
Closes #7ex04_codeowners.sh— CODEOWNERS 파일 생성
CODE
#!/usr/bin/env bash
set -euo pipefail
REPO=$(mktemp -d)
cd "$REPO"
git init -q -b main
git config user.name "관리자" && git config user.email "admin@example.com"
git config commit.gpgsign false
mkdir -p .github/workflows docs src/backend src/frontend
cat > .github/CODEOWNERS << 'EOF'
# CODEOWNERS — PR 생성 시 자동으로 리뷰어를 지정합니다.
# 기본: 모든 파일
* @admin-user
# 문서
docs/ @tech-writer @admin-user
# 백엔드 소스
src/backend/ @backend-team
# 프론트엔드 소스
src/frontend/ @frontend-team
# CI/CD 워크플로우 — 보안상 DevOps 팀만
.github/workflows/ @devops-team
EOF
echo "백엔드" > src/backend/server.py
echo "프론트엔드" > src/frontend/index.html
echo "문서" > docs/guide.md
echo "Hello" > .github/workflows/ci.yml
git add .
git commit -m "chore: add CODEOWNERS and project skeleton"
echo "=== CODEOWNERS 파일 ==="
cat .github/CODEOWNERS
echo ""
echo "=== 저장소 파일 구조 ==="
git ls-files
rm -rf "$REPO"
▶ 실행 결과
=== CODEOWNERS 내용 ===
# 기본 소유자: 모든 파일
* @admin-user
...📝 과제 (exercises)
직접 풀어보고, 막힐 때 정답을 펼쳐 비교해보세요.
과제 1
문제 1 (hw01.sh)
목표: fork → 기능 브랜치 → PR 병합 전체 흐름을 로컬 bare 저장소로 시뮬레이션하세요.
요구사항
- 파일명: hw01.sh
▶정답 코드 펼치기 / 접기
SOLUTION
#!/usr/bin/env bash
set -euo pipefail
WORKDIR=$(mktemp -d)
git init --bare -b main "$WORKDIR/upstream.git" -q
# upstream 초기화
git clone "$WORKDIR/upstream.git" "$WORKDIR/upstream_local" -q
git -C "$WORKDIR/upstream_local" config user.name "메인테이너"
git -C "$WORKDIR/upstream_local" config user.email "main@example.com"
git -C "$WORKDIR/upstream_local" config commit.gpgsign false
echo "# 프로젝트" > "$WORKDIR/upstream_local/README.md"
git -C "$WORKDIR/upstream_local" add .
git -C "$WORKDIR/upstream_local" commit -q -m "init"
git -C "$WORKDIR/upstream_local" push origin main -q
# fork
git clone "$WORKDIR/upstream.git" "$WORKDIR/my_fork" -q
git -C "$WORKDIR/my_fork" config user.name "기여자"
git -C "$WORKDIR/my_fork" config user.email "contrib@example.com"
git -C "$WORKDIR/my_fork" config commit.gpgsign false
git -C "$WORKDIR/my_fork" remote add upstream "$WORKDIR/upstream.git"
cd "$WORKDIR/my_fork"
git switch -c feature/new-page
echo "# 새 페이지" > new_page.md
git add . && git commit -q -m "docs: add new page"
# upstream에 직접 push (fork 시뮬레이션)
git push upstream feature/new-page -q
# upstream에서 PR 병합
cd "$WORKDIR/upstream_local"
git fetch origin -q
git merge --no-ff origin/feature/new-page -m "merge: PR — add new page"
echo "=== 최종 이력 ==="
git log --oneline --graph --all
rm -rf "$WORKDIR"
과제 2
문제 2 (hw02.sh)
목표: CODEOWNERS 파일과 Issue 연동 커밋 메시지를 포함한 저장소를 만드세요.
요구사항
- 파일명: hw02.sh
입출력 예시
.github/CODEOWNERS
src/api.py
docs/api.md
▶정답 코드 펼치기 / 접기
SOLUTION
#!/usr/bin/env bash
set -euo pipefail
REPO=$(mktemp -d)
cd "$REPO"
git init -q -b main
git config user.name "관리자" && git config user.email "admin@example.com"
git config commit.gpgsign false
mkdir -p .github src docs
cat > .github/CODEOWNERS << 'EOF'
* @admin
src/ @backend
docs/ @writer
EOF
echo "def api(): pass" > src/api.py
echo "# API 문서" > docs/api.md
git add . && git commit -q -m "chore: initial project structure"
# Issue #12 수정
git switch -c fix/issue-12
echo "def api(version=1): pass" > src/api.py
git add .
git commit -m "fix: add version parameter to api
Closes #12"
echo "=== 커밋 메시지 ==="
git log -1 --format="%B"
echo "=== CODEOWNERS ==="
cat .github/CODEOWNERS
rm -rf "$REPO"
▶ 실행 결과
.github/CODEOWNERS
src/api.py
docs/api.md