전체 검색

글, 태그와 카테고리를 검색하고 바로 이동해요

충돌을 카드 한 장씩 고르게 만들기

꺾쇠 기호가 잔뜩 박힌 파일을 처음 본 사람은 대개 겁을 먹어요. 그걸 "두 버전이 같은 곳을 다르게 고쳤어요, 어느 쪽을 쓸까요?"로 바꾼 이야기입니다.

2026. 07. 29. 10:11:157분 읽기프로젝트/Git GUI
조회 30
갈림길 앞에서 두 장의 카드 중 하나를 고르려는 사람과 뒤에 줄 선 카드들 일러스트

병합 충돌이 나면 파일에 꺾쇠 기호가 잔뜩 박힙니다. 처음 본 사람은 대개 파일이 망가진 줄 알아요.

사실 저도 처음엔 그랬거든요. 저 기호를 지워야 하는지, 지우면 뭐가 사라지는지 아무도 안 알려주잖아요.

이 앱에서 제일 하고 싶었던 게 이거였습니다. 충돌을 사고가 아니라 질문으로 바꾸는 것.

꺾쇠 기호가 실제로 뭔지

먼저 이걸 짚고 가야 뒤 이야기가 됩니다. 충돌 마커는 사실 아주 정직한 표기예요.

<<<<<<< HEAD
const color = "blue";        // 내가 고친 것
=======
const color = "green";       // 상대가 고친 것
>>>>>>> feature/theme

git은 여기서 판단을 포기한 거예요. 같은 줄을 양쪽이 다르게 고쳤으니 둘 중 뭐가 맞는지 모르겠다고, 둘 다 파일에 적어 둔 겁니다.

그러니까 파일이 망가진 게 아니에요. 질문이 파일 안에 적혀 있는 거죠. 문제는 그 질문이 사람이 읽으라고 쓰인 형식이 아니라는 거고요.

사용자가 보는 것실제 의미앱이 해야 할 일
꺾쇠와 등호가 잔뜩두 버전이 같은 자리를 다르게 고침질문 문장으로 바꾸기
파일이 저장이 안 됨아직 해결 표시를 안 함진행 상태를 보여주기
커밋이 안 됨병합이 진행 중인 상태지금 무슨 상태인지 말해주기
되돌리는 법을 모름병합 취소가 가능함빠져나갈 문을 항상 보여주기

기본은 카드 한 장씩

충돌을 한꺼번에 보여주지 않아요. 한 건씩 카드로 냅니다.

카드에는 이렇게 적혀 있어요. "두 버전이 같은 곳을 다르게 고쳤어요. 어느 쪽을 쓸까요?" 그리고 양쪽 코드를 나란히 보여줍니다.

한쪽을 고르면 다음 충돌로 넘어가요. 위에는 진행 표시가 있습니다. 세 곳 중 두 번째, 하는 식으로요.

별것 아닌 것 같은데, 이 진행 표시가 체감을 많이 바꿨어요. 끝이 보이는 일과 안 보이는 일은 완전히 다른 일이거든요.

(충돌 마커만 보고 있으면 이게 세 군데인지 서른 군데인지도 모르잖아요.)

한 건씩 보여주려면 먼저 쪼개야 해요

카드 한 장씩 보여주겠다고 정하고 나니, 그 앞에 할 일이 있더라고요. 충돌을 "건"으로 셀 수 있어야 합니다.

// 파일 하나에서 충돌 덩어리들을 뽑아냅니다
type ConflictHunk = {
  id: string;
  file: string;
  startLine: number;
  ours: string[];    // 내 쪽 줄들
  theirs: string[];  // 상대 쪽 줄들
  base?: string[];   // 공통 조상 (3-way 일 때)
  choice: "ours" | "theirs" | "custom" | null;
};

// 카드 한 장 = 헝크 하나
// 진행 표시는 choice 가 채워진 개수 / 전체 개수

base를 같이 들고 있는 게 은근히 중요했어요. "원래는 이랬는데 양쪽이 이렇게 고쳤어요"까지 보여줄 수 있거든요. 양쪽만 보면 왜 다른지가 안 보입니다.

그리고 choice를 헝크마다 따로 들고 있는 게, 뒤에 나올 "선택을 유지한 채 상세 뷰로 넘어가기"를 가능하게 만든 구조예요. 선택이 파일이 아니라 헝크에 붙어 있으니까요.

