
Git, 이것만 알면 협업한다 3편 — GitHub와 주고받기: push·pull·clone
내 컴퓨터에만 있던 커밋을 GitHub라는 '클라우드 금고'에 올리고(push), 동료의 커밋을 받아 오는(pull) 법을 배웁니다. 빈 저장소 만들기, clone, 2026년식 인증(gh auth login), 하루 루틴, 그리고 누구나 한 번은 만나는 push 거절(rejected)을 실제 출력으로 따라가며 풀어 봅니다.

내 컴퓨터에만 있던 커밋을 GitHub라는 '클라우드 금고'에 올리고(push), 동료의 커밋을 받아 오는(pull) 법을 배웁니다. 빈 저장소 만들기, clone, 2026년식 인증(gh auth login), 하루 루틴, 그리고 누구나 한 번은 만나는 push 거절(rejected)을 실제 출력으로 따라가며 풀어 봅니다.
지난 2편까지 민지는 자기 컴퓨터 안에서 cafe-menu 프로젝트에 세이브 포인트(커밋)를 차곡차곡 쌓았습니다. 그런데 이 기록은 아직 민지의 노트북 안에만 있습니다. 노트북을 잃어버리면 기록도 함께 사라지고, 함께 일하는 도윤은 민지가 무엇을 바꿨는지 볼 방법이 없습니다.
이번 3편에서는 그 기록을 인터넷 위의 공용 저장소로 올리고, 동료가 올린 기록을 받아 오는 법을 배웁니다. 명령은 딱 네 개가 중심입니다. clone(처음 한 번 통째로 받기), push(올리기), pull(받아서 합치기), 그리고 fetch(받아 오기만 하기). 여기에 누구나 한 번은 만나는 빨간 글씨, push 거절(rejected)을 실제 출력 그대로 재현해 보고 푸는 법까지 익힙니다.
git init --bare로 만든 "가짜 원격 저장소" 하나와, 민지·도윤 두 사람 몫의 복제본을 만들어 재현했고, 주소만 https://github.com/minji/cafe-menu.git로 바꿔 적었습니다. 실제 GitHub에 push하면 Enumerating objects…, Writing objects… 같은 진행 상황 줄이 몇 줄 더 나오는데, 뜻은 "보내는 중"이라 여기서는 생략했습니다. 터미널이 한국어로 설정되어 있으면 같은 내용이 한국어로 나옵니다.
1편에서 본 네 공간을 다시 떠올려 봅시다. 작업 폴더 → 스테이징 → 로컬 저장소까지는 모두 내 컴퓨터 안이었습니다. 네 번째 공간인 원격 저장소(remote repository)만 인터넷 저편에 있습니다.
원격 저장소를 클라우드 금고라고 생각하면 쉽습니다.
git add만 해 둔 파일은 금고로 가지 않습니다.이 금고를 빌려주는 서비스가 여럿 있는데, 가장 널리 쓰이는 곳이 GitHub입니다(그 밖에 GitLab, Bitbucket 등이 있고 사용법은 거의 같습니다). 이 글은 GitHub를 기준으로 설명합니다.
원격 저장소는 긴 주소(https://github.com/minji/cafe-menu.git)를 갖고 있습니다. 매번 이 주소를 치기는 번거로우니, git은 주소에 짧은 별명을 붙여 둡니다. 관례적으로 첫 번째 원격 저장소의 별명은 origin(원본)입니다. 특별한 기능이 있는 이름은 아니고, 모두가 그렇게 부르기로 한 약속일 뿐입니다. 앞으로 origin이 보이면 "그 금고"라고 읽으면 됩니다.
금고부터 만들어 봅시다. GitHub 계정이 없다면 github.com에서 먼저 가입합니다(무료 계정으로 비공개 저장소도 만들 수 있습니다).
cafe-menu를 적습니다. 설명(Description)은 비워도 됩니다.https://github.com/아이디/cafe-menu.git)와 함께 "다음에 칠 명령" 안내가 화면에 나옵니다.fatal: refusing to merge unrelated histories라는 낯선 에러가 나옵니다. 기존 폴더를 올릴 땐 완전히 빈 저장소를 만드는 것이 가장 깔끔합니다. (이미 체크했다면 FAQ에 해결법이 있습니다.)
GitHub 화면의 버튼 위치나 문구는 가끔 바뀝니다. 위 설명과 조금 달라도 "새 저장소 → 이름 → 공개 범위 → 만들기"라는 뼈대는 같습니다.
터미널을 좋아한다면: 뒤에서 설치할 GitHub CLI(
gh)로 로그인한 뒤, 프로젝트 폴더 안에서gh repo create cafe-menu --private --source=. --push를 치면 저장소 만들기·원격 등록·첫 push를 한 번에 해 줍니다. 다만 처음 배울 때는 아래 과정을 한 단계씩 직접 해 보는 편이 무엇이 일어나는지 이해하기 좋습니다.
GitHub는 아무나 내 금고에 커밋을 넣게 두지 않습니다. push(비공개 저장소라면 clone·pull도)를 하려면 "이 사람이 정말 민지인가"를 증명해야 합니다. 여기서 초보자가 가장 많이 막힙니다. 먼저 꼭 알아야 할 사실 하나.
대신 쓸 수 있는 방법은 세 가지입니다.
| 방법 | 어떻게 | 난이도 | 추천 대상 |
|---|---|---|---|
① GitHub CLI 로그인gh auth login | 브라우저로 한 번 로그인하면, git이 쓸 인증을 gh가 대신 관리 | 가장 쉬움 | 처음 시작하는 대부분의 사람 |
| ② HTTPS + 개인 액세스 토큰 | GitHub 설정에서 토큰(긴 문자열)을 만들어 비밀번호 칸에 붙여 넣기 | 보통 | gh를 설치할 수 없는 환경 |
| ③ SSH 키 | 내 컴퓨터에 열쇠 쌍을 만들고 공개 열쇠를 GitHub에 등록 | 조금 어려움 | 여러 서버를 오가는 개발자 |
gh auth loginGitHub CLI(gh)는 GitHub가 만든 공식 명령줄 도구입니다. 먼저 설치합니다.
# macOS (Homebrew)
brew install gh
# Windows (winget)
winget install --id GitHub.cli
설치가 끝나면 로그인합니다.
gh auth login
몇 가지 질문이 차례로 나옵니다. 처음이라면 이렇게 고르면 됩니다.
gh auth login --help에 따르면 이렇게 받은 인증 토큰은 운영체제의 안전한 저장소(macOS 키체인 등)에 보관됩니다. 잘 되었는지는 다음 명령으로 확인합니다.
gh auth status
이제부터 git push·git pull을 할 때 비밀번호를 묻지 않습니다. 혹시 로그인은 했는데 git이 여전히 Username을 묻는다면, gh를 git의 인증 도우미로 등록하는 명령을 한 번 실행하세요.
gh auth setup-git
개인 액세스 토큰(Personal Access Token, PAT)은 비밀번호 대신 쓰는 긴 문자열입니다. 비밀번호와 다른 점은 권한과 만료일을 좁게 정할 수 있다는 것입니다. GitHub 웹에서 오른쪽 위 프로필 → Settings → Developer settings → Personal access tokens로 가서 Fine-grained token을 만들고, 이 저장소에 대한 Contents 읽기·쓰기 권한만 주는 식입니다.
push할 때 터미널이 Username과 Password를 물으면, Username에는 GitHub 아이디를, Password 칸에 토큰을 붙여 넣습니다. 토큰은 만들 때 한 번만 보여 주므로 안전한 곳에 적어 두세요. 그리고 토큰은 비밀번호와 똑같이 취급해야 합니다. 채팅방이나 코드 파일에 붙여 넣지 마세요.
SSH는 "내 컴퓨터에만 있는 비밀 열쇠"와 "GitHub에 맡겨 둔 자물쇠(공개 열쇠)"를 맞춰 보는 방식입니다. 한 번 설정하면 편하지만 처음엔 단계가 많습니다. 쉽게 하려면 gh auth login에서 git 작업 방식을 SSH로 고르면 됩니다. gh가 기존 SSH 키를 찾아 올려 주거나, 없으면 새로 만들지 물어봅니다. SSH를 쓰면 원격 주소가 https://github.com/… 대신 git@github.com:minji/cafe-menu.git 모양이 됩니다.
gh auth login 조합이 설정할 것이 가장 적고, 나중에 SSH로 바꾸고 싶어지면 그때 바꿔도 늦지 않습니다.
GitHub와 연결하는 출발점은 두 가지입니다.
| 상황 | 쓰는 명령 | 이 글의 인물 |
|---|---|---|
| A. 내 컴퓨터에 이미 저장소(커밋)가 있고, 빈 금고에 처음 올린다 | git remote add → git push -u | 민지 |
| B. 금고에 이미 프로젝트가 있고, 내 컴퓨터로 통째로 받아 온다 | git clone | 도윤 |
민지는 1·2편에서 만든 cafe-menu 폴더가 있으니 A입니다. 폴더 안에서 시작합니다. 지금 연결된 원격 저장소가 있는지부터 봅시다.
git remote -v
아무것도 나오지 않습니다. git init으로 만든 저장소는 원격이 비어 있기 때문입니다. 이 상태에서 무작정 push를 하면 git이 "어디로 보내라는 거죠?"라고 되묻습니다.
$ git push
fatal: No configured push destination.
Either specify the URL from the command-line or configure a remote repository using
git remote add <name> <url>
and then push using the remote name
git push <name>
(출력 아래쪽의 remote group 안내는 줄였습니다.) 안내대로 금고 주소를 origin이라는 별명으로 등록합니다. 주소는 2단계에서 GitHub가 보여 준 것을 복사해 쓰세요.
git remote add origin https://github.com/minji/cafe-menu.git
등록됐는지 다시 확인합니다.
git remote -v
origin https://github.com/minji/cafe-menu.git (fetch)
origin https://github.com/minji/cafe-menu.git (push)
같은 주소가 두 줄 나옵니다. fetch(받아 올 때 쓰는 주소)와 push(올릴 때 쓰는 주소)입니다. 보통은 같습니다. 주소를 잘못 넣었다면 git remote set-url origin <새 주소>로 고칠 수 있습니다.
git push -u origin main드디어 올립니다.
git push -u origin main
To https://github.com/minji/cafe-menu.git
* [new branch] main -> main
branch 'main' set up to track 'origin/main'.
한 줄씩 읽어 봅시다.
To https://github.com/minji/cafe-menu.git — 이 금고로 보냈습니다.* [new branch] main -> main — 내 main 브랜치를 금고에 새 브랜치 main으로 만들었습니다. 금고가 비어 있었으니 "new branch"입니다.branch 'main' set up to track 'origin/main'. — 이게 -u가 한 일입니다. 아래에서 자세히 봅니다.이제 GitHub 웹에서 저장소 페이지를 새로고침하면 index.html, menu.md, README.md와 커밋 기록이 보입니다. 민지의 세이브 포인트가 처음으로 컴퓨터 밖으로 나간 순간입니다.
-u와 upstream: "짝꿍 브랜치" 정해 두기-u는 --set-upstream의 줄임말입니다. upstream(업스트림)은 "내 브랜치가 평소에 주고받는 원격 쪽 짝꿍 브랜치"라는 뜻입니다. git push -u origin main은 "origin의 main으로 보내고, 앞으로 내 main의 짝꿍은 origin/main이라고 기억해 둬"라는 명령입니다.
짝꿍을 한 번 정해 두면 두 가지가 편해집니다.
git push, git pull만 쳐도 됩니다. 어디로 보낼지 git이 압니다.git status가 "금고와 비교해 내가 앞서 있는지 뒤처져 있는지"를 알려 줍니다.git status
On branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean
Your branch is up to date with 'origin/main' — 내 main과 짝꿍 origin/main이 같은 상태라는 뜻입니다. 짝꿍 관계는 이 명령으로도 볼 수 있습니다.
git branch -vv
* main b901b3a [origin/main] README에 영업시간 추가
대괄호 안의 [origin/main]이 짝꿍입니다. 만약 -u 없이 브랜치를 만들고 그냥 git push를 치면 이런 안내가 나옵니다.
$ git push
fatal: The current branch main has no upstream branch.
To push the current branch and set the remote as upstream, use
git push --set-upstream origin main
To have this happen automatically for branches without a tracking
upstream, see 'push.autoSetupRemote' in 'git help config'.
겁먹을 필요 없습니다. git이 알려 준 명령(git push --set-upstream origin main, 줄여서 git push -u origin main)을 그대로 치면 됩니다. -u는 브랜치마다 처음 한 번만 붙이면 됩니다.
origin/main은 무엇인가요? — 내 컴퓨터가 기억하는 "마지막으로 확인했을 때 금고의 main은 여기였다"는 메모입니다. 금고 그 자체가 아니라, 내 컴퓨터 안의 사본 표시입니다. 이 메모는 push·fetch·pull을 할 때만 새로 고쳐집니다. 이 차이가 6장에서 중요해집니다.
git clone으로 통째로 받아 오기이제 도윤 차례입니다. 금고에는 민지가 올린 프로젝트가 있고, 도윤의 컴퓨터에는 아무것도 없습니다. (비공개 저장소라면 민지가 GitHub 저장소 설정에서 도윤을 협업자(Collaborator)로 초대해야 도윤이 받고 올릴 수 있습니다.)
도윤은 프로젝트를 둘 상위 폴더에서 clone을 합니다. clone(복제)은 금고의 모든 커밋 기록과 파일을 통째로 내려받아 새 폴더를 만들어 주는 명령입니다.
git clone https://github.com/minji/cafe-menu.git
Cloning into 'cafe-menu'...
done.
cafe-menu 폴더가 생겼습니다. 안으로 들어가 봅시다.
cd cafe-menu
git remote -v
origin https://github.com/minji/cafe-menu.git (fetch)
origin https://github.com/minji/cafe-menu.git (push)
git remote add를 하지 않았는데도 origin이 이미 등록되어 있습니다. clone은 받아 온 곳을 자동으로 origin이라고 기억합니다. 기록도 모두 받아졌습니다.
git log --oneline
b901b3a README에 영업시간 추가
a223993 카페 웹사이트 첫 버전
git branch -vv
* main b901b3a [origin/main] README에 영업시간 추가
짝꿍(upstream)까지 자동으로 설정되어 있습니다. 그래서 clone으로 시작한 사람은 -u를 붙일 필요 없이 처음부터 git push, git pull만 쓰면 됩니다.
| 비교 | A. remote add + push -u | B. clone |
|---|---|---|
| 시작할 때 내 컴퓨터 | 이미 커밋이 있는 폴더 | 아무것도 없음 |
| 시작할 때 금고 | 비어 있음 | 이미 프로젝트가 있음 |
| origin 등록 | 직접 git remote add | 자동 |
| upstream(짝꿍) 설정 | 첫 push에 -u | 자동 |
| 몇 번 하나 | 프로젝트당 한 번 | 컴퓨터당 한 번 |
빈 금고를 clone하면? GitHub에서 방금 만든 빈 저장소를 clone해도 됩니다. 이때는
warning: You appear to have cloned an empty repository.라는 경고가 나오는데, "비어 있는 걸 받았어요"라는 알림일 뿐 에러가 아닙니다. 그 폴더에서 파일을 만들고 커밋한 뒤 push하면 됩니다. 새 프로젝트라면 이렇게 "빈 저장소 만들기 → clone → 작업"으로 시작하는 쪽이 오히려 헷갈릴 일이 적습니다.
git remote는 금고 주소록을 다루는 명령입니다. 주소록의 각 줄은 "별명 → 주소" 한 쌍이고, 지금까지 쓴 origin도 그중 하나일 뿐입니다. 한 저장소에 원격을 여러 개 등록할 수도 있습니다(6편에서 오픈소스에 기여할 때 upstream이라는 두 번째 원격을 씁니다). 자주 쓰는 명령을 한 번에 정리합니다. 아래 출력은 모두 실제로 실행한 결과입니다.
① 목록 보기 — 별명만 보려면 git remote, 주소까지 보려면 -v를 붙입니다.
git remote
git remote -v
$ git remote
origin
$ git remote -v
origin https://github.com/minji/cafe-menu.git (fetch)
origin https://github.com/minji/cafe-menu.git (push)
② 추가하기 — git remote add <별명> <주소>. 이미 있는 별명으로 또 추가하면 거절됩니다. clone한 폴더에서 git remote add origin ...을 따라 치다가 자주 보는 에러입니다.
$ git remote add origin https://github.com/minji/cafe-menu.git
error: remote origin already exists.
이 에러가 나면 새로 추가할 게 아니라 이미 등록돼 있다는 뜻입니다. 주소가 맞는지 git remote -v로 확인하고, 다르면 아래 ④ set-url로 고치세요. 다른 별명이라면 얼마든지 추가할 수 있습니다. 예를 들어 민지가 백업용 금고를 하나 더 둔다면:
git remote add backup https://gitlab.com/minji/cafe-menu.git
git remote -v
backup https://gitlab.com/minji/cafe-menu.git (fetch)
backup https://gitlab.com/minji/cafe-menu.git (push)
origin https://github.com/minji/cafe-menu.git (fetch)
origin https://github.com/minji/cafe-menu.git (push)
이제 git push backup main처럼 별명으로 보낼 곳을 고릅니다. 별명 없이 git push만 치면 짝꿍(upstream)이 정해진 곳, 보통 origin으로 갑니다.
③ 이름 바꾸기·지우기 — rename과 remove는 주소록만 고칩니다. 금고(GitHub 저장소) 자체나 내 커밋은 전혀 건드리지 않으니 안심하고 써도 됩니다.
git remote rename backup mirror
git remote remove mirror
git remote -v
origin https://github.com/minji/cafe-menu.git (fetch)
origin https://github.com/minji/cafe-menu.git (push)
없는 별명을 지우려 하면 error: No such remote: 'mirror'가 나옵니다. 오타가 없는지 git remote로 목록부터 보세요.
④ 주소 바꾸기 — 저장소 이름을 바꿨거나, 인증을 HTTPS에서 SSH로 옮길 때 씁니다. SSH 주소는 git@github.com:아이디/저장소.git 모양입니다(SSH 키를 먼저 등록해 두어야 합니다, 3장 ③ 참고).
git remote set-url origin git@github.com:minji/cafe-menu.git
git remote -v
origin git@github.com:minji/cafe-menu.git (fetch)
origin git@github.com:minji/cafe-menu.git (push)
반대로 SSH에서 HTTPS로 돌아갈 때도 같은 명령에 https://github.com/minji/cafe-menu.git을 넣으면 됩니다. 주소 하나만 보고 싶다면 git remote get-url origin을 씁니다.
⑤ 자세히 보기 — git remote show origin은 그 원격과 내 브랜치들이 어떻게 짝지어져 있는지 한눈에 보여 줍니다. 이 명령은 금고에 실제로 접속해서 확인합니다.
git remote show origin
* remote origin
Fetch URL: https://github.com/minji/cafe-menu.git
Push URL: https://github.com/minji/cafe-menu.git
HEAD branch: main
Remote branch:
main tracked
Local branch configured for 'git pull':
main merges with remote main
Local ref configured for 'git push':
main pushes to main (up to date)
HEAD branch: main — 금고의 기본 브랜치는 main입니다.main merges with remote main — 내 main에서 git pull하면 금고의 main을 받아 합칩니다.main pushes to main (up to date) — 내 main에서 git push하면 금고의 main으로 가고, 지금은 둘이 같습니다.| 하고 싶은 일 | 명령 | 금고(원격 저장소)에 영향? |
|---|---|---|
| 별명 목록 | git remote / git remote -v | 없음 |
| 원격 추가 | git remote add <별명> <주소> | 없음 (주소록에만 추가) |
| 별명 바꾸기 | git remote rename <옛> <새> | 없음 |
| 원격 지우기 | git remote remove <별명> | 없음 (GitHub 저장소는 그대로) |
| 주소 바꾸기 | git remote set-url <별명> <새 주소> | 없음 |
| 주소 하나 보기 | git remote get-url <별명> | 없음 |
| 짝꿍 관계 자세히 | git remote show <별명> | 없음 (읽기만) |
git remote 명령은 모두 내 컴퓨터의 주소록만 바꿉니다. 금고에 무언가가 실제로 오가는 건 push·fetch·pull·clone뿐입니다. GitHub 저장소 자체를 지우거나 이름을 바꾸는 일은 GitHub 웹의 저장소 Settings에서 합니다.
이제 두 사람이 같은 금고를 씁니다. 도윤이 메뉴에 음료를 하나 추가해 커밋하고 올립니다.
git add menu.md
git commit -m "메뉴에 바닐라라떼 추가"
[main 137a16b] 메뉴에 바닐라라떼 추가
1 file changed, 1 insertion(+)
git push
To https://github.com/minji/cafe-menu.git
b901b3a..137a16b main -> main
b901b3a..137a16b main -> main은 "금고의 main을 b901b3a에서 137a16b로 앞으로 옮겼다"는 뜻입니다. 첫 push 때의 [new branch] 대신 이전..이후 형태로 나옵니다.
git status는 금고를 들여다보지 않는다같은 시각, 민지가 자기 컴퓨터에서 상태를 확인합니다.
git status
On branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean
"up to date(최신)"라고 나옵니다. 하지만 금고에는 도윤의 새 커밋이 이미 들어가 있습니다. 왜 이럴까요? 4장 끝에서 본 대로 origin/main은 마지막으로 확인했을 때의 메모이기 때문입니다. git status는 인터넷에 접속하지 않고, 이 메모와 내 main을 비교할 뿐입니다. 즉 이 문장은 "금고와 같다"가 아니라 "내가 마지막으로 본 금고와 같다"로 읽어야 합니다.
git fetch: 받아 오기만 하기메모를 새로 고치는 명령이 git fetch입니다. 금고에 접속해 새 커밋을 내려받고 origin/main 메모를 갱신하지만, 내 main과 작업 폴더의 파일은 건드리지 않습니다.
git fetch
From https://github.com/minji/cafe-menu
b901b3a..137a16b main -> origin/main
"금고의 main이 137a16b까지 왔길래 내 메모(origin/main)를 옮겨 뒀다"는 뜻입니다. 이제 상태를 다시 보면 달라집니다.
git status
On branch main
Your branch is behind 'origin/main' by 1 commit, and can be fast-forwarded.
(use "git pull" to update your local branch)
nothing to commit, working tree clean
behind ... by 1 commit — 금고보다 커밋 1개 뒤처져 있다는 뜻입니다. can be fast-forwarded는 "내 쪽에 새 커밋이 없으니 그냥 앞으로 당기기만 하면 된다"는 뜻인데, 이 fast-forward(빨리 감기)는 4편에서 자세히 다룹니다. git이 친절하게 다음 할 일도 알려 줍니다: use "git pull".
git pull: 받아서 합치기git pull
Updating b901b3a..137a16b
Fast-forward
menu.md | 1 +
1 file changed, 1 insertion(+)
이제 민지의 menu.md에도 바닐라라떼가 들어왔습니다.
git log --oneline
137a16b 메뉴에 바닐라라떼 추가
b901b3a README에 영업시간 추가
a223993 카페 웹사이트 첫 버전
방금은 fetch를 먼저 하고 pull을 했지만, 사실 git pull은 안에서 fetch를 먼저 한 뒤 merge(합치기)를 하는 명령입니다. fetch를 따로 하지 않고 바로 git pull만 쳐도 결과는 같습니다.
| 명령 | 금고에 접속 | origin/main 메모 | 내 main·파일 | 언제 쓰나 |
|---|---|---|---|---|
git status | 안 함 | 그대로 | 그대로 | 내 상태 확인 (메모 기준) |
git fetch | 함 | 갱신 | 그대로 | "뭐가 바뀌었나" 먼저 보고 싶을 때 |
git pull | 함 | 갱신 | 합쳐짐 | 받아서 바로 내 것으로 만들 때 (평소) |
git push | 함 | 갱신 | 그대로 | 내 커밋을 금고에 올릴 때 |
fetch는 "택배를 문 앞까지만 받아 두기", pull은 "받아서 뜯고 정리까지 하기"라고 생각하면 됩니다. 입문 단계에서는 거의 항상 git pull을 쓰고, 합치기 전에 무엇이 들어왔는지 먼저 보고 싶을 때만 git fetch 후 git log --oneline main..origin/main처럼 확인하면 충분합니다.
이제 협업에서 가장 자주 만나는 장면입니다. 둘 다 최신 상태에서 출발해, 도윤은 menu.md에 레몬에이드를, 민지는 index.html에 영업시간을 추가했습니다. 도윤이 먼저 push합니다.
$ git push
To https://github.com/minji/cafe-menu.git
137a16b..81deecf main -> main
잠시 뒤 민지도 커밋하고 push합니다.
git add index.html
git commit -m "첫 화면에 영업시간 표시"
git push
[main 6614321] 첫 화면에 영업시간 표시
1 file changed, 1 insertion(+)
To https://github.com/minji/cafe-menu.git
! [rejected] main -> main (fetch first)
error: failed to push some refs to 'https://github.com/minji/cafe-menu.git'
hint: Updates were rejected because the remote contains work that you do not
hint: have locally. This is usually caused by another repository pushing to
hint: the same ref. If you want to integrate the remote changes, use
hint: 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
빨간 글씨가 잔뜩 나와 놀랄 수 있지만, 번역해 보면 아주 점잖은 말입니다.
왜 거절할까요? 금고의 main은 지금 도윤의 레몬에이드 커밋을 가리키고 있습니다. 민지의 main은 그 커밋을 모른 채 바닐라라떼 → 민지의 영업시간 커밋으로 이어집니다. 여기서 민지의 main으로 금고를 그냥 덮으면 도윤의 레몬에이드 커밋이 금고에서 사라집니다. git은 남의 작업을 몰래 지우지 않도록, 금고가 "내 커밋 뒤에 이어 붙이기만 하면 되는 경우"에만 push를 받아 줍니다. 거절은 고장이 아니라 안전장치입니다.
git pull로 도윤의 커밋을 받아 민지의 커밋과 합친다(merge).git push가 받아들여진다.안내대로 git pull을 쳤더니, 최근 git에서는 이런 긴 안내와 함께 멈출 수 있습니다.
$ git pull
From https://github.com/minji/cafe-menu
137a16b..81deecf main -> origin/main
hint: You have divergent branches and need to specify how to reconcile them.
hint: You can do so by running one of the following commands sometime before
hint: your next pull:
hint:
hint: git config pull.rebase false # merge
hint: git config pull.rebase true # rebase
hint: git config pull.ff only # fast-forward only
hint:
hint: You can replace "git config" with "git config --global" to set a default
hint: preference for all repositories. You can also pass --rebase, --no-rebase,
hint: or --ff-only on the command line to override the configured default per
hint: invocation.
fatal: Need to specify how to reconcile divergent branches.
divergent branches(갈라진 브랜치)란 "내 쪽에도 새 커밋이 있고, 금고 쪽에도 새 커밋이 있어서 역사가 두 갈래로 갈라졌다"는 뜻입니다. 지금 상태가 정확히 그렇습니다.
$ git status
On branch main
Your branch and 'origin/main' have diverged,
and have 1 and 1 different commits each, respectively.
(use "git pull" if you want to integrate the remote branch with yours)
갈라진 두 갈래를 하나로 만드는 방법은 크게 두 가지가 있습니다. git은 사용자가 어느 쪽을 원하는지 모르니, 한 번 정해 달라고 요청하는 것입니다. fetch는 이미 끝났고(맨 위 두 줄), 합치기 단계에서 멈춘 상태입니다.
| 설정 | 하는 일 | 결과 | 입문자에게 |
|---|---|---|---|
pull.rebase false | 두 갈래를 merge 커밋으로 묶어 합침 | 역사가 있는 그대로 남음. 그래프에 합쳐진 흔적이 보임 | 추천 — 이해하기 쉽고, 문제가 생겨도 되돌리기 쉬움 |
pull.rebase true | 내 커밋을 금고 커밋 뒤로 옮겨 다시 쌓음 | 역사가 한 줄로 깔끔함 | 개념을 익힌 뒤에 (6편에서 개념 소개) |
pull.ff only | 빨리 감기가 가능할 때만 pull, 갈라졌으면 거부 | 합치기는 직접 해야 함 | 조심스러운 팀용 |
이 시리즈는 merge 방식을 기본으로 합니다. 모든 저장소에 한 번에 적용되도록 --global을 붙여 설정합니다.
git config --global pull.rebase false
설정은 한 번만 하면 됩니다. 확인은 이렇게 합니다.
git config --global --get pull.rebase
false
이제 다시 pull합니다.
git pull
이때 merge 커밋 메시지를 적는 편집기 화면이 열릴 수 있습니다. Merge branch 'main' of https://github.com/…라는 기본 메시지가 이미 적혀 있으니, 그대로 저장하고 닫으면 됩니다. (vim이 열렸다면 :wq를 치고 Enter. 편집기를 건너뛰고 싶으면 git pull --no-edit을 쓰세요.) 그러면 이렇게 끝납니다.
Merge made by the 'ort' strategy.
menu.md | 1 +
1 file changed, 1 insertion(+)
ort는 git이 기본으로 쓰는 합치기 방식의 이름이니 신경 쓰지 않아도 됩니다. 그래프로 보면 무슨 일이 있었는지 한눈에 들어옵니다.
git log --oneline --graph
* f3d8af5 Merge branch 'main' of https://github.com/minji/cafe-menu
|\
| * 81deecf 메뉴에 레몬에이드 추가
* | 6614321 첫 화면에 영업시간 표시
|/
* 137a16b 메뉴에 바닐라라떼 추가
* b901b3a README에 영업시간 추가
* a223993 카페 웹사이트 첫 버전
바닐라라떼 커밋에서 두 갈래(민지의 영업시간, 도윤의 레몬에이드)로 갈라졌다가, 맨 위 merge 커밋 f3d8af5에서 다시 만났습니다. 이제 민지의 main은 금고의 모든 커밋을 포함하므로, push가 "이어 붙이기"가 됩니다.
git push
To https://github.com/minji/cafe-menu.git
81deecf..f3d8af5 main -> main
성공입니다. 도윤이 나중에 pull하면 민지의 영업시간과 merge 커밋을 빨리 감기로 받아 갑니다.
$ git pull
From https://github.com/minji/cafe-menu
81deecf..f3d8af5 main -> origin/main
Updating 81deecf..f3d8af5
Fast-forward
index.html | 1 +
1 file changed, 1 insertion(+)
menu.md, 민지는 index.html을 고쳐서 git이 자동으로 합칠 수 있었습니다. 둘이 같은 파일의 같은 줄을 서로 다르게 고쳤다면 git은 어느 쪽이 맞는지 모르니 pull 도중 충돌(conflict)을 알리고 사람에게 골라 달라고 합니다. 충돌을 읽고 푸는 법은 5편에서 따로 다룹니다. 지금은 "거절 → pull → push"라는 흐름만 기억하세요.
(non-fast-forward)거절 메시지의 괄호 안이 fetch first가 아니라 non-fast-forward인 경우도 있습니다.
$ git push
To https://github.com/minji/cafe-menu.git
! [rejected] main -> main (non-fast-forward)
error: failed to push some refs to 'https://github.com/minji/cafe-menu.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. If you want to integrate the remote changes,
hint: use 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
차이는 사소합니다. fetch first는 "금고에 처음 보는 커밋이 있다", non-fast-forward는 "fetch로 받아 두기는 했는데 내 main에 합치지 않았다"입니다. 해결법은 똑같이 git pull 후 git push입니다.
아래에서 두 사람의 컴퓨터와 금고를 버튼으로 움직여 볼 수 있습니다. 추천 순서대로 한 번 눌러 거절을 겪어 보고, 그다음엔 fetch만 하고 push해 non-fast-forward도 만들어 보세요. git status 줄이 "ahead(앞섬)", "behind(뒤처짐)", "diverged(갈라짐)"로 바뀌는 것도 눈여겨보세요.
push --force로 밀어붙이지 마세요거절 메시지를 검색하다 보면 "git push --force(또는 -f)를 쓰면 된다"는 답을 보게 됩니다. 실제로 그 명령을 치면 거절 없이 올라갑니다. 문제는 어떻게 올라가느냐입니다.
git push --force를 쳤다면, 금고의 main은 민지의 커밋을 가리키게 되고 도윤의 레몬에이드 커밋은 금고에서 사라집니다. 도윤은 영문도 모른 채 다음 pull에서 이상한 상태를 만나게 됩니다. 거절 메시지를 없애는 방법이 아니라, 동료의 작업을 지우는 방법입니다.
규칙은 간단합니다.
--force를 쓰지 않습니다. 거절되면 언제나 pull 먼저.git revert를 씁니다. 되돌리기 방법들은 5편에서 자세히 다룹니다.--force-with-lease가 더 안전합니다. 하지만 이 시리즈를 읽는 단계에서는 "force가 떠오르면 멈추고 물어보기"로 충분합니다.많은 팀이 GitHub의 브랜치 보호 규칙으로 main에 강제 push를 아예 막아 둡니다. 이 설정은 6편에서 다룹니다.
지금까지 배운 것을 하루 흐름으로 묶으면 이렇습니다. 이 여섯 줄만 몸에 익혀도 협업의 대부분은 문제없이 굴러갑니다.
git pull — 밤사이 동료가 올린 커밋부터 받기. 오래된 파일 위에서 일하지 않기 위해.git status·git diff로 무엇이 바뀌었는지 확인.git add → git commit -m "..." — 의미 있는 단위마다 세이브 포인트(2편). 하루에 여러 번 해도 좋습니다.git pull — 작업하는 사이 누가 push했을 수 있습니다. 먼저 받아 합쳐 두면 거절되지 않습니다.git push — 커밋이 내 컴퓨터를 떠나 금고로. 동료가 받아 볼 수 있게 됩니다.몇 가지 요령을 덧붙이면:
git stash는 5편에서).git log --oneline -3으로 한 번 훑기. 무엇이 올라가는지 눈으로 확인하는 습관은, 나중에 AI 에이전트와 일할 때 더 중요해집니다.순서를 섞어 놓은 카드를 올바른 순서로 눌러 보세요. 순서를 틀리면 실제로 어떤 일이 생기는지 알려 줍니다.
마지막으로, 이번 편에서 만났거나 곧 만날 에러 메시지를 한곳에 모았습니다. 목록에서 고르거나 터미널에 뜬 메시지를 그대로 붙여 넣으면 쉬운 설명과 해결 명령을 보여 줍니다.
그중 가장 자주 만나는 것만 표로 정리하면 다음과 같습니다.
| 메시지의 핵심 문구 | 뜻 | 해결 |
|---|---|---|
[rejected] ... (fetch first) | 금고에 내가 모르는 커밋이 있음 | git pull → git push |
[rejected] ... (non-fast-forward) | 받아 두기만 하고 안 합침 | git pull → git push |
divergent branches | 양쪽 다 새 커밋, 합치는 방식 미정 | git config --global pull.rebase false 후 다시 pull |
has no upstream branch | 짝꿍 브랜치가 없음 | git push -u origin main |
No configured push destination | 원격 주소를 등록 안 함 | git remote add origin <주소> |
remote origin already exists | origin이 이미 등록돼 있음(clone한 폴더에서 흔함) | git remote -v로 확인, 다르면 git remote set-url |
src refspec main does not match any | 올릴 main 브랜치가 없음(커밋이 없거나 이름이 다름) | 커밋부터 하거나 git branch로 이름 확인 |
refusing to merge unrelated histories | 뿌리가 다른 두 역사(GitHub에서 README 체크) | --allow-unrelated-histories (FAQ 참고) |
| 인증 실패(Authentication failed) | 비밀번호를 넣었거나 토큰이 틀림/만료 | gh auth login |
git log --oneline origin/main..HEAD로 무엇이 올라가는지 ② 올라가는 곳이 어느 원격·어느 브랜치인지 ③ 명령에 --force나 -f가 섞여 있지 않은지. 에이전트가 push 거절을 만났을 때 force로 밀어붙이자고 하면 거절하고 "pull해서 합친 뒤 다시 push해 줘"라고 말하면 됩니다. 에이전트에게 권한을 어디까지 줄지, 위험한 명령을 아예 막는 설정은 7편에서 다룹니다.
Q1. GitHub에서 README를 체크하고 저장소를 만들어 버렸어요. 첫 push가 거절돼요.
금고에 GitHub가 만든 "Initial commit"이 있고, 내 컴퓨터엔 따로 시작한 커밋이 있어서 뿌리가 다른 상태입니다. 그냥 git pull origin main을 하면 divergent branches 안내가, 합치기 방식을 정한 뒤에는 fatal: refusing to merge unrelated histories가 나옵니다. "뿌리가 달라도 합쳐도 된다"고 명시하면 됩니다.
git pull origin main --allow-unrelated-histories --no-rebase
git push -u origin main
From https://github.com/minji/cafe-menu
* branch main -> FETCH_HEAD
Merge made by the 'ort' strategy.
README.md | 1 +
1 file changed, 1 insertion(+)
create mode 100644 README.md
(편집기가 열리면 저장하고 닫으세요.) 양쪽 README가 모두 있었다면 충돌이 날 수 있는데, 그 해결은 5편을 보세요. 아직 아무것도 올리지 않았다면 GitHub에서 저장소를 지우고 빈 저장소로 다시 만드는 것도 방법입니다.
Q2. git pull과 git fetch 중 뭘 써야 하나요?
평소에는 git pull이면 됩니다. fetch는 "받아 오기만 하고 내 파일은 그대로 두는" 명령이라, 합치기 전에 동료가 무엇을 올렸는지 먼저 살펴보고 싶을 때 씁니다. git fetch 후 git status를 보면 몇 커밋 뒤처졌는지 알 수 있고, git log --oneline main..origin/main으로 새로 들어온 커밋 목록을 볼 수 있습니다.
Q3. git status가 "up to date"라는데, 동료는 방금 push했다고 해요.
git status는 인터넷에 접속하지 않고, 마지막으로 확인한 금고 상태(origin/main 메모)와 비교합니다. git fetch를 한 번 한 뒤 다시 git status를 보면 "behind"로 바뀝니다. 그래서 "up to date"는 "내가 마지막으로 본 금고와 같다"로 읽어야 합니다.
Q4. push하려는데 Username과 Password를 물어요. 비밀번호를 넣었더니 실패해요.
GitHub는 git 작업에서 계정 비밀번호를 받지 않습니다(2021년 8월부터). 가장 쉬운 해결은 gh auth login으로 로그인하는 것이고, 토큰을 쓰려면 GitHub 설정에서 개인 액세스 토큰을 만들어 비밀번호 칸에 붙여 넣습니다. 한 번 잘못된 값이 저장되어 계속 실패한다면, gh auth login 후 gh auth setup-git을 실행해 인증을 gh에 맡기세요.
Q5. 원격 저장소 주소를 바꾸고 싶어요 (저장소 이름을 바꿨거나, SSH로 옮기고 싶어요).
git remote set-url로 origin의 주소만 바꾸면 됩니다. 커밋 기록은 그대로입니다.
git remote set-url origin https://github.com/minji/cafe-menu-v2.git
git remote -v
Q6. pull할 때마다 "Merge branch 'main' of …" 커밋이 생기는 게 지저분해 보여요.
merge 방식(pull.rebase false)의 자연스러운 결과입니다. 두 갈래를 합쳤다는 사실이 기록에 남는 것이라 문제는 아닙니다. 올리기 전 pull 습관을 들이면 갈라질 일 자체가 줄어 merge 커밋도 줄어듭니다. 한 줄짜리 깔끔한 역사를 원하는 팀은 rebase 방식을 쓰는데, 그 차이와 주의점은 6편에서 개념으로 소개합니다. 역사와 내부 구조까지 궁금하다면 Git 완전 정복도 참고하세요.
| 하고 싶은 일 | 명령 | 메모 |
|---|---|---|
| GitHub에 로그인(인증) | gh auth login | 비밀번호로는 push 불가. 확인은 gh auth status |
| 금고 주소 등록 | git remote add origin <주소> | 기존 폴더를 올릴 때 한 번 |
| 등록된 원격 확인 | git remote -v | fetch·push 주소 두 줄 |
| 원격 주소 바꾸기 | git remote set-url origin <새 주소> | 저장소 이름 변경·HTTPS↔SSH 전환 |
| 원격 이름 바꾸기·지우기 | git remote rename · git remote remove | 주소록만 바뀜, GitHub 저장소는 그대로 |
| 짝꿍 관계 자세히 보기 | git remote show origin | pull·push가 어디로 가는지 |
| 첫 push + 짝꿍 설정 | git push -u origin main | -u는 브랜치마다 처음 한 번 |
| 금고에서 통째로 받기 | git clone <주소> | origin·짝꿍 자동 설정 |
| 올리기 | git push | 커밋만 올라감 |
| 받아서 합치기 | git pull | = fetch + merge |
| 받아 오기만 | git fetch | 내 파일은 그대로, origin/main만 갱신 |
| 짝꿍과 앞섬/뒤처짐 보기 | git status · git branch -vv | 기준은 마지막으로 본 금고 |
| pull 합치기 방식 정하기 | git config --global pull.rebase false | 한 번만. 이 시리즈는 merge 방식 |
| push 거절됐을 때 | git pull → git push | --force 금지 |
| 하루 루틴 | pull → 작업 → add → commit → pull → push | 자주, 작게 |
이번 편까지 민지와 도윤은 둘 다 main 한 줄 위에서 일했습니다. 그래서 누가 먼저 push하느냐에 따라 거절과 merge가 생겼죠. 그런데 도윤이 몇 주 걸리는 새 메뉴판 디자인을 만드는 동안, 민지는 오늘 당장 가격을 고쳐야 한다면 어떻게 할까요? 4편 — 브랜치와 merge에서는 브랜치로 작업 줄기를 나눠 서로 방해하지 않고 일한 뒤, 완성된 것만 합치는 법을 배웁니다. 이번 편에서 스쳐 지나간 fast-forward와 merge 커밋의 정체도 거기서 밝혀집니다.