git push가 "non-fast-forward / failed to push"로 거절될 때 안전하게 해결하는 법
빨간 메시지에 겁먹지 말자: 이건 버그가 아니라 안전장치다
방금 전까지 멀쩡히 잘 되던 git push가 갑자기 빨간 글씨로 거절당하면 누구나 당황한다.
! [rejected] main -> main (non-fast-forward)
error: failed to push some refs to 'github.com:team/repo.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref.결론부터 말하면, 이 메시지는 버그가 아니라 Git이 당신(혹은 동료)의 커밋을 지켜주는 안전장치다.
Git의 push는 기본적으로 "fast-forward(빨리 감기)"만 허용한다. 즉 원격의 현재 위치가 내 로컬 히스토리 안에 그대로 포함되어 있어서, 원격 포인터를 앞으로 쭉 밀기만 하면 되는 경우에만 통과시킨다. 그런데 내가 마지막으로 fetch한 뒤에 원격에 새 커밋이 올라왔다면, 원격을 앞으로 미는 것만으로는 내 상태를 반영할 수 없다. 이때 Git은 "그냥 밀면 원격의 그 커밋이 사라진다"고 판단해 push를 거절한다.
정리하면 단 한 문장이다. "내 로컬이 원격의 최신 상태를 모른 채 push하려고 해서 거절당한 것" — 그래서 해결의 90%는 force가 아니라 "원격 변경을 먼저 가져오는 것"이다.
메시지 → 원인 매핑표 & 5초 진단
거절 메시지는 환경(CLI 버전, GitHub/GitLab)마다 표현이 조금씩 다르지만 대부분 같은 원인이다. 표현에 휘둘리지 말자.
| 거절 메시지 | 실제 원인 | 1차 해결 |
|---|---|---|
! [rejected] ... (non-fast-forward) | 원격에 내가 모르는 커밋이 있음 | git pull --rebase 후 push |
Updates were rejected because the remote contains work | 위와 동일 | git pull --rebase 후 push |
tip of your current branch is behind its remote counterpart | 로컬이 원격보다 뒤처짐 | git pull --rebase 후 push |
hint: ... fetch first | 원격 변경을 안 가져옴 | git fetch → 진단 → pull |
failed to push some refs to ... | 위 상황들의 묶음 결과 메시지 | 위와 동일 |
! [rejected] ... (already exists) (tag) | 같은 이름 태그가 원격에 이미 존재 | 태그 갱신/재발행 (아래 참고) |
대부분 위쪽 5개는 한 가지 원인(원격이 앞서 있음)이다. 이제 내 상황이 "단순히 뒤처진 것"인지 "갈라진(diverged) 것"인지 5초 만에 판별하자.
# 1) 원격 최신 상태만 받아오기 (로컬 작업물은 건드리지 않음)
git fetch origin
# 2) 내 브랜치와 원격 브랜치가 어디서 갈렸는지 한눈에 보기
git log --oneline --graph --left-right HEAD...origin/main
git statusHEAD...origin/main(점 3개) 출력에서:
<표시만 있고>는 없다 → 나만 앞서 있음(정상). 보통 이 경우는 push가 됨.>표시만 있다 → 나는 뒤처지기만 함.git pull로 fast-forward면 끝.<와>가 섞여 있다 → 갈라짐(diverged). 가장 주의해야 하는 케이스.
상황별 안전한 해결법
분기 1 — 원격에 다른 커밋만 있는 일반 케이스
가장 흔하다. 동료가 같은 브랜치에 먼저 push했을 때다. 정석은 pull --rebase다.
git pull --rebase origin main
# 충돌이 없다면:
git push origin maingit pull(기본 merge)과 git pull --rebase의 차이는 이렇다.
git pull(merge): 원격 변경을 가져와 머지 커밋을 하나 만든다. 히스토리에 "Merge branch..." 커밋이 남는다.git pull --rebase: 내 커밋을 잠깐 떼어두고 원격 커밋 위에 다시 얹는다. 히스토리가 한 줄로 깔끔하게 유지된다.
선택 기준: 개인/기능 브랜치에서 깔끔한 직선 히스토리를 원하면 --rebase, 팀이 머지 커밋으로 병합 흐름을 기록하길 원하면 일반 pull. 매번 입력이 귀찮다면 다음으로 기본값을 지정할 수 있다.
git config --global pull.rebase true분기 2 — PR squash merge 후 로컬이 뒤처진 케이스
요즘 GitHub/GitLab은 squash merge가 기본 머지 방식인 팀이 많다. 내 PR이 머지되면 원격 main에는 여러 커밋이 하나로 합쳐진 새 커밋이 생긴다. 이때 로컬 main을 갱신하지 않고 새 작업을 push하면 거절된다. 이 경우 로컬 main을 원격에 맞추는 게 안전하다.
git checkout main
git fetch origin
git reset --hard origin/main # 로컬 main을 원격 기준으로 정렬 (main에 미push 작업이 없을 때만)주의:
reset --hard는 작업 디렉터리를 날린다. main 브랜치에 아직 push 안 한 변경이 있다면 절대 쓰지 말고, 먼저 백업 브랜치를 만들자.
분기 3 — 브랜치가 갈라졌을(diverged) 때
<와 >가 섞여 있던 경우다. 무엇보다 백업 브랜치를 먼저 만들고 정리한다.
# 1) 안전벨트: 현재 상태를 그대로 보존
git branch backup/my-feature
# 2) 원격 위에 내 커밋을 다시 얹기
git pull --rebase origin my-feature
# 3) 충돌이 나면 파일 수정 후
git add <충돌파일>
git rebase --continue
# 4) 마무리 push (개인 브랜치 한정, 아래 force 섹션 참고)
git push --force-with-lease origin my-featurebackup 브랜치가 있으면 정리가 꼬여도 git reset --hard backup/my-feature로 언제든 되돌아올 수 있다. 이 습관 하나가 사고의 9할을 막아준다.
force의 두 얼굴: --force vs --force-with-lease
rebase로 히스토리를 다시 썼다면 push에 force가 필요해진다. 하지만 force에는 두 종류가 있고, 이 차이가 동료의 커밋을 살리고 죽인다.
| 항목 | --force (-f) | --force-with-lease |
|---|---|---|
| 동작 | 원격 상태와 무관하게 무조건 덮어씀 | 내가 본 원격 상태와 같을 때만 덮어씀 |
| 동료 커밋 유실 위험 | 매우 높음 | 낮음 (모르는 변경 있으면 거절) |
| 그나마 허용되는 상황 | 사실상 권장 안 함 | 내 개인 브랜치 rebase/amend 후 |
| 공유 브랜치(main 등) | 절대 금지 | 절대 금지 |
요약하면, force를 써야 한다면 무조건 --force-with-lease를 기본으로 삼자. 이 옵션은 "내가 마지막으로 본 원격 상태와 지금이 다르면 멈춰라"는 안전핀이다.
⚠️ 안티패턴: "급해서 --force 했다가 동료 커밋 3개를 날렸다"
before (원격 origin/main):
CODEa1b2c3 (origin/main) 동료: 결제 모듈 버그 수정 d4e5f6 동료: 테스트 추가 7g8h9i 동료: 로깅 개선나는 이 3개를 fetch하지 않은 상태에서 내 커밋만 가진 채 실행했다.
Bashgit push --force origin main # 😱after (원격 origin/main):
CODEz9y8x7 (origin/main) 나: 사소한 오타 수정동료의 커밋 3개가 흔적도 없이 사라졌다. reflog로 복구는 가능하지만, 누가 무엇을 잃었는지 파악하는 데만 한나절이 걸린다.
만약
git push --force-with-lease였다면? 원격에 내가 모르는 커밋이 있으므로 push가 거절되어 사고 자체가 일어나지 않았다. 이것이 두 명령의 운명을 가른다.
이런 사고 때문에 요즘 GitHub/GitLab은 protected branch에서 force push 차단을 기본 권장한다. main/develop 같은 공유 브랜치는 반드시 보호 설정을 켜두자.
특수 케이스: 태그 push가 already exists로 거절될 때
! [rejected] v1.2.0 -> v1.2.0 (already exists)태그는 한 번 발행되면 변하지 않는 게 원칙이라, 같은 이름이 원격에 있으면 거절된다. 의도적으로 갱신해야 한다면(권장하진 않음):
git tag -f v1.2.0 # 로컬 태그를 새 커밋으로 이동
git push origin v1.2.0 --force # 원격 태그 갱신이미 배포/CI가 참조한 태그라면 갱신보다 새 버전 태그 발행이 안전하다.
실무 한마디
개인적으로 팀에 들어가면 가장 먼저 권하는 두 가지가 git config --global pull.rebase true와 "force는 무조건 --force-with-lease"다. 처음엔 타이핑이 길어 귀찮다는 반응이 많지만, 단 한 번이라도 --force-with-lease가 push를 막아 동료 커밋을 지켜준 경험을 하면 다들 기본 습관으로 굳힌다. 거절 메시지는 적이 아니라, 사고가 터지기 직전에 울려주는 알람이라고 생각하면 마음이 편해진다.
결론: 거절당했을 때의 황금 루틴 3단계
- 당황 금지 — 이건 에러가 아니라 안전장치다. 내 커밋은 아직 로컬에 안전히 있다.
- fetch로 진단 —
git fetch origin후git log --oneline --graph --left-right HEAD...origin/main로 뒤처짐인지 갈라짐인지 본다. - pull --rebase 우선 — 대부분
git pull --rebase→git push로 끝. force가 필요하면 개인 브랜치에서만--force-with-lease.
이건 다른 에러예요 (혼동 방지)
| 구분 | 이번 글의 non-fast-forward | refusing to merge unrelated histories |
|---|---|---|
| 원인 | 원격이 내 로컬보다 앞서 있음 | 공통 조상이 전혀 없는 두 히스토리를 합치려 함 |
| 증상 | push가 rejected (non-fast-forward) | pull/merge 시 refusing to merge unrelated histories |
| 해결 | git pull --rebase 후 push | --allow-unrelated-histories (별도 글 참고) |
refusing to merge unrelated histories, Permission denied (publickey), git rebase 충돌 해결은 push 거절과 원인이 전혀 다른 별개의 문제다. 이 글의 처방을 적용하지 말고 각 주제의 별도 글을 참고하자.
자주 묻는 질문 (FAQ)
Q. git pull --rebase와 그냥 git pull 중 뭘 써야 하나요?
A. 직선형 히스토리를 원하거나 개인/기능 브랜치라면 --rebase를 권합니다. 팀이 머지 커밋 기록을 선호하면 일반 pull을 쓰세요. 헷갈리면 git config --global pull.rebase true로 rebase를 기본값으로 두는 게 무난합니다.
Q. git push --force 절대 쓰면 안 되나요?
A. 공유 브랜치(main 등)에서는 절대 금지입니다. 개인 브랜치에서 rebase/amend 후 어쩔 수 없이 force가 필요할 땐 --force 대신 항상 --force-with-lease를 쓰세요. 원격에 모르는 변경이 있으면 자동으로 막아줍니다.
Q. pull 했더니 충돌(conflict)이 났어요. 어떻게 하나요?
A. 충돌은 push 거절과는 다른 단계의 문제입니다. 충돌 파일을 수정하고 git add 후 git rebase --continue(rebase 시) 또는 커밋(merge 시)으로 마무리합니다. 자세한 충돌 해결 절차는 별도 글을 참고하세요.
이 글은 AI 에이전트가 1차 초안을 작성한 뒤, 사람 편집자가 사실관계·출처·톤과 맥락을 검토하여 발행했습니다. 오류나 부정확한 내용이 확인되면 24시간 이내에 정정합니다.
댓글
불러오는 중...