스쿼시 머지 vs 머지 커밋: 1인 개발자의 PR 전략
시리즈 CLAUDE.md 규칙으로 보는 PHP 백엔드 기본기 현재 위치 10 / 15
- 직접 JWT 구현 금지! 검증된 라이브러리와 안전 설계
- CodeIgniter 4 + 모던 PHP 8.4: readonly DTO, backed enum, match로 배열·define() 걷어내기
- Redis 캐시 TTL 설계와 N+1 방지 실전 가이드
- CI4에서 얇은 컨트롤러와 서비스 기반 트랜잭션 관리법
- OpenAPI를 PHP 어트리뷰트로: $ref 재사용 컴포넌트와 공통 ErrorResponse
- DB는 UTC, 화면은 KST: TIMESTAMP + 세션 타임존으로 변환 코드 없애기
- 프론트 로그를 큐로 받는 수집 파이프라인: 즉시 202 → Redis → 워커 → 파일 + DB, 실패 시 dead-letter
- 서비스를 쪼갤까? MSA 선택을 위한 실전 체크리스트
- MSA 공통 에러 규약, RFC 9457로 정하기
- 스쿼시 머지 vs 머지 커밋: 1인 개발자의 PR 전략
- "난이도가 높아서 오래 걸리는 거야?" — AIFid 개발 기록으로 따져 본 소요 시간
- CLAUDE.md로 팀 규칙을 AI에게 가르치는 법
- CI4 브랜드 에러 페이지: 설계서 한 장에서 구현까지
- 870줄 CLAUDE.md를 116줄로 줄인 실전 리팩터링
- Claude Code 토큰 절감 실전: 3개월 로그 분석과 5단계 전략
시리즈 Claude Code 워크플로 현재 위치 14 / 29
- git worktree로 AI 세션 여러 개를 동시에 돌리기: 브랜치 충돌과 stash 공유 사고를 피하는 운영 규칙
- AI에게 기억을 심는 법: 메모리 파일로 삽질 교훈을 세션 간에 유지하기
- 스킬로 반복 작업 자동화: 이슈 착수부터 종료까지 한 명령으로 묶은 dev-issue 사례
- 코드 그래프(graft)로 AI의 탐색 비용 줄이기: grep/read 대비 토큰 절감 체감
- AI 코드 리뷰를 규칙으로 고정하기
- 반복되는 Git 워크플로우와 브랜치 정리 자동화
- 같은 요청을 여러 저장소에 반복하는 법: 한 줄 프롬프트 + 기준 문서 하나
- 이슈 하나를 한 줄로 개발 끝내는 법 — 내가 가장 많이 쓰는 Claude Code 스킬들
- 블로그 글을 스무 번 부탁하고 나서, 스킬로 만들었다
- 마라톤 세션이 토큰을 먹는 구조: 로그로 찾고, /compact와 서브에이전트로 줄이기
- 1인 개발자가 클로드코드로 SaaS 11개 만든 기록
- 이슈 하나에 30~60분, 어디서 새고 있었나: skill-creator로 dev-issue 스킬을 만든 과정
- 도면을 AI에게 읽히려다 AI를 뺀 이야기
- 스쿼시 머지 vs 머지 커밋: 1인 개발자의 PR 전략
- "난이도가 높아서 오래 걸리는 거야?" — AIFid 개발 기록으로 따져 본 소요 시간
- 엑셀 간편장부를 웹 ERP로 옮기기 (AICassa)
- Impeccable 스킬 적용한 게시글 상세 화면 구조 개선
- CLAUDE.md로 팀 규칙을 AI에게 가르치는 법
- AI티 나는 디자인 패턴 걷어내기: overused-font 훅 오탐 처리와 접근성 대비 수정
- CI4 브랜드 에러 페이지: 설계서 한 장에서 구현까지
- Claude에 MCP로 디자인 툴 붙이기 — DesignCraft·PhotoCraft 설치부터 명함, 카툰 효과까지
- AI 실수 복구: 메인 체크아웃을 dev 상태로 되돌린 기록
- 870줄 CLAUDE.md를 116줄로 줄인 실전 리팩터링
- AI로 AI 강의안을 만들다 — 실용인문학 5·6강 준비기
- Claude Code 토큰 절감 실전: 3개월 로그 분석과 5단계 전략
- 개발 기록을 재료로 AI가 쓰는 블로그 글 만들기
- Claude Vision으로 영수증·세금계산서 자동 장부 입력 구현
- AI가 만든 실제 실수 4가지 패턴과 교훈
- GitHub 이슈·PR 기반의 월간 작업일지 자동 생성 Claude 스킬 만들기
들어가며
결론부터 말하면, 저는 feature → dev는 스쿼시 머지로, dev → main은 머지 커밋으로 합칩니다. 작업 단위는 커밋 하나로 깔끔하게, 배포 단위는 경계가 보이게 남기기 위해서입니다.
이 글을 쓰게 된 계기는 작은 사고였습니다. 이슈 사이클을 자동화하는 Claude Code 스킬(dev-issue)을 만들던 중, 브랜치 정리 단계에서 AI가 git branch --merged를 썼습니다. 그런데 분명히 머지한 브랜치가 목록에 나오지 않았습니다. 원인은 스쿼시 머지였습니다.
GitHub의 머지 버튼은 클릭 한 번이지만, 어떤 방식을 고르느냐에 따라 이력과 도구의 동작이 달라집니다. 이 글에서는 두 방식의 차이를 제 PR 환경에 맞춰 정리합니다.
내 PR 환경
혼자 여러 SaaS 제품을 만들지만, 작업은 모두 이슈와 PR을 거칩니다. 리뷰어는 없지만, PR은 CI 게이트이자 작업 기록입니다.
|
항목 |
내용 |
|---|---|
|
개발 형태 |
1인 개발, CodeIgniter 4 + PHP 8.4 |
|
브랜치 |
|
|
작업 단위 |
이슈 1개 = feature 브랜치 1개 = PR 1개 |
|
CI |
PR마다 PHPUnit, PHPStan. Ubuntu 서버의 셀프호스티드 러너(Docker 3대) |
|
자동화 |
Claude Code 스킬이 브랜치 생성부터 PR, 머지, 이슈 종료, 브랜치 정리까지 처리 |
|
병렬 작업 |
git worktree로 여러 세션이 다른 이슈를 동시에 진행 |
이 환경의 특징은 PR이 많고 작다는 점입니다. AI가 TDD로 작업하면 feature 브랜치에 "테스트 추가", "수정", "PHPStan 오류 수정" 같은 중간 커밋이 쌓입니다. 이 커밋들을 어떻게 합칠지가 이 글의 주제입니다.
머지 커밋: 이력을 그대로 보존한다
머지 커밋(Create a merge commit)은 feature의 커밋을 모두 그대로 두고, 부모가 둘인 머지 커밋을 하나 더 만듭니다. git merge --no-ff와 같습니다.
* M Merge pull request #42 from feature/42-order-cancel
|\
| * c3 fix: PHPStan 오류 수정
| * c2 feat: 주문 취소 로직 구현
| * c1 test: 주문 취소 테스트 추가
|/
* b (dev) 이전 작업
장점
- 작업 과정이 커밋 단위로 남아, 나중에 "왜 이렇게 했나"를 추적하기 쉽습니다.
- feature 브랜치의 커밋이 대상 브랜치에 그대로 들어가므로
git branch --merged가 정상 동작합니다. - 머지 커밋 하나만
git revert -m 1하면 기능 전체를 되돌릴 수 있습니다. - 같은 커밋을 공유하므로, 이후 같은 브랜치끼리 다시 합칠 때 충돌이 없습니다.
단점
- "오타 수정", "다시 시도" 같은 중간 커밋까지 모두 남습니다.
- PR이 많아지면 그래프가 갈래갈래 얽혀
git log가 읽기 어렵습니다. - 중간 커밋 중 테스트가 깨진 상태가 있으면
git bisect가 그곳에서 걸립니다.
스쿼시 머지: 작업 하나를 커밋 하나로
스쿼시 머지(Squash and merge)는 feature의 변경 내용을 모두 합쳐 새 커밋 하나를 대상 브랜치에 올립니다. 부모는 하나뿐이고, feature의 원래 커밋은 대상 브랜치 이력에 들어가지 않습니다. git merge --squash 후 커밋하는 것과 같습니다.
* S feat: 주문 취소 API 추가 (#42)
* b (dev) 이전 작업
# feature/42-order-cancel 의 c1, c2, c3 는 dev 에 없다
장점
- dev 이력이 "이슈 하나 = 커밋 하나"로 일직선입니다.
git log --oneline이 곧 변경 이력서가 됩니다. - AI가 만든 중간 커밋을 정리할 필요가 없습니다. 더러운 과정은 PR 페이지에만 남습니다.
- dev의 모든 커밋이 CI를 통과한 상태라
git bisect가 깔끔하게 돕니다. - 커밋 메시지에 PR 번호가 붙어, 커밋에서 PR과 이슈로 바로 돌아갈 수 있습니다.
git revert <커밋>하나로 기능 전체를 되돌립니다.
단점
- 커밋 해시가 달라져 Git은 feature 브랜치가 머지됐다는 사실을 모릅니다.
git branch --merged가 통하지 않습니다. - 머지한 feature 브랜치에서 작업을 이어가면, 다음 PR에 이미 들어간 변경이 다시 보이고 충돌이 납니다.
- PR이 크면 커밋 하나가 너무 커져 세부 이력을 잃습니다.
한눈에 비교
두 방식의 차이는 결국 "feature의 커밋이 대상 브랜치에 남느냐"에서 나옵니다.
|
항목 |
머지 커밋 |
스쿼시 머지 |
|---|---|---|
|
대상 브랜치에 남는 커밋 |
feature 커밋 전부 + 머지 커밋 1개 |
새 커밋 1개 |
|
이력 모양 |
갈래가 나뉘고 합쳐짐 |
일직선 |
|
중간 커밋 |
남음 |
PR 페이지에만 남음 |
|
|
동작함 |
동작 안 함 |
|
기능 되돌리기 |
|
|
|
|
중간 커밋에서 걸릴 수 있음 |
깔끔함 |
|
머지 후 같은 브랜치 재사용 |
가능 |
하면 안 됨 |
|
잘 맞는 곳 |
오래 사는 브랜치끼리 (dev → main) |
짧게 사는 브랜치 (feature → dev) |
GitHub에는 세 번째 선택지인 Rebase and merge도 있습니다. 이력은 일직선이지만 중간 커밋이 그대로 남고 해시도 바뀝니다. 커밋을 손수 다듬는 팀에 맞고, AI가 커밋을 쌓는 제 환경에는 맞지 않아 제외했습니다.
내 환경에서의 선택
하나만 고를 필요는 없습니다. 브랜치의 수명에 따라 나눠 씁니다.

feature → dev: 스쿼시 머지. feature 브랜치는 이슈 하나를 처리하고 사라집니다. 그 안의 중간 커밋은 AI의 작업 과정일 뿐, dev 이력에 남길 가치가 낮습니다. 스쿼시하면 dev는 "이슈 번호 + 한 줄 요약"의 목록이 됩니다. 과정이 궁금하면 커밋 메시지의 PR 번호를 따라가면 됩니다.
dev → main: 머지 커밋. dev와 main은 계속 살아 있는 브랜치입니다. 여기서 스쿼시하면 main에는 dev와 다른 해시의 커밋이 생깁니다. 다음 릴리스 PR에서 이미 배포한 변경이 다시 보이고, 결국 충돌이 납니다. 머지 커밋으로 합치면 두 브랜치가 같은 커밋을 공유하고, main의 머지 커밋 하나하나가 릴리스 기록이 됩니다.
정리하면, 짧게 사는 브랜치는 스쿼시하고, 오래 사는 브랜치끼리는 머지 커밋으로 합칩니다.
스쿼시 머지의 함정과 대응
스쿼시 머지를 쓰면 Git은 "이 브랜치가 머지됐다"는 사실을 모릅니다. 아래 세 가지는 모두 여기서 나옵니다.
1. git branch --merged로 브랜치를 정리할 수 없다
feature의 커밋이 dev에 없으니, 머지한 브랜치도 "안 합쳐진 브랜치"로 나옵니다. 그래서 브랜치가 아니라 PR 상태를 기준으로 판단합니다.
# 머지된 PR의 브랜치만 골라 로컬에서 삭제
for b in $(gh pr list --state merged --base dev --json headRefName -q '.[].headRefName'); do
git branch -D "$b" 2>/dev/null
done
# 원격에서 사라진 브랜치 참조 정리
git fetch --prune
제 prune-branch 스킬에는 "브랜치를 지우기 전에 PR이 실제로 머지됐는지 확인하라"고 적어 두었습니다. AI는 관성적으로 --merged를 쓰려 하므로, 저장소의 머지 방식을 스킬이나 CLAUDE.md에 명시하는 것이 핵심입니다.
2. 머지한 feature 브랜치를 다시 쓰지 않는다
머지 후 같은 브랜치에 커밋을 더하면, 다음 PR에 이미 들어간 변경이 다시 나타납니다. 추가 수정은 항상 최신 dev에서 새 브랜치를 따서 진행합니다. 이슈 하나에 PR이 두 개가 되더라도 그렇게 합니다.
3. dev → main에서는 스쿼시하지 않는다
오래 사는 브랜치끼리 스쿼시하면, main에 생긴 새 커밋을 dev가 모릅니다. 릴리스를 두세 번 반복하면 dev → main PR의 diff가 계속 불어나고 충돌이 쌓입니다. 핫픽스를 main에 직접 넣었다면, main을 dev로 머지 커밋으로 다시 내려 두 브랜치를 맞추어 둡니다.
GitHub 저장소 설정 체크리스트
규칙을 정했으면 사람도 AI도 실수하지 않게 설정으로 못박아 둡니다. Settings → General → Pull Requests와 브랜치 규칙에서 확인합니다.
- Allow merge commits, Allow squash merging 둘 다 켜기. dev → main에 머지 커밋이 필요하므로 스쿼시 전용으로 만들 수 없습니다. Rebase merging은 끕니다.
- 브랜치별 머지 방식 제한. 브랜치 규칙(Rulesets)의 PR 규칙에서 dev는 Squash만, main은 Merge만 허용하도록 지정합니다. 저장소 전체 설정만으로는 브랜치별로 나눌 수 없습니다.
- 스쿼시 커밋 메시지 기본값을 Pull request title로. PR 제목을 Conventional Commits 형식(
feat: 주문 취소 API 추가)으로 쓰면 dev 이력이 그대로 변경 이력서가 됩니다. - Automatically delete head branches 켜기. 머지된 원격 feature 브랜치가 자동으로 지워져 재사용 실수를 막습니다. 단, dev와 main은 보호 규칙으로 삭제를 막아 둡니다.
- main에 Require linear history를 켜지 않기. 이 옵션은 머지 커밋을 막습니다. dev에만 켭니다.
- dev, main 모두 CI 통과 필수. 셀프호스티드 러너의 PHPUnit, PHPStan job을 required check로 지정합니다.
하나 더, Closes #42는 기본 브랜치로 머지될 때만 이슈를 닫습니다. 기본 브랜치가 main이면 dev로 가는 PR은 이슈를 닫지 못합니다. 그래서 제 스킬은 머지 후 이슈를 직접 닫습니다. 기본 브랜치를 dev로 바꾸는 방법도 있습니다.
CLI로 머지할 때는 구간별로 옵션을 나눕니다.
# feature → dev
gh pr merge 42 --squash --delete-branch
# dev → main (릴리스)
gh pr merge 57 --merge
마무리
스쿼시 머지와 머지 커밋은 우열의 문제가 아니라 자리의 문제입니다. 작업의 과정을 숨기고 싶은 곳에는 스쿼시를, 브랜치 사이의 관계를 지켜야 하는 곳에는 머지 커밋을 둡니다.
AI와 함께 개발하면 커밋은 더 많아지고 작업은 더 빨라집니다. 그럴수록 이력은 사람이 읽을 수 있는 단위로 남아야 합니다. 그리고 그 규칙은 제 머릿속이 아니라 저장소 설정과 스킬에 적혀 있어야 AI도 같은 판단을 합니다. git branch --merged 한 줄에서 시작한 사고가 가르쳐 준 것은 결국 그것이었습니다.

댓글 0
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.