현록

Git merge와 rebase 차이

Git 브랜치의 변경을 합칠 때 mergerebase를 자주 사용한다.
두 명령 모두 한 브랜치의 작업을 다른 브랜치 위에 통합할 수 있지만, 만들어지는 커밋 그래프는 다르다.

merge는 갈라졌던 이력을 연결해서 분기 구조를 보존한다.
rebase는 커밋을 새 기준점 위에 다시 적용해서 한 줄에 가까운 이력을 만든다.
어느 쪽이 더 좋은지를 먼저 정하기보다 어떤 이력을 남기고 누구와 공유한 커밋인지부터 판단해야 한다.

출발점이 되는 커밋 그래프

mainfeature가 공통 커밋 B 이후에 갈라진 상태를 보자.

      C---D  main
     /
A---B
     \
      E---F  feature

C, Dmain에만 있고 E, Ffeature에만 있다.
두 브랜치가 가리키는 마지막 커밋은 다르지만 공통 조상은 B다.

브랜치는 커밋 자체를 복사한 별도 폴더가 아니라 특정 커밋을 가리키는 이름이다.
mergerebase의 차이는 이 포인터와 커밋의 부모 관계를 어떻게 바꾸는지에서 드러난다.

merge가 만드는 이력

main으로 이동한 뒤 feature를 merge하면 Git은 두 브랜치의 변경을 통합한다.

git switch main
git merge feature

위처럼 이미 이력이 갈라져 있다면 Git은 공통 조상 B, 현재 브랜치의 D, 합칠 브랜치의 F를 이용해 three-way merge를 수행한다.
자동으로 통합할 수 있으면 결과 snapshot을 담은 merge commit M을 만든다.

      C---D
     /     \
A---B       M  main
     \     /
      E---F  feature

MDF를 부모로 갖는다.
그래서 로그에는 두 작업이 B 이후에 병렬로 진행되었다가 M에서 합쳐졌다는 사실이 남는다.

기존의 C, D, E, F는 다시 만들지 않는다.
브랜치의 실제 분기와 통합 시점을 보존하고 싶을 때 merge가 잘 맞는다.

fast-forward의 예외

git merge가 항상 merge commit을 만드는 것은 아니다.
현재 브랜치가 합칠 브랜치의 조상이라면 이력이 갈라져 있지 않으므로 fast-forward가 가능하다.

A---B  main
     \
      C---D  feature

이 상태에서 mainfeature를 merge하면 Git은 기본적으로 새 커밋을 만들 필요가 없다.
main 포인터를 B에서 D로 앞으로 옮기기만 한다.

A---B---C---D  main, feature

이 동작도 Git에서는 merge의 한 결과지만 별도의 merge commit은 없다.
기능 브랜치가 합쳐진 경계를 커밋으로 남기고 싶다면 --no-ff를 사용한다.

git switch main
git merge --no-ff feature

반대로 fast-forward일 때만 통합하고, 분기된 상태라면 명령을 실패시키고 싶을 때는 --ff-only를 사용한다.

git merge --ff-only feature

따라서 로그에서 merge commit이 보이는지는 merge 명령을 사용했는지만으로 판단할 수 없다.
통합 직전의 그래프와 --ff, --no-ff, --ff-only 정책을 함께 봐야 한다.

rebase가 만드는 이력

같은 출발점에서 feature로 이동해 main을 기준으로 rebase해 보자.

git switch feature
git rebase main

Git은 feature가 공통 조상 B 이후에 만든 변경을 찾는다.
그다음 main의 끝인 D를 새 기준으로 삼고 E, F의 변경을 순서대로 다시 적용한다.

A---B---C---D  main
             \
              E'---F'  feature

여기서 E', F'는 원래 E, F가 자리를 옮긴 것이 아니다.
같은 변경을 새 부모 위에 적용해 만든 새로운 커밋 객체다.

