
"Git을 공부하면서 쓰는 시리즈" 4편입니다.
1편: git init · 2편: 브랜치 · 3편: git log 트리
시작하면서
3편까지 따라왔다면 지금 이런 상태입니다.
* c1d2e3f (feature/login) 로그인 페이지 초안 추가
* a9b3c12 (HEAD -> main) README에 프로젝트 설명 추가
* 3f2a1b4 첫 번째 커밋: README 추가
feature/login에서 작업을 마쳤습니다. 이제 이걸 main에 합쳐야 합니다. 그게 merge입니다.
그런데 merge를 찾아보다 보면 "fast-forward", "merge commit", "conflict" 같은 단어가 나옵니다. 처음엔 다 뭔가 싶었습니다. 이번 편에서 하나씩 짚어봅니다.
git merge 기본 사용법
main 브랜치로 이동한 다음 merge합니다.
git switch main
git merge feature/login
Updating a9b3c12..c1d2e3f
Fast-forward
login.md | 1 +
1 file changed, 1 insertion(+)
create mode 100644 login.md
Fast-forward라는 말이 보입니다. 이게 뭔지 먼저 이해해야 다음이 편합니다.
Fast-forward merge
현재 상태를 그림으로 보면 이렇습니다.
main: C1 ── C2
\
feature/login: C3
main은 C2에 머물러 있고, feature/login은 C3까지 앞서 있습니다. 그리고 C3는 C2의 직계 자손입니다. 중간에 다른 갈래가 없어요.
이 경우 Git은 굳이 새로운 merge 커밋을 만들지 않습니다. 그냥 main 포인터를 C3까지 앞으로 당기면 끝입니다.
main: C1 ── C2 ── C3
↑
(HEAD, main, feature/login)
포인터만 이동한 거라서 "빨리감기(Fast-forward)"라고 부릅니다. merge 커밋이 따로 생기지 않습니다.
git lg로 확인해보면 이렇습니다.
* c1d2e3f (HEAD -> main, feature/login) 로그인 페이지 초안 추가
* a9b3c12 README에 프로젝트 설명 추가
* 3f2a1b4 첫 번째 커밋: README 추가
main과 feature/login이 같은 커밋을 가리키고 있습니다.
Merge commit — 두 흐름이 만날 때
이번엔 다른 경우를 만들어봅니다. feature/login을 작업하는 동안 main에도 새 커밋이 생긴 상황입니다.
# main에서 다른 파일 수정
git switch main
echo "공지사항 추가" > notice.md
git add notice.md
git commit -m "공지사항 파일 추가"
이제 구조가 이렇게 됩니다.
main: C1 ── C2 ── C4
\
feature/login: C3
main도 C4로 앞으로 나갔고, feature/login도 C3이 있습니다. 두 브랜치가 C2에서 각자 다른 방향으로 갈라진 거예요.
이 상태에서 merge하면 fast-forward가 불가능합니다. 단순히 포인터를 당기는 것만으로는 두 흐름을 합칠 수 없으니까요. Git은 두 브랜치를 하나로 엮는 merge 커밋을 새로 만듭니다.
git switch main
git merge feature/login
Merge made by the 'ort' strategy.
login.md | 1 +
1 file changed, 1 insertion(+)
git lg로 보면 이렇게 됩니다.
* e7f8a9b (HEAD -> main) Merge branch 'feature/login'
|\
| * c1d2e3f (feature/login) 로그인 페이지 초안 추가
* | b3c4d5e 공지사항 파일 추가
|/
* a9b3c12 README에 프로젝트 설명 추가
* 3f2a1b4 첫 번째 커밋: README 추가
|\와 |/로 갈라졌다가 합쳐지는 모양이 보입니다. e7f8a9b가 두 브랜치를 묶어주는 merge 커밋입니다. 부모가 두 개인 특수한 커밋이에요.
충돌(Conflict)이란
여기까지는 각 브랜치가 서로 다른 파일을 건드렸기 때문에 Git이 자동으로 합쳐줬습니다.
문제는 두 브랜치가 같은 파일의 같은 줄을 서로 다르게 수정했을 때입니다. Git이 "어느 쪽이 맞는지 나는 모르겠다"고 판단하면 충돌이 납니다.
충돌 상황을 직접 만들어봅니다.
# main에서 README.md 수정
git switch main
echo "main 브랜치에서 수정한 내용" > README.md
git add README.md
git commit -m "main에서 README 수정"
# feature/login 브랜치에서도 같은 파일 수정
git switch feature/login
echo "feature 브랜치에서 수정한 내용" > README.md
git add README.md
git commit -m "feature에서 README 수정"
# main으로 돌아와서 merge 시도
git switch main
git merge feature/login
Auto-merging README.md
CONFLICT (content): Merge conflict in README.md
Automatic merge failed; fix conflicts and then commit the result.
CONFLICT가 났습니다. merge가 중단된 상태입니다.
충돌 마커 읽는 법
충돌이 난 파일을 열어보면 이런 내용이 들어있습니다.
<<<<<<< HEAD
main 브랜치에서 수정한 내용
=======
feature 브랜치에서 수정한 내용
>>>>>>> feature/login
세 가지 기호가 각각 영역을 나눕니다.
<<<<<<< HEAD~=======: 현재 브랜치(main)의 내용=======~>>>>>>> feature/login: 합치려는 브랜치(feature/login)의 내용
Git이 "여기 둘 중에 뭘 쓸지 결정해줘"라고 표시해둔 겁니다.
충돌 해결하기
해결 방법은 단순합니다. 파일을 직접 열어서 원하는 내용으로 고치고, 충돌 마커를 전부 지우면 됩니다.
예를 들어 두 내용을 모두 남기기로 결정했다면 이렇게 수정합니다.
main 브랜치에서 수정한 내용
feature 브랜치에서 수정한 내용
<<<<<<<, =======, >>>>>>> 세 줄이 남아있으면 안 됩니다. 이 마커들은 결과물이 아니라 Git이 임시로 표시해둔 안내선이니까요.
수정이 끝나면 add하고 commit합니다.
git add README.md
git commit
커밋 메시지 입력창이 열립니다. 기본으로 Merge branch 'feature/login'이 채워져 있는데, 그대로 저장해도 됩니다.
충돌 중에 현재 상태 확인하기
충돌이 난 상태에서 git status를 치면 어느 파일이 문제인지 알 수 있습니다.
git status
On branch main
You have unmerged paths.
(fix conflicts and run "git commit")
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: README.md
no changes added to commit
both modified가 충돌난 파일입니다. 여러 파일에서 동시에 충돌이 나는 경우도 있는데, 이 목록을 보면서 하나씩 해결하면 됩니다.
merge를 취소하고 싶다면
충돌을 해결하다가 일단 원상복구하고 싶을 때가 있습니다. 이럴 때는 --abort를 씁니다.
git merge --abort
merge 시작 전 상태로 돌아갑니다. 충돌 마커도 전부 사라집니다.
fast-forward와 merge commit, 어느 게 낫나요?
사람마다 의견이 좀 다른 주제입니다.
fast-forward를 선호하는 쪽 — 히스토리가 깔끔하게 일직선으로 이어집니다. 나중에 git log로 볼 때 단순해서 읽기 편합니다.
merge commit을 선호하는 쪽 — "이 기능이 언제 합쳐졌는지"가 커밋 기록에 남습니다. 브랜치 단위로 작업 흔적이 보존되기 때문에 나중에 추적하기 쉽습니다.
fast-forward 상황에서도 merge commit을 강제로 만들고 싶다면 --no-ff 옵션을 씁니다.
git merge --no-ff feature/login
어느 쪽이 정답이라기보다는 팀의 규칙에 따르는 편이 좋습니다. 혼자 작업할 때는 취향 차이입니다.
이번 편 정리
| 상황 | Git의 동작 |
|---|---|
| 합치려는 브랜치가 현재 브랜치의 직계 자손 | Fast-forward — 포인터만 이동, 새 커밋 없음 |
| 두 브랜치가 각자 다른 커밋을 가짐 | Merge commit 생성 — 부모가 두 개인 새 커밋 |
| 같은 파일의 같은 줄을 서로 다르게 수정 | Conflict — 사람이 직접 해결해야 함 |
충돌 해결 순서
- 충돌난 파일 열기
<<<<<<<,=======,>>>>>>>마커 확인- 원하는 내용으로 직접 편집, 마커 모두 제거
git add <파일>git commit
명령어 모음
| 명령어 | 하는 일 |
|---|---|
git merge <브랜치> |
현재 브랜치에 지정 브랜치를 합치기 |
git merge --no-ff <브랜치> |
fast-forward 상황에서도 merge commit 생성 |
git merge --abort |
진행 중인 merge 취소, 원상복구 |
다음 편 예고
merge를 배웠으니 이제 로컬에서 만든 걸 GitHub 같은 원격 저장소에 올리고 싶어집니다. 다음 편에서는 git remote, push, pull, fetch를 다룹니다. 원격 저장소와 로컬이 어떻게 연결되는지, pull이 사실 두 동작의 합성이라는 것도 알아봅니다.
이 시리즈의 다른 글
- [기초편 1편] git init과 첫 커밋까지
- [기초편 2편] 브랜치, 어렵지 않아요
- [기초편 3편] git log를 트리로 보는 법
- [기초편 5편] 원격 저장소 연결하기
'블로그, 컴퓨터 > DevOps' 카테고리의 다른 글
| Git 기초편 6편 — .gitignore 제대로 쓰기 (0) | 2026.05.11 |
|---|---|
| Git 기초편 5편 — 원격 저장소 연결하기 (0) | 2026.05.11 |
| Git 기초편 3편 — git log를 트리로 보는 법 (1) | 2026.05.10 |
| Git 기초편 2편 — 브랜치, 어렵지 않아요 (0) | 2026.05.09 |
| Git 기초편 1편 — git init과 첫 커밋까지 (0) | 2026.05.09 |