왜 세밀한 staging이 중요한가
잘 만들어진 Git 이력은 하나의 이야기를 들려줍니다. 각 commit은 단일 논리적 변경을 나타냅니다: 버그 수정, 리팩토링, 새 기능. 하지만 실제 개발은 그렇게 깔끔하지 않습니다. 버그를 수정하고, 근처의 오타를 발견하고, 포매팅을 조정하고, 작은 리팩토링을 시작합니다 -- 모두 같은 파일에서. 파일 전체를 한 번에 staging하고 commit하면, 관련 없는 변경사항이 섞인 commit이 됩니다. 이는 코드 리뷰를 어렵게 만들고, bisect로 버그를 추적하기 어렵게 만들며, commit 이력의 문서로서의 유용성을 떨어뜨립니다.
세밀한 staging을 사용하면 같은 파일 내에서 변경사항이 뒤섞여 있더라도 의미 있는 commit으로 작업을 나눌 수 있습니다. 모든 것을 한꺼번에 commit하는 대신, 어떤 변경사항이 함께 속하는지 정확히 선택할 수 있습니다.
Staging의 세 가지 수준
파일 수준 staging
가장 기본적인 수준입니다. git add로 전체 파일을 staging합니다:
git add src/auth.ts src/utils.ts
각 파일에 하나의 논리적 변경만 포함되어 있을 때는 괜찮습니다. 하지만 파일에 여러 관련 없는 수정이 포함되면, 파일 수준 staging은 너무 거칩니다.
Hunk 수준 staging
Git은 파일의 변경사항을 "hunk"으로 나눌 수 있습니다 -- 변경되지 않은 코드로 구분된 수정된 줄의 연속 블록입니다. git add -p(패치 모드)를 사용하면, Git이 각 hunk를 하나씩 표시하고 staging할지 묻습니다:
git add -p src/auth.ts
각 hunk에 대해 y(staging), n(건너뛰기), s(더 작은 hunk로 분할), 또는 e(수동 편집)를 선택합니다. 이는 파일 수준 staging에 비해 큰 개선이지만 한계가 있습니다. Hunk는 근접성으로 정의됩니다: 관련 없는 두 변경사항이 가까이 있으면(3줄 미만의 컨텍스트로 구분), Git은 이를 하나의 hunk로 묶습니다. 이를 분리하려면 수동 패치 편집이 필요하며, 이는 번거롭고 오류가 발생하기 쉽습니다.
줄 수준 staging
줄 수준 staging은 가장 정밀한 옵션입니다. 파일이나 hunk 대신 staging할 개별 줄을 선택합니다. 12번째와 15번째 줄은 staging하고 싶지만 14번째 줄은 아닌 경우? 가능합니다. 파일 내에서 변경사항이 얼마나 가깝든 각 commit에 무엇이 들어갈지 완전한 통제권을 갖게 됩니다.
커맨드라인에서 줄 수준 staging은 git add -p와 e 옵션으로 패치를 수동으로 편집하는 것을 의미합니다. unified diff 형식을 이해하고, 패치를 신중하게 수정하며, 오류를 도입하지 않기를 바라야 합니다. 작동하지만, 대부분의 개발자가 정기적으로 하고 싶어하는 것은 아닙니다.
GitSquid에서의 줄 수준 staging
GitSquid는 줄 수준 staging을 시각적이고 간단하게 만듭니다. 수정된 파일을 선택하면 diff 뷰어가 변경된 모든 줄을 보여줍니다. 특정 줄을 staging하려면:
- 단일 줄을 클릭하여 선택합니다.
- Cmd+Click(Windows/Linux에서는 Ctrl+Click)으로 여러 개별 줄을 선택합니다.
- Shift+Click으로 줄 범위를 선택합니다.
원하는 줄을 선택한 후 staging합니다. 해당 줄만 인덱스에 추가됩니다. 나머지는 작업 디렉토리에 staging되지 않은 수정으로 남아 다른 commit에 포함될 준비가 됩니다.
같은 워크플로우가 반대로도 작동합니다. 이미 staging한 줄을 staging 영역에서 제거하고 싶다면, staging된 diff에서 해당 줄을 선택하고 그 줄만 staging 해제할 수 있습니다.
내부 동작 방식
개별 줄을 staging하면, GitSquid는 서버 측에서 선택된 변경사항만 포함하는 패치를 생성합니다. 이 패치는 Git의 패치 메커니즘을 사용하여 인덱스에 적용됩니다. 클라이언트 측에서 diff를 조작하는 대신 서버 측에서 패치 생성을 처리하므로, 결과는 신뢰할 수 있으며 Git이 기대하는 것과 정확히 일치합니다. 패치를 수동으로 편집할 때 만날 수 있는 형식 문제나 오프바이원 오류가 없습니다.
실용적인 예시
버그 수정과 리팩토링 분리하기
리팩토링 작업 중 버그를 발견했습니다. 수정이 명확하므로 즉시 수정합니다. 이제 파일에는 두 가지 유형의 변경사항이 있습니다: 리팩토링과 버그 수정. 줄 수준 staging을 사용하면 버그 수정 줄만 선택하고, "fix null check in auth validation"과 같은 명확한 메시지로 commit한 다음, 리팩토링을 별도로 commit합니다. 결과는 하나의 혼란스러운 commit 대신 두 개의 깔끔하고 리뷰하기 쉬운 commit입니다.
디버그 코드와 프로덕션 코드 분리하기
디버깅하면서 실제 코드 변경 바로 옆에 console.log 문을 추가했습니다. 어떤 줄이 디버그이고 어떤 줄이 실제인지 기억하려고 노력하는 대신, 시각적으로 프로덕션 변경사항만 선택하고 staging하고 commit합니다. 디버그 줄은 작업 디렉토리에 남아 계속 사용하거나 나중에 폐기할 수 있습니다.
리뷰를 위한 부분 commit 준비하기
많은 변경사항이 있지만 일부 작업만 리뷰 준비가 되었습니다. 줄 수준 staging을 사용하면 완성된 부분을 commit하면서 진행 중인 코드는 commit되지 않은 상태로 유지할 수 있습니다. Pull request는 집중적으로 유지되고, 리뷰어는 불완전한 코드를 헤쳐나갈 필요가 없습니다.
Commit을 원자적으로 유지하기
원자적 commit은 단일하고 완전한 논리적 변경을 포함하는 것입니다. 테스트를 통과하고, 빌드를 깨뜨리지 않으며, 독립적으로 이해할 수 있습니다. 원자적 commit은 단순한 정리 정돈의 문제가 아닙니다. 실용적인 이점이 있습니다:
- 코드 리뷰. 리뷰어가 각 commit을 독립적으로 이해할 수 있습니다. "fix input validation"이라는 제목의 commit이 유효성 검증 로직만 건드린다면 리뷰하기 쉽습니다. "various changes"라는 제목의 commit이 유효성 검증, 스타일링, 설정을 건드린다면 그렇지 않습니다.
- 버그 추적. 무언가가 깨졌을 때,
git bisect가 문제를 도입한 정확한 commit을 찾아낼 수 있습니다. 원자적 commit은 결과를 의미 있게 만듭니다: 버그를 일으킨 특정 변경을 찾지, 관련 없는 수정의 잡동사니가 아닙니다. - Revert. Commit을 revert해야 할 때, 원자적 commit은 깔끔하게 revert할 수 있습니다. 혼합된 commit은 실제로 유지하고 싶었던 변경까지 revert하도록 강제할 수 있습니다.
- 문서로서의 이력. 수개월 후, 누군가가 코드가 존재하는 이유를 이해하기 위해 commit 이력을 읽을 때, 명확한 메시지가 있는 원자적 commit은 유용한 이야기를 들려줍니다.
줄 수준 staging은 원자적 commit을 실용적으로 만드는 도구입니다. 이것 없이는 지저분한 실제 개발에서 깔끔한 commit을 만들려면 규율과 수동 노력이 필요합니다. 이것이 있으면, 프로세스는 함께 속하는 줄을 선택하는 것만큼 간단합니다.
최고의 commit은 정확히 한 가지를 하는 것입니다. 세밀한 staging은 작업 디렉토리가 다른 이야기를 하고 있더라도 그것을 실현할 수 있는 통제력을 제공합니다.