그런데 고르기만으로는 부족해요

실제로는 "양쪽을 섞어야 하는" 경우가 많습니다. 내 쪽 변수명에 상대 쪽 로직인 경우 같은 거요.

그래서 카드에서 "자세히 보기"로 넘어가면 결과를 직접 고칠 수 있게 했어요. 그리고 중요한 게 하나 있는데 — 앞에서 고른 선택이 유지된 채로 넘어갑니다.

세 곳 중 두 곳을 카드로 처리하고 나머지 한 곳만 손으로 고치는 게 되는 거죠. 이게 안 되면 사람들은 결국 처음부터 손으로 하게 되고, 그러면 카드는 장식이 됩니다.

그래서 두 화면이 같은 데이터를 봅니다.

// 카드 뷰와 상세 뷰가 같은 헝크 배열을 씁니다
// 카드에서 고른 것 → choice 채움
// 상세에서 직접 고친 것 → choice: "custom" + 결과 텍스트

const resolvedText = hunks.map(h => {
  switch (h.choice) {
    case "ours":   return h.ours;
    case "theirs": return h.theirs;
    case "custom": return h.customLines;
    case null:     return null;   // 아직 안 정함
  }
});

// 하나라도 null 이면 해결 표시를 못 합니다

여기서 흔히 하는 실수가 상세 뷰를 "텍스트 편집기"로 만드는 거예요. 그러면 카드에서 고른 선택을 텍스트로 한 번 굽고 나서 편집하게 되고, 되돌아올 수가 없습니다.

그래서 상세 뷰도 헝크 단위로 편집해요. 카드로 돌아가면 선택이 그대로 남아 있고요. 이게 안 되면 사람들은 결국 처음부터 손으로 하게 되고, 그러면 카드는 장식이 됩니다.

안 하기로 한 것

자동 해결은 안 넣었어요. 이건 명시적으로 범위에서 뺐습니다.

충돌을 앱이 알아서 정리해주면 편하죠. 그런데 그 순간 사용자는 자기 코드에 무슨 일이 일어났는지 모르게 됩니다.

이 앱의 원칙이 "현재 상태를 숨기지 않는다"라서요. 어려운 걸 쉬운 말로 설명하는 것과, 어려운 걸 안 보이게 치우는 건 다른 일이에요.

자동 해결을 안 넣은 게 제일 오래 고민한 결정이에요

정직하게 말하면 여러 번 흔들렸어요. 자동 해결은 만들기도 어렵지 않고, 데모에서 제일 인상적인 기능이거든요.

자동 해결을 넣으면좋아지는 것대신 잃는 것
공백·줄바꿈만 다른 충돌클릭 한 번이 사라짐어떤 판단이 있었는지 사용자가 모름
한쪽만 수정한 충돌대부분 자동 처리됨"대부분"이 언제 아닌지를 사용자가 모름
import 문 정렬 충돌거의 항상 맞음거의가 아닌 경우가 조용히 섞임

오른쪽 열이 전부 같은 말이에요. 사용자가 모른다는 것. 그리고 이 앱을 쓰는 사람은 지금 git을 배우는 중인 사람이거든요.

어려운 걸 쉬운 말로 설명하는 것과, 어려운 걸 안 보이게 치우는 건 다른 일이에요. 앞쪽은 사용자를 남기고, 뒤쪽은 앱에 대한 의존을 남깁니다.

그래서 범위 문서에 "안 하는 것" 항목으로 적어 뒀어요. 나중에 제가 흔들릴 걸 알았거든요.

선택 화면에는 세 층의 설명이 있습니다

한쪽을 고르면 다른 쪽이 결과에서 빠지잖아요. 그건 무서운 일이에요. 그래서 설명을 세 겹으로 깔았습니다.

말해주는 것
상단 안내문무슨 일이 벌어졌고 무엇을 하면 되는지
각 선택지 한 줄이 선택이 코드에서 뜻하는 것
결과 안내고르면 어떻게 되는지, 그리고 안 고른 쪽도 역사에 남는다는 것

세 번째 줄이 제일 중요했어요. "안 고른 쪽도 사라지지 않아요"라는 한 문장이요.

