← Git 강의 목록으로
🔀
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
코드 리뷰 → 승인 → Merge

2) Issue 작성 및 PR 연결

Issue 번호(예: #42)를 PR 설명이나 커밋 메시지에 포함하면 자동 연결됩니다.

text
# PR 설명 예시
Closes #42

# 커밋 메시지 예시
fix: resolve login error (#42)

`Closes`, `Fixes`, `Resolves` 키워드 사용 시 PR 병합 시 Issue 자동 닫힘.

3) Code Review 단어 모음

약어의미
LGTMLooks Good To Me (승인)
nit사소한 개선 제안
RFCRequest for Comments
WIPWork In Progress
NBNota 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 branches

5) 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부서별 담당자 목록

자주 하는 실수

  1. **PR 없이 main에 직접 push** — 브랜치 보호 규칙이 없으면 팀원 리뷰를 건너뛰게 된다.
  2. **Issue 없이 PR 생성** — 왜 이 작업이 필요한지 맥락이 없어 리뷰가 어렵다.
  3. **PR 설명에 `Closes #N` 생략** — Issue가 자동으로 닫히지 않아 관리가 번거로워진다.
  4. **fork 저장소에서 upstream 동기화 생략** — upstream 변경이 없어 충돌이 커진다.
  5. **리뷰 댓글을 모두 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

응용:

  1. 실제 GitHub 저장소를 fork해서 오타 수정 PR을 올려 보세요.
  2. Issue 템플릿(`.github/ISSUE_TEMPLATE/`)을 만들어 버그 리포트 양식을 만들어 보세요.

💻 예제 (examples)

실제로 실행해 결과를 확인할 수 있는 예제입니다.

ex01_fork.shfork 워크플로우 시뮬레이션 (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) init
ex02_pr.shPR 시뮬레이션 (로컬 병합으로 대체)
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 init
ex03_issue.shIssue 번호를 커밋 메시지에 연결
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 #7
ex04_codeowners.shCODEOWNERS 파일 생성
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
     
예제 코드 / 강의 자료

전체 강의 자료와 예제 코드는 GitHub에서 자유롭게 받아볼 수 있습니다.

GitHub에서 보기 ↗