커밋 ID는 파일 snapshot만으로 정해지지 않고 부모 커밋 같은 정보에도 영향을 받는다.
부모가 바뀐 E', F'는 원래 커밋과 다른 ID를 갖는다.
그래서 rebase를 이력 재작성이라고 부른다.

rebase가 끝난 뒤 main에서 feature를 merge하면 fast-forward할 수 있다.

git switch main
git merge --ff-only feature

최종 그래프는 한 줄로 이어진다.

A---B---C---D---E'---F'  main, feature

실제로는 두 작업이 병렬로 진행되었지만 최종 이력은 main 작업 뒤에 feature 작업을 순서대로 한 것처럼 보인다.
이 단순한 로그가 rebase의 장점인 동시에 실제 분기 구조를 남기지 않는다는 특성이다.

결과 snapshot과 이력의 차이

충돌을 같은 방식으로 해결했다면 merge의 마지막 M과 rebase 뒤의 마지막 F'가 가리키는 파일 snapshot은 같을 수 있다.
달라지는 핵심은 최종 파일보다 그 결과에 도달한 부모 관계와 커밋 ID다.

merge는 기존 커밋을 유지하고 필요할 때 부모가 둘 이상인 커밋으로 이력을 연결한다.
rebase는 대상 커밋의 변경을 다른 기준점 위에 재적용해 새로운 커밋 열을 만든다.

이 차이는 git log --graph로 확인하기 쉽다.

git log --graph --oneline --decorate --all

분기와 통합 시점이 중요한 저장소에서는 merge 그래프가 유용한 정보가 된다.
각 기능의 커밋을 한 방향으로 읽는 것이 중요한 저장소에서는 rebase된 그래프가 단순하다.

merge 충돌 처리

두 브랜치가 같은 파일의 같은 부분을 다르게 수정하면 Git이 결과를 자동으로 정하지 못할 수 있다.
merge는 이때 중단되고 충돌 파일과 index를 해결 대기 상태로 남긴다.

git status

파일의 충돌 표시를 확인하고 의도한 최종 내용으로 수정한 뒤 stage에 올린다.

git add path/to/conflicted-file
git merge --continue

모든 충돌을 해결하고 stage에 올리면 merge commit을 완성할 수 있다.
진행 중인 merge를 취소하고 시작 전 상태로 돌아가려면 다음 명령을 사용한다.

git merge --abort

merge 전에 작업 트리를 깨끗하게 만드는 편이 안전하다.
커밋하지 않은 변경이 많으면 충돌 상태와 원래 작업을 구분하기 어려워지고, 상황에 따라 --abort가 시작 전 변경을 완전히 복구하지 못할 수도 있다.

rebase 충돌 처리

rebase도 충돌을 자동으로 없애는 명령은 아니다.
Git은 원래 커밋을 하나씩 새 기준점 위에 재적용하다가 충돌한 커밋에서 멈춘다.

충돌을 수정하고 stage에 올린 뒤 rebase를 계속한다.

git add path/to/conflicted-file
git rebase --continue

여러 커밋이 같은 부분을 단계적으로 수정했다면 rebase 중 충돌을 여러 번 해결할 수 있다.
각 단계에서는 현재 다시 적용 중인 커밋의 의도를 확인해야 한다.

전체 rebase를 취소하려면 다음 명령을 사용한다.

git rebase --abort

git rebase --skip으로 현재 커밋을 건너뛸 수도 있지만, 그 커밋의 변경이 최종 이력에서 빠진다.
단순히 충돌이 번거롭다는 이유로 사용하지 말고 해당 변경이 이미 반영되었거나 정말 불필요한지 확인해야 한다.

공유 브랜치의 안전 원칙

안전 기준은 단순하다.
다른 사람이 내려받아 그 위에서 작업했을 수 있는 커밋은 rebase하지 않는다.