사실 git에서는 당연한 얘기예요. 커밋된 건 안 없어지니까요. 그런데 그걸 모르는 사람에게는 지금 이 클릭이 남의 코드를 지우는 일처럼 느껴집니다.

(아이콘 중심 UI를 만들면서도 이건 문장으로 적었어요. 그림으로 안심시킬 수는 없더라고요.)

막히면 알아서 치워두고 진행

여기에 하나 더 붙였어요. 저장 안 한 변경이 있는 채로 병합이나 브랜치 전환을 시도하면 git이 막습니다.

그럴 때 "먼저 정리하고 오세요"라고 하면 사용자는 방금 하려던 일을 잊어버려요.

그래서 자동으로 치워두고 → 하려던 작업을 하고 → 다시 꺼내옵니다. 꺼내오다 충돌이 나면 그때 다시 같은 카드 흐름으로 안내하고요.

돌아보면 이 앱에서 제일 자주 쓰는 기능이 됐습니다. 저부터 그렇거든요.

치워두고 돌아오는 흐름

이 기능은 사실 흐름이 전부예요. 순서를 틀리면 사용자 변경이 사라질 수 있어서, 단계마다 실패했을 때 어디로 가는지를 먼저 정했습니다.

단계실패하면사용자에게 보이는 것
1. 변경 자동 보관중단하고 원래대로"보관에 실패해서 아무것도 안 했어요"
2. 하려던 작업 수행보관한 것을 즉시 복원"작업이 안 돼서 되돌렸어요"
3. 보관한 것 복원충돌 해결 흐름으로 안내같은 카드 화면
4. 보관 항목 정리남겨 둠"보관함에 아직 남아 있어요"

4번을 남겨 두는 게 포인트예요. 복원이 성공한 것 같아도 바로 지우지 않습니다. 지웠는데 뭔가 잘못됐으면 돌아갈 데가 없으니까요.

돌아보면 이 앱에서 제일 자주 쓰는 기능이 됐어요. 저부터 그렇거든요. 브랜치 옮길 때마다 뭔가 고치다 만 게 항상 있잖아요.

만들고 나서 안 것

충돌 해결을 쉽게 만드는 일의 팔 할은 UI가 아니라 상태를 정확히 아는 것이었어요.

지금 병합 중인지, 어디까지 해결됐는지, 해결 표시가 된 파일은 뭔지, 취소하면 어디로 돌아가는지. 이걸 정확히 알아야 카드 한 장을 그릴 수 있습니다.

쉬운 화면은 대충 만든 화면이 아니라, 정확히 아는 화면이었어요.

그래서 상태를 얼마나 정확히 알아야 하냐면

"쉬운 화면은 정확히 아는 화면"이라고 썼는데, 카드 한 장을 그리기 위해 실제로 알아야 하는 것들을 적어 볼게요.

알아야 하는 것모르면 생기는 일
지금 병합 중인가 · 리베이스 중인가취소 버튼이 엉뚱한 걸 취소함
충돌 파일이 몇 개이고 헝크가 몇 개인가진행 표시가 거짓말을 함
어떤 파일이 해결 표시가 됐는가다 됐는데 커밋 버튼이 안 열림
취소하면 어디로 돌아가는가"되돌리기"가 무섭게 느껴짐
보관함에 방금 넣은 게 있는가복원을 안 하고 화면을 떠남

이걸 전부 git 명령 출력에서 파싱해서 도메인 모델로 만들어 둬요. 화면은 그 모델만 봅니다. 화면이 git을 직접 물어보기 시작하면 상태가 갈라지거든요.

결국 이 편의 결론은 UI 이야기가 아니었어요. 쉬운 화면은 대충 만든 화면이 아니라, 정확히 아는 화면이었습니다.


다음 편은 앱 안에 터미널이 들어온 이야기예요. 워크트리를 진짜로 다룰 줄 아는 git 도구를 못 찾아서 직접 만든 얘기도 같이요.

시리즈 이어읽기

Git GUI, 도구가 되기까지

1 / 5

이야기 나누기

반응을 남기고 새 글 소식을 받아보세요.

댓글0
서로를 존중하는 대화를 남겨주세요.

첫 댓글을 남겨 대화를 시작해보세요.

새 글 구독
새 글이 공개되면 이메일로 알려드려요.