GitHub fork·template·clone 차이, 새 프로젝트에는 무엇을 쓸까
예제 코드를 복사해 새 프로젝트를 시작했는데, 원본 업데이트를 가져오려니 unrelated histories 오류가 뜬다. 처음에 fork 대신 template을 고른 것이 영향을 준 걸까?

- fork는 원본의 이력을 이어받아 수정안을 보내거나 원본 변경을 가져오는 데 맞는다.
- template은 파일 구조를 가져와 새 이력으로 프로젝트를 시작하며 원본의 후속 변경을 자동으로 받지 않는다.
- clone은 저장소를 로컬로 가져오는 작업이므로 fork나 template을 만든 뒤에도 필요할 수 있다.
새 프로젝트와 원본 기여의 출발점
오픈소스의 오류를 고쳐 원래 프로젝트에 제안하려면 fork를 만든다. 예제 웹사이트의 구성만 가져와 자신의 서비스로 발전시키려면 template이 맞는다. 이미 참여 중인 팀 저장소의 코드를 컴퓨터에서 수정하려면 해당 저장소를 clone하면 된다.
GitHub의 템플릿 설명에 따르면 fork는 원본의 커밋 이력을 포함하고, 템플릿에서 만든 저장소는 새 커밋 하나로 시작한다. 커밋은 변경 내용을 기록한 단위다. 파일 내용이 같아 보여도 이전 변경의 연결이 남는지가 다르므로, 나중에 원본과 변경 사항을 합칠 때 작업 방식도 달라진다.
| 방법 | 생기는 결과 | 맞는 작업 |
|---|---|---|
| Fork | 원본과 연결된 내 GitHub 저장소가 생기며 선택한 브랜치의 기존 이력을 이어받는다. | 원본에 PR을 보내거나 원본 개발을 따라가며 수정한다. |
| Use this template | 파일·디렉터리 구조로 새 저장소를 만들고 새 이력으로 시작한다. | 공통 뼈대를 사용하는 별도 프로젝트를 시작한다. |
| git clone | 지정한 원격 저장소의 로컬 복제본이 생긴다. | 컴퓨터에서 코드를 읽고 실행하거나 수정한다. |
예를 들어 템플릿으로 회사 소개 사이트를 만든 뒤에는 새로 생긴 자신의 저장소를 clone해 문구와 디자인을 바꾼다. 오픈소스에 수정안을 보내는 경우에도 자신의 fork를 clone해 작업한다.
템플릿으로 독립 프로젝트 만들기
원본 저장소가 템플릿으로 지정돼 있고 읽기 권한이 있다면 GitHub의 Use this template에서 Create a new repository를 선택한다. 새 저장소를 소유할 계정이나 조직, 이름, 공개 범위를 정하고 생성한다. 원본의 코드 뼈대를 가져오되 새 프로젝트의 이름과 이후 변경 기록은 새 저장소에 쌓인다.
생성 화면에서는 기본 브랜치만 가져오거나 모든 브랜치를 포함할 수 있다. 대부분의 시작용 예제를 쓰는 상황에서는 필요한 기본 브랜치부터 가져오면 된다. 다른 브랜치에 별도의 예제가 있어 함께 가져오더라도, 그 브랜치들의 기존 공통 이력이 복제되는 것은 아니다. GitHub는 템플릿으로 생성된 브랜치들의 이력이 서로 관련되지 않아 일반적인 브랜치 간 PR이나 병합을 할 수 없다고 설명한다.
이 차이는 원본이 계속 개선될 때 드러난다. Reddit의 한 사용자는 템플릿으로 만든 프로젝트에 원본 업데이트를 가져오려 했다. 원본을 원격 저장소로 등록하고 병합하는 과정에서 서로 관련 없는 이력이라는 오류를 만났다고 답했다. 템플릿을 썼다는 사실만으로 원본 개발을 계속 따라가는 구조가 생기지는 않는다는 사례다.
앞으로 원본의 변경을 자주 합칠 계획이라면 시작할 때 fork를 택하는 편이 그 작업에 맞는다. 이미 템플릿에서 출발했다면 프로젝트를 버리고 다시 만들 필요는 없다. 원본 CI 설정의 변경처럼 필요한 파일을 비교해 가져올 수 있다. 원본의 실행 경로와 내 프로젝트의 경로가 다르면 그 부분을 맞춰 편집하고 자체 커밋으로 남긴다.
내 fork를 만들고 로컬에서 작업하기
fork 생성은 원본 저장소 오른쪽 위의 Fork에서 시작한다. 소유자와 저장소 이름을 정하고 기본 브랜치만 복사할지 선택한 뒤 생성한다. GitHub에 내 저장소가 생겨도 아직 컴퓨터에는 작업 파일이 없으므로, 내 fork의 Code에서 HTTPS 주소를 복사해 clone한다.
다음 예시는 Git이 설치돼 있고 GitHub 인증을 사용할 수 있는 터미널을 기준으로 한다. YOUR-ACCOUNT와 project는 실제 내 계정과 저장소 이름으로 바꾼다. 두 번째 명령의 폴더 이름도 clone으로 만들어진 실제 이름을 쓴다. 같은 이름의 기존 작업 폴더가 있다면 그 폴더를 지우지 말고 다른 위치에서 시작한다.
git clone https://github.com/YOUR-ACCOUNT/project.git
cd project
git remote -v 일반적인 git clone은 지정한 저장소의 파일과 이력을 로컬로 가져온다. 마지막 명령에 보이는 origin은 보통 방금 clone한 주소다. 내 fork를 복제했다면 내 계정의 URL이 보여야 한다. 원본 URL을 clone했다면 origin도 원본을 가리키며, 이 동작만으로 내 GitHub 계정에 fork가 생기거나 원본에 쓸 권한이 생기지는 않는다.
원본 변경을 받아올 주소는 관례적으로 upstream이라는 이름을 붙인다. 아래에서는 원본 소유자와 이름을 실제 값으로 바꾼다. 이미 같은 이름이 등록돼 있다면 다시 추가하지 않고 git remote -v에서 가리키는 주소를 읽는다.
git remote add upstream https://github.com/ORIGINAL-OWNER/project.git
git remote -v GitHub의 원격 저장소 구성 예시처럼 origin에는 내 fork, upstream에는 원본 주소가 보이면 두 곳을 구별해 사용할 수 있다. 출력에 각 주소의 fetch와 push 항목이 함께 나와도 원본에 쓰기 권한을 부여받은 것은 아니다. 이후 작업에서는 원본 변경을 upstream에서 받고, 내가 수정한 브랜치를 origin으로 보낸다.
작업 브랜치를 보내고 원본에 PR 만들기
수정안은 작업 브랜치에서 만든 뒤 내 fork에 올린다. 아래는 README의 오타를 고치는 예시다. 새 브랜치를 만든 다음 편집기에서 README.md를 수정하고 저장한 후 나머지 명령을 실행한다. 파일 이름이나 브랜치 이름이 다른 프로젝트에서는 실제 작업 대상을 쓴다.
git switch -c fix-readme
# README.md를 편집하고 저장한 뒤 실행한다.
git diff -- README.md
git add README.md
git commit -m "Fix README typo"
git push -u origin fix-readme diff에서 의도한 수정만 들어갔는지 읽은 뒤 커밋한다. 마지막 push가 성공하면 로컬의 fix-readme 브랜치가 내 GitHub fork에도 생긴다. 아직 원본 프로젝트의 파일은 바뀌지 않았다. 그 변경을 원본 관리자가 검토하도록 보내는 것이 PR이다.
fork에서 PR을 만드는 화면에서는 필요하면 compare across forks를 선택한다. 변경을 받을 base 저장소·브랜치는 원본으로, head fork와 비교 브랜치는 내 fork의 fix-readme로 정한다. 바뀐 파일과 줄을 확인하고 변경 이유를 작성해 PR을 만든다. 목적지를 내 fork의 main으로 잡으면 원본 프로젝트에 보내려던 요청과 달라지므로 이 화면의 저장소 이름이 중요하다.
push가 거부되면 PR 화면보다 먼저 origin 주소와 GitHub 인증 계정을 살핀다. 원본으로 직접 보내고 있거나 다른 계정으로 인증했다면 내 fork에 올리는 단계부터 끝나지 않는다. push 성공 여부와 PR 생성 여부는 GitHub의 내 브랜치 목록과 원본의 PR 목록에서 각각 볼 수 있다.
원본 변경을 내 fork와 컴퓨터에 반영하기
원본에 새 수정이 들어와도 내 fork와 컴퓨터의 작업 폴더가 동시에 바뀌지는 않는다. GitHub 웹에서 내 fork의 Sync fork를 열고 Update branch를 누르면 GitHub에 있는 fork 브랜치를 갱신한다. 원본과 충돌하는 수정이 있으면 자동 갱신 대신 PR을 통한 충돌 해결로 이어질 수 있다. 이 동작은 GitHub의 동기화 절차에 설명돼 있다.
터미널에서 원본 변경을 가져와 처리하려면 먼저 작업 중인 파일을 저장하고 필요한 변경을 커밋해 작업 트리를 정리한다. 아래는 실제 기본 브랜치 이름이 main이고 upstream 등록을 마친 경우다. 다른 이름을 쓰는 저장소에서는 로컬 브랜치와 원본 브랜치를 각각 그 이름으로 바꾼다.
git status
git fetch upstream
git checkout main
git merge upstream/main fetch는 원본의 최신 커밋을 내려받고, merge는 그것을 현재 선택한 로컬 main에 합친다. 충돌이 발생하면 파일의 충돌 부분을 고쳐 병합을 완료해야 한다. 이때까지 바뀐 곳은 로컬 저장소다. 프로젝트의 검사 절차를 실행하고 병합 결과가 맞으면 git push origin main으로 내 GitHub fork에도 보낸다.
반대로 웹에서 먼저 fork를 갱신했다면 컴퓨터에서는 origin을 대상으로 git fetch origin을 실행하고 로컬 main에서 git merge origin/main을 실행해 그 결과를 받는다. 로컬에서도 같은 부분을 수정했다면 이 과정 역시 충돌할 수 있다. 웹의 동기화 완료 표시가 컴퓨터의 미완성 작업까지 합쳤다는 뜻은 아니다.
이미 템플릿으로 만든 프로젝트의 업데이트
템플릿 프로젝트에서는 원본 주소를 추가하더라도 생성할 때 생략된 공통 이력이 되살아나지 않는다. 원본의 CI 설정 한 부분이 필요한 상황이라면 전체 이력을 한꺼번에 병합하기보다 해당 파일의 현재 내용을 비교하고 필요한 부분을 가져오는 방법이 있다. 이는 원본을 계속 동기화하는 기능이 아니라 현재 프로젝트에 개선을 직접 반영하는 작업이다.
원본 주소를 upstream으로 등록한 상태에서 다음 명령으로 양쪽 파일 차이를 볼 수 있다. 마지막 경로는 실제 원본에 있는 관심 파일로 바꾼다. 아래 예시의 .github/workflows/ci.yml은 CI 설정 파일의 가상 이름이다.
git fetch upstream
git diff --name-status HEAD upstream/main
git diff HEAD upstream/main -- .github/workflows/ci.yml 두 커밋을 받는 git diff는 현재 커밋과 원본 main 끝점의 내용을 비교한다. 따라서 템플릿을 만든 뒤 원본에서 바뀐 부분만 보여 주는 것은 아니다. 내 프로젝트에서 바꾼 이름·설정도 차이에 포함된다. 출력의 추가·삭제 표시는 현재 내용에서 원본 내용으로 바꿀 때의 차이이므로, 그대로 모두 적용하면 내 수정까지 되돌릴 수 있다.
예를 들어 원본 CI가 검사 명령을 바꿨더라도 내 프로젝트의 실행 명령과 작업 폴더가 이미 달라졌다면 해당 부분을 맞춰 편집한다. 파일을 저장하고 내 프로젝트의 검사를 실행한 뒤 자체 커밋으로 남긴다.
복사 방법에 따라 달라지는 공개 범위
공개 저장소를 fork하면 내 fork도 공개된다. GitHub의 fork 규칙상 fork만 따로 공개 범위를 바꿀 수 없으므로, 공개 원본을 fork한 다음 비공개로 전환하려는 계획은 맞지 않는다. 비공개 저장소의 fork는 원본 접근 권한과 소유자·조직 정책에 따라 허용 여부가 달라진다.
템플릿에서 만드는 새 저장소는 생성 화면에서 공개 범위를 고른다. 공개 예제를 바탕으로 별도 비공개 프로젝트를 시작하려는 경우에는 이 생성 방식이 요구에 맞을 수 있다. 생성 후에는 새 저장소 URL을 clone해 작업하면 된다. 로컬 origin이 새 주소를 가리키는지 확인하면 첫 push를 어느 프로젝트에 보낼지도 분명해진다.