공유된 커밋을 rebase하면 기존 커밋 대신 ID가 다른 새 커밋이 생긴다.
동료의 브랜치는 여전히 예전 커밋을 부모로 삼고 있으므로 같은 변경이 중복되어 보이거나 불필요한 충돌과 merge가 생길 수 있다.

main, release, 여러 명이 함께 쓰는 feature 브랜치처럼 공용 기준이 되는 브랜치에서는 이력을 보존하는 merge가 안전한 기본값이다.
이미 원격에 push한 개인 PR 브랜치도 누군가 그 위에 작업하고 있다면 공유 브랜치로 취급한다.

rebase는 아직 push하지 않은 개인 작업이나, 다른 사람이 기반으로 삼지 않는다고 팀에서 명확히 합의한 브랜치에 사용하는 편이 안전하다.
원격 개인 브랜치의 rebase를 허용하는 팀이라도 강제 push 전에는 리뷰 도구와 협업자의 상태를 확인해야 한다.

git push --force-with-lease는 원격이 예상과 달라졌을 때 덮어쓰기를 거부해 주지만, 공유 이력을 재작성해도 된다는 허가를 만들어 주지는 않는다.
보호 브랜치의 규칙과 팀의 통합 정책이 명령의 편의보다 우선한다.

merge 중심 작업 흐름

분기 구조와 기능 단위의 통합 시점을 남기려면 feature 브랜치를 main에 merge한다.

git fetch origin
git switch main
git merge --ff-only origin/main
git merge --no-ff feature
git push origin main

첫 번째 --ff-only merge는 로컬 main이 원격보다 갈라졌다면 중단되어 실수로 별도 merge commit을 만드는 일을 막는다.
두 번째 --no-ff merge는 feature 경계를 명시적인 merge commit으로 남긴다.

실제 저장소에서는 직접 main에 push하기보다 pull request와 보호 브랜치를 통해 같은 정책을 적용하는 경우가 많다.
중요한 점은 팀이 fast-forward, merge commit, squash 중 어떤 통합 방식을 쓰는지 일관되게 정하는 것이다.

rebase 중심 개인 작업 흐름

공유 전의 개인 feature 브랜치를 최신 main 위로 정리하려면 원격을 fetch한 뒤 rebase한다.

git fetch origin
git switch feature
git rebase origin/main

충돌이 나면 파일을 수정하고 git addgit rebase --continue를 반복한다.
완료한 뒤 테스트를 실행하고 커밋 그래프와 변경 내용을 다시 확인한다.

git log --graph --oneline --decorate --all
git diff origin/main...feature

아직 원격에 올리지 않은 브랜치라면 일반 push로 게시할 수 있다.

git push -u origin feature

이미 게시한 브랜치라면 무조건 강제 push하기 전에 그 브랜치가 실제로 개인 소유인지와 팀이 이력 재작성을 허용하는지 확인해야 한다.
공유 여부를 확신할 수 없다면 merge로 최신 기준 브랜치를 받아들이는 편이 안전하다.

선택 기준

실제 분기와 통합 기록을 보존해야 하면 merge를 선택한다.
여러 사람이 기준으로 삼는 브랜치이거나 merge commit 자체가 리뷰와 배포의 경계라면 특히 잘 맞는다.

아직 공유하지 않은 개인 커밋을 최신 기준 위에 정리하고 선형 로그를 만들고 싶다면 rebase를 선택한다.
작은 커밋을 제출 전에 정돈하는 작업에도 rebase가 유용하지만, 커밋 ID가 바뀐다는 사실을 전제로 해야 한다.

충돌이 생기는지 여부만으로 둘을 고르기는 어렵다.
두 방식 모두 같은 변경 지점에서 충돌할 수 있고, rebase는 커밋별로 충돌이 반복될 수도 있다.

팀에서는 개인 브랜치를 공유 전에 rebase하고 공용 브랜치에는 merge하는 혼합 흐름도 많이 사용한다.
핵심은 개인 이력을 정리하는 단계와 공유 이력을 통합하는 단계를 구분하는 것이다.

