/개발/git push non-fast-forward / failed to push 거절 안전하게 해결하기
개발non-fast-forward 해결failed to push some refs

git push non-fast-forward / failed to push 거절 안전하게 해결하기

git push가 non-fast-forward, failed to push some refs, Updates were rejected로 거절될 때 5초 진단법과 git pull --rebase 정석, force push 위험, --force-with-lease 사용법까지 복붙 명령어로 정리했다.

git push non-fast-forward / failed to push 거절 안전하게 해결하기

git push가 "non-fast-forward / failed to push"로 거절될 때 안전하게 해결하는 법

빨간 메시지에 겁먹지 말자: 이건 버그가 아니라 안전장치다

방금 전까지 멀쩡히 잘 되던 git push가 갑자기 빨간 글씨로 거절당하면 누구나 당황한다.

CODE
! [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초 만에 판별하자.

Bash
# 1) 원격 최신 상태만 받아오기 (로컬 작업물은 건드리지 않음)
git fetch origin

# 2) 내 브랜치와 원격 브랜치가 어디서 갈렸는지 한눈에 보기
git log --oneline --graph --left-right HEAD...origin/main
git status

HEAD...origin/main(점 3개) 출력에서:

  • < 표시만 있고 >는 없다 → 나만 앞서 있음(정상). 보통 이 경우는 push가 됨.
  • > 표시만 있다 → 나는 뒤처지기만 함. git pull로 fast-forward면 끝.
  • <>가 섞여 있다갈라짐(diverged). 가장 주의해야 하는 케이스.

상황별 안전한 해결법

분기 1 — 원격에 다른 커밋만 있는 일반 케이스

가장 흔하다. 동료가 같은 브랜치에 먼저 push했을 때다. 정석은 pull --rebase다.

Bash
git pull --rebase origin main
# 충돌이 없다면:
git push origin main

git pull(기본 merge)과 git pull --rebase의 차이는 이렇다.

  • git pull(merge): 원격 변경을 가져와 머지 커밋을 하나 만든다. 히스토리에 "Merge branch..." 커밋이 남는다.
  • git pull --rebase: 내 커밋을 잠깐 떼어두고 원격 커밋 위에 다시 얹는다. 히스토리가 한 줄로 깔끔하게 유지된다.

선택 기준: 개인/기능 브랜치에서 깔끔한 직선 히스토리를 원하면 --rebase, 팀이 머지 커밋으로 병합 흐름을 기록하길 원하면 일반 pull. 매번 입력이 귀찮다면 다음으로 기본값을 지정할 수 있다.

Bash
git config --global pull.rebase true

분기 2 — PR squash merge 후 로컬이 뒤처진 케이스

요즘 GitHub/GitLab은 squash merge가 기본 머지 방식인 팀이 많다. 내 PR이 머지되면 원격 main에는 여러 커밋이 하나로 합쳐진 새 커밋이 생긴다. 이때 로컬 main을 갱신하지 않고 새 작업을 push하면 거절된다. 이 경우 로컬 main을 원격에 맞추는 게 안전하다.

Bash
git checkout main
git fetch origin
git reset --hard origin/main   # 로컬 main을 원격 기준으로 정렬 (main에 미push 작업이 없을 때만)

주의: reset --hard는 작업 디렉터리를 날린다. main 브랜치에 아직 push 안 한 변경이 있다면 절대 쓰지 말고, 먼저 백업 브랜치를 만들자.

분기 3 — 브랜치가 갈라졌을(diverged) 때

<>가 섞여 있던 경우다. 무엇보다 백업 브랜치를 먼저 만들고 정리한다.

Bash
# 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-feature

backup 브랜치가 있으면 정리가 꼬여도 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):

CODE
a1b2c3 (origin/main) 동료: 결제 모듈 버그 수정
d4e5f6 동료: 테스트 추가
7g8h9i 동료: 로깅 개선

나는 이 3개를 fetch하지 않은 상태에서 내 커밋만 가진 채 실행했다.

Bash
git push --force origin main   # 😱

after (원격 origin/main):

CODE
z9y8x7 (origin/main) 나: 사소한 오타 수정

동료의 커밋 3개가 흔적도 없이 사라졌다. reflog로 복구는 가능하지만, 누가 무엇을 잃었는지 파악하는 데만 한나절이 걸린다.

만약 git push --force-with-lease였다면? 원격에 내가 모르는 커밋이 있으므로 push가 거절되어 사고 자체가 일어나지 않았다. 이것이 두 명령의 운명을 가른다.

이런 사고 때문에 요즘 GitHub/GitLab은 protected branch에서 force push 차단을 기본 권장한다. main/develop 같은 공유 브랜치는 반드시 보호 설정을 켜두자.

특수 케이스: 태그 push가 already exists로 거절될 때

CODE
! [rejected]   v1.2.0 -> v1.2.0 (already exists)

태그는 한 번 발행되면 변하지 않는 게 원칙이라, 같은 이름이 원격에 있으면 거절된다. 의도적으로 갱신해야 한다면(권장하진 않음):

Bash
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단계

  1. 당황 금지 — 이건 에러가 아니라 안전장치다. 내 커밋은 아직 로컬에 안전히 있다.
  2. fetch로 진단git fetch origingit log --oneline --graph --left-right HEAD...origin/main로 뒤처짐인지 갈라짐인지 본다.
  3. pull --rebase 우선 — 대부분 git pull --rebasegit push로 끝. force가 필요하면 개인 브랜치에서만 --force-with-lease.

이건 다른 에러예요 (혼동 방지)

구분이번 글의 non-fast-forwardrefusing 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 addgit rebase --continue(rebase 시) 또는 커밋(merge 시)으로 마무리합니다. 자세한 충돌 해결 절차는 별도 글을 참고하세요.

✦ ✦ ✦
편집 검토 · Editorial Review

이 글은 AI 에이전트가 1차 초안을 작성한 뒤, 사람 편집자가 사실관계·출처·톤과 맥락을 검토하여 발행했습니다. 오류나 부정확한 내용이 확인되면 24시간 이내에 정정합니다.

작성 · Content Reviewer·검토 · 사람 편집자·발행 · 2026년 6월 14일

댓글

불러오는 중...