> ## Documentation Index
> Fetch the complete documentation index at: https://blog.nvim.me/llms.txt
> Use this file to discover all available pages before exploring further.

# 병합 충돌이 나면 어떻게 해결할까요?

> 같은 줄을 서로 다르게 고쳐 충돌을 만들고, 파일 편집과 스테이징을 거쳐 병합을 완료해요. 별도 시나리오에서 병합 중단도 확인해요.

`main`에서는 인사말을 `hello git`으로, 작업 브랜치에서는 `hello world`로 바꿨어요. 두 작업을 합치려는데 `CONFLICT`가 나와요. 파일을 열어보니 코드 사이에 `<<<<<<<` 같은 표시까지 생겼어요. 어느 내용을 남겨야 할까요?

[앞편](https://blog.nvim.me/git/learn/merge-and-commit-graph.md)에서는 서로 다른 파일의 변경을 자동으로 합쳤어요. 이번에는 **Git이 자동으로 합치지 못한 부분을 직접 결정하고 병합을 마무리**해봐요. 충돌이 났다고 기존 커밋이 사라진 것은 아니에요.

## 같은 출발점에서 같은 줄을 다르게 고쳤어요

이번 실습은 공통 조상이 있는 두 로컬 브랜치의 텍스트 병합이에요. 세 시점의 `index.js`를 비교해봐요.

| 시점 | 파일 내용 |
| - | - |
| 공통 조상 | `console.log("hello");` |
| 현재 브랜치 `main` | `console.log("hello git");` |
| 합칠 브랜치 `feature/world` | `console.log("hello world");` |

양쪽이 같은 줄에 서로 다른 결과를 기록했어요. Git은 어느 인사말이 요구사항에 맞는지 판단하지 못해요. 병합을 멈추고 사람이 결과를 정하도록 남겨둬요. 같은 파일을 수정했다고 항상 충돌하는 것은 아니에요. 겹치지 않는 부분은 자동으로 합칠 수 있어요. [Pro Git의 병합 충돌 설명](https://git-scm.com/book/en/v2/Git-Branching-Basic-Branching-and-Merging)을 참고해요.

여기서는 **인사말을 한 번 출력하되 두 단어를 모두 남겨 `hello git world`로 만든다**고 정할게요. 충돌 해결은 한쪽을 통째로 고르는 일만 뜻하지 않아요. 양쪽 의도를 반영한 새로운 내용으로 편집할 수도 있어요.

## 별도 연습 저장소를 준비해요

명령은 **Bash** 기준이에요. 준비 명령의 `git init -b main`에는 Git 2.28 이상이 필요해요. 설치와 작성자 설정은 [첫 커밋 실습](https://blog.nvim.me/git/learn/setup-and-first-commit.md)을 참고해요. Node.js와 원격 저장소는 필요하지 않아요.

앞편의 저장소는 이미 병합을 마쳤으므로 그대로 두고 새 저장소를 만들어요. 기존 Git 저장소 밖에서 실행해요. 같은 이름의 폴더가 있어 `mkdir`가 실패하면 멈추고 새 이름을 골라요.

```bash theme={null}
mkdir git-practice-conflicts && cd git-practice-conflicts && git init -b main
```

아래 `>`는 파일 전체를 덮어써요. 방금 만든 빈 연습 폴더에서만 실행해요. 작성자와 충돌 표시 설정은 이 저장소에만 적용해요. 별도 hooks, 서명, merge driver, 자동 충돌 해결 재사용 설정이 없는 환경을 기준으로 해요. `merge.conflictStyle`을 `merge`로 지정해 아래 표시와 맞춰요.

```bash theme={null}
git config --local user.name "Git Learner"
git config --local user.email "learner@example.com"
git config --local merge.conflictStyle merge
printf 'console.log("hello");\n' > index.js
printf '# Git practice\n' > README.md
git add index.js README.md
git commit -m "Add initial script and README"
git switch -c feature/world
printf 'console.log("hello world");\n' > index.js
git add index.js
git commit -m "Change greeting to hello world"
git switch main
printf 'console.log("hello git");\n' > index.js
git add index.js
git commit -m "Change greeting to hello git"
git branch practice/abort
git status
git log --graph --oneline --decorate --all
git remote -v
```

준비가 끝나면 아래 상태예요. `practice/abort`는 뒤에서 중단을 실습하려고 병합 전 `main`에 붙인 이름이에요. 지금 전환하지는 않아요.

| 항목 | 시작 상태 |
| - | - |
| 현재 브랜치 | `main`, 인사말은 `hello git` |
| 합칠 브랜치 | `feature/world`, 인사말은 `hello world` |
| 중단 연습 브랜치 | `practice/abort`, 현재 `main`과 같은 커밋 |
| 커밋 | 첫 커밋에서 두 갈래로 나뉜 총 3개 |
| `README.md` | 모든 브랜치에서 `# Git practice` 한 줄 |
| 작업 트리·인덱스·원격 | 미커밋 변경 없음, 원격 없음 |

커밋 ID는 실행마다 달라질 수 있어요. 로그가 별도 화면으로 열리면 `q`로 나와요.

## 충돌이 나면 무엇이 멈출까요?

현재 브랜치가 `main`이고 `status`가 clean인지 확인한 뒤 실행해요. `--ff`를 명시해 fast-forward만 허용하는 개인 설정과 구분해요.

```bash theme={null}
git merge --ff --no-edit feature/world
```

**이 명령은 의도적으로 실패해요.** 검증 환경에서는 아래 출력이 나왔어요. 안내 문구와 번역은 환경에 따라 다를 수 있어요.

```text theme={null}
Auto-merging index.js
CONFLICT (content): Merge conflict in index.js
Automatic merge failed; fix conflicts and then commit the result.
```

실패를 확인한 뒤 상태와 파일을 읽어요.

```bash theme={null}
git status
git status --short
git diff --name-only --diff-filter=U
cat index.js
git log --oneline -1
git rev-list --count --all
```

`status`에는 `Unmerged paths`와 `both modified: index.js`가 보여요. 짧은 상태 출력은 아래와 같아요. 충돌 중인 두 칸은 평소 스테이징 전후 표시와 구분해서 읽어야 해요. `UU`는 양쪽에서 수정한 파일이 아직 해결되지 않았다는 뜻이에요. [git status의 충돌 상태 표](https://git-scm.com/docs/git-status#_short_format)를 기준으로 해요.

```text theme={null}
UU index.js
```

`--diff-filter=U`는 아직 병합되지 않은 파일을 골라요. 여기서는 `index.js` 하나예요. 마지막 커밋은 여전히 `Change greeting to hello git`이고 총 커밋 수도 `3`이에요. **파일은 병합 도중의 상태로 바뀌었지만, 병합 커밋은 아직 없어요.**

```text title="index.js" theme={null}
<<<<<<< HEAD
console.log("hello git");
=======
console.log("hello world");
>>>>>>> feature/world
```

`<<<<<<< HEAD` 아래부터 `=======` 위까지는 현재 `main` 쪽 내용이에요. 그 아래부터 `>>>>>>> feature/world` 위까지는 합치려는 브랜치 내용이에요. 이 표시는 작업 파일에 들어 있어요. 구분선만 지우고 두 줄을 모두 남기면 인사말을 두 번 출력하는 코드가 돼요.

> [!NOTE]
>
> 여기서 현재 쪽(ours)은 `main`, 상대 쪽(theirs)은 `feature/world`예요. 항상 나와 동료를 가리키는 이름은 아니에요. 특히 rebase에서는 해석이 달라질 수 있으므로 이 병합의 방향을 다른 작업에 그대로 적용하지 않아요.

## 파일 편집, add, 커밋은 서로 다른 단계예요

**1. 최종 파일 내용을 결정하기**

편집기로 `index.js`를 아래 한 줄로 바꾸고 모든 충돌 표시를 지워요. 이 작은 연습 파일에서는 다음 명령으로 전체 내용을 교체해도 같아요. 실제 파일에는 충돌 밖의 코드도 있으므로 파일 전체를 이 예제로 덮어쓰지 않아요.

```bash theme={null}
printf 'console.log("hello git world");\n' > index.js
cat index.js
git diff --check
git status --short
```

`cat`으로 인사말이 한 줄인지 확인해요. `diff --check`에 출력이 없어도 아직 상태는 `UU index.js`예요. 파일을 편집한 것만으로 Git의 미해결 상태가 해제되지는 않아요.

`diff --check`는 변경에 들어간 충돌 표시나 공백 오류를 검사해요. 프로그램의 문법이나 요구사항까지 검사하지는 않아요. 오류가 나오면 고친 뒤 다시 확인해요. [git diff의 검사 옵션](https://git-scm.com/docs/git-diff#Documentation/git-diff.txt---check)을 참고해요.

**2. 해결한 내용을 스테이징하기**

파일 내용이 맞으면 이 파일만 스테이징해요.

```bash theme={null}
git add index.js
git status
git status --short
git diff --name-only --diff-filter=U
git diff --cached --check
git diff --cached -- index.js
git diff -- index.js
```

`status`에는 `All conflicts fixed but you are still merging.`이 보여요. 짧은 출력은 `M  index.js`이고 미해결 파일 목록은 비어 있어요. `diff --cached`에는 `hello git`에서 `hello git world`로 바뀐 내용이 보여요. 마지막 `diff`는 비어 있어야 해요.

`add`는 지금 파일 내용을 인덱스에 담고 이 경로를 해결한 것으로 표시해요. 내용이 올바른지는 사람이 확인해야 해요. 이후 다시 편집했다면 그 변경을 확인하고 다시 `add`해야 커밋에 들어가요. [git add 공식 문서](https://git-scm.com/docs/git-add)를 참고해요.

**3. 병합 커밋으로 마무리하기**

미해결 파일이 없고 스테이징한 내용이 맞으면 병합을 완료해요. 이번에는 메시지 편집기를 열지 않도록 `commit -m`을 써요.

```bash theme={null}
git commit -m "Merge greetings as hello git world"
git status
git show HEAD:index.js
git show --no-patch --format='%h %s%nparents: %p' HEAD
git rev-parse HEAD^1 practice/abort HEAD^2 feature/world
git log --graph --oneline --decorate --all
git rev-list --count --all
```

총 커밋은 `4`개이고 새 커밋에는 부모가 둘 있어요. `rev-parse`의 첫째·둘째 ID, 셋째·넷째 ID가 각각 같아요. 첫 부모는 병합 전 `main`, 둘째 부모는 `feature/world`예요. `main`만 병합 커밋으로 이동하고 두 연습 브랜치는 원래 커밋에 남아요.

`git merge --continue`도 충돌 해결 후 병합을 완료하는 방법이에요. 이 실습에서는 이미 `commit`으로 끝냈으므로 이어 실행하지 않아요. 두 방식의 관계는 [git merge의 충돌 해결 절차](https://git-scm.com/docs/git-merge#_how_to_resolve_conflicts)를 참고해요.

> [!TIP]
>
> 현재 브랜치는 `main`이고 상태는 clean이에요. 커밋에 담긴 인사말은 `hello git world` 한 줄이며 병합 커밋의 부모가 둘이에요. 실제 프로젝트에서는 파일과 diff 확인에 더해 해당 코드의 테스트도 실행해요.

## 해결을 그만두고 병합 전으로 돌아가려면

이번에는 준비 단계에서 남겨둔 `practice/abort`로 전환해 **별도 병합**을 시도해요. 방금 완성한 `main`의 커밋은 그대로 남아요.

```bash theme={null}
git switch practice/abort
git status
cat index.js
git rev-parse HEAD
git merge --ff --no-edit feature/world
```

전환 후 파일은 `hello git`이고 병합 전 상태는 clean이에요. 마지막 명령은 앞과 같은 충돌로 실패해요. 이번에는 해결 커밋을 만들지 않고 중단할 거예요.

> [!WARNING]
>
> `git merge --abort`는 진행 중인 병합을 중단해요. 병합 도중 편집한 해결 내용도 버리므로 필요한 내용은 먼저 별도로 복사해요. 병합 전에 미커밋 변경이 있었다면 항상 복원할 수 있다고 가정하지 않아요. 완료된 병합 커밋을 되돌리는 명령도 아니에요. [git merge의 중단 조건](https://git-scm.com/docs/git-merge#Documentation/git-merge.txt---abort)을 참고해요.

중단이 편집도 되돌리는지 확인하려고 연습 파일을 한 번 고친 뒤 중단해요.

```bash theme={null}
printf 'console.log("temporary resolution");\n' > index.js
cat index.js
git status --short
git merge --abort
git status
cat index.js
git rev-parse HEAD
git diff
git diff --cached
git rev-list --count --all
```

파일은 `hello git`으로 돌아오고 상태는 clean이에요. `HEAD`는 병합 시도 전에 출력한 ID와 같아요. 두 diff는 비어 있고 커밋 수는 `4` 그대로예요. `temporary resolution`은 커밋하지 않았으며 중단하면서 작업 파일에서도 없어졌어요.

마지막으로 완료한 병합이 있는 `main`으로 돌아가요.

```bash theme={null}
git switch main
git status
cat index.js README.md
git diff
git diff --cached
git log --graph --oneline --decorate --all
git remote -v
```

최종 상태는 `main`, clean, 인사말 `hello git world`예요. README는 처음 내용 그대로이고 원격은 없어요. `feature/world`와 `practice/abort`는 남아 있어요.

## 상태를 보고 다음 행동을 골라요

| 관찰한 상태 | 다음 행동 |
| - | - |
| `CONFLICT`와 `Unmerged paths`가 보여요 | 충돌 파일을 읽고 최종 내용을 정해요. 그만두려면 진행 중인 병합을 중단해요 |
| 표시를 지웠는데 `UU`가 남아요 | 파일을 검토한 뒤 `git add`로 해결한 내용을 기록해요 |
| `All conflicts fixed but you are still merging.`이 보여요 | 스테이징한 diff와 파일을 검토하고 병합 커밋을 만들어요 |
| `add` 뒤 다시 파일을 고쳤어요 | `git diff`로 빠진 변경을 읽고 필요하면 다시 `add`해요 |
| 로컬 변경을 덮어쓴다는 이유로 병합이 거절됐어요 | `status`와 두 diff로 미커밋 변경을 확인해요. 충돌 해결 단계에 들어갔다고 단정하지 않아요 |
| 파일 삭제·이름 변경이나 바이너리 충돌이에요 | 모든 충돌에 텍스트 표시가 생기지는 않아요. 이 한 줄 예제를 그대로 적용하지 말고 파일별 의도와 `status`를 먼저 확인해요 |

충돌 해결의 기준은 구분선이 사라졌는지가 아니라 **의도한 파일을 검토하고 그 결과로 병합을 완료했는지**예요. 스테이징한 내용이 헷갈리면 [세 상태 비교](https://blog.nvim.me/git/learn/working-tree-index-and-commit.md)를, 부모 관계가 헷갈리면 [병합과 커밋 그래프](https://blog.nvim.me/git/learn/merge-and-commit-graph.md)를 다시 읽어봐요.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.