정리

merge는 기존 커밋을 유지하고 갈라진 이력을 연결한다.
다만 fast-forward가 가능하면 기본적으로 새 merge commit 없이 브랜치 포인터만 이동할 수 있다.

rebase는 커밋의 변경을 새 기준 위에 다시 적용한다.
부모 관계가 달라지므로 새로운 커밋과 새로운 ID가 만들어지고, 결과 이력은 한 줄에 가까워진다.

개인적이고 아직 공유하지 않은 커밋은 rebase로 정리할 수 있다.
다른 사람이 기반으로 삼을 수 있는 공유 커밋은 재작성하지 않고 merge로 통합하는 것이 안전한 기준이다.

참고 자료

관련 포스트
HTML div와 span 차이 thumbnail
HTML div와 span 차이
HTML div와 span의 차이를 기본 display, 담을 수 있는 내용, 시맨틱 태그, CSS와 JavaScript에서 그룹화하는 기준으로 정리합니다.
HTTP 상태 코드 정리 thumbnail
HTTP 상태 코드 정리
HTTP 상태 코드의 2xx, 3xx, 4xx, 5xx 의미와 200, 201, 204, 301, 304, 400, 401, 403, 404, 500, 502, 503 차이를 정리합니다.
HTML id와 class 차이 thumbnail
HTML id와 class 차이
HTML id와 class의 차이를 문서 내 유일성, 여러 값 사용, CSS 선택자, JavaScript 탐색, fragment 링크와 이름 작성 기준으로 정리합니다.
HTML button과 a 태그 차이 thumbnail
HTML button과 a 태그 차이
HTML button과 a 태그의 차이를 이동과 동작의 의미, href, type 속성, 폼 제출, 키보드 접근성, 잘못된 사용 패턴으로 정리합니다.
HTTP GET과 POST 차이 thumbnail
HTTP GET과 POST 차이
HTTP GET과 POST의 차이를 초보자 기준으로 정리합니다. 조회와 변경, query string과 request body, 캐시, safe, idempotent, form과 fetch 사용 기준을 함께 봅니다.
npm outdated와 npm update 차이 thumbnail
npm outdated와 npm update 차이
npm outdated와 npm update의 차이를 초보자 기준으로 정리합니다. Current, Wanted, Latest의 의미와 semver 범위 안에서 업데이트되는 방식, package-lock.json 변화까지 함께 봅니다.
npm install -g와 npx 차이 thumbnail
npm install -g와 npx 차이
npm install -g와 npx의 차이를 초보자 기준으로 정리합니다. 전역 설치, 로컬 설치, 일회성 실행, 프로젝트 scripts에 넣는 기준까지 함께 봅니다.
package.json에서 ^와 ~ 차이 thumbnail
package.json에서 ^와 ~ 차이
package.json 의존성 버전 앞에 붙는 ^와 ~의 차이를 초보자 기준으로 정리합니다. semantic versioning, 버전 범위, 0.x 예외, package-lock.json과의 관계까지 함께 봅니다.
npx와 npm exec 차이 thumbnail
npx와 npm exec 차이
npx와 npm exec가 어떤 명령어인지 초보자 기준으로 정리합니다. 로컬 패키지 실행, 원격 패키지 임시 실행, --package 옵션, -- 인자 전달 차이까지 함께 봅니다.
npm scripts와 npm run 정리 thumbnail
npm scripts와 npm run 정리
npm scripts란 무엇인지, package.json scripts와 npm run의 관계를 초보자 기준으로 정리합니다. npm run dev, npm start, npm test, node_modules/.bin, -- 인자 전달 방식까지 함께 봅니다.
dependencies와 devDependencies 차이 thumbnail
dependencies와 devDependencies 차이
package.json의 dependencies와 devDependencies 차이를 초보자 기준으로 정리합니다. npm install과 npm install -D, 배포 환경 설치, package-lock.json과의 관계까지 함께 봅니다.
npm ci와 npm install 차이 thumbnail
npm ci와 npm install 차이
npm ci와 npm install의 차이를 초보자 기준으로 정리합니다. clean install의 의미, package-lock.json 조건, CI에서 npm ci를 쓰는 이유, .npmrc 플래그 주의점까지 함께 봅니다.
.npmrc 파일이란? thumbnail
.npmrc 파일이란?
storybook을 사용해보려고 하다 마주한 이슈의 해결법을 알아보다가 등장한 .npmrc라는 파일에 대해 공부해보았다. .npmrc 파일이란? .npmrc 파일은 npm에 대한 config 파일이다. (npm에 대한 rc 파일) 프로젝트별 registry, install 옵션, 인증 토큰처럼 npm CLI가 읽는 설정을 관리할 때 사용한다.
스토리북이란? thumbnail
스토리북이란?
Storybook은 UI 컴포넌트를 독립적으로 개발하고, 문서화하고, 테스트하기 위한 프론트엔드 워크샵입니다. Storybook 10.4 기준 설치 흐름과 stories, 문서화, 테스트 활용 방식을 정리합니다.
TTV와 TTI 차이 thumbnail
TTV와 TTI 차이
TTV와 TTI의 차이를 초보자 기준으로 정리합니다. 사용자가 화면을 보는 시점, 상호작용 가능한 시점, FCP, LCP, INP, Core Web Vitals와의 관계까지 함께 봅니다.
Maria DB 외부 접속 설정하기 thumbnail
Maria DB 외부 접속 설정하기
안녕하세요. 오늘은 Maria DB 초기 세팅 시, 외부에서 접속이 안될 때 매뉴얼을 작성해보겠습니다. dotenv 패키지를 통해서 환경변수로 관리한다면, 별도의 추가 작업을 할 일이 없으실 겁니다.
package-lock.json 파일의 역할 thumbnail
package-lock.json 파일의 역할
안녕하세요. 오늘은 node 환경의 개발자라면 한번쯤 궁금했을만한 package-lock.json의 역할에 대해 알아보겠습니다. 우리는 node 환경에서 개발을 할 때 다양한 패키지들을 설치하여 활용하곤 합니다. 우리가 설치하는 패키지 또한 다른 npm 패키지를 활용하여 만든 패키지들이고 이들 또한 설치를 하게 됩니다. 이렇게 직간접적으로 설치된 패키지들은 대부분 호환성을 "^1.1.5"와 같이 표현하여, 범위로 지정해두고 있습니다.
Linux 환경 배포 자동화 체험해보기 thumbnail
Linux 환경 배포 자동화 체험해보기
안녕하세요. 요즘 포트폴리오를 만들면서 서버 상에 자주 반영할 일이 생겼는데, 매번 명령어들을 타이핑하는 것이 비효율적이라 생각이 들어 배포 자동화를 생각해보게 되었습니다. 현재 레벨에서는 단순히 명령어들만 단축시켜도 효율적이라 생각이 들어 간단한 쉘 스크립트만 작성하였습니다. 정말 간단하니 여러분도 도전해보시길 바랍니다.
협업 필수품. Prettier thumbnail
협업 필수품. Prettier
안녕하세요. 오늘은 Prettier이라는 도구에 대해 알려드리고자 합니다. 개발자는 각자의 코딩스타일이 존재합니다. 그러다보니 같은 프로젝트에서도 작성하는 소스마다 스타일이 제각기 다르기 일쑤입니다. 그럴 때 도입하면 좋은 것이 Prettier입니다. 프로젝트 root 폴더에 .prettierrc 라는 파일을 생성한 뒤, 위 예시와 같이 원하는 옵션을 JSON 형식으로 작성해주면 됩니다.