매3개 | Git의 다양한 Merge 방법 (merge, rebase, squash)

github에서 pr을 생성하고 해당 pr들을 merge를 하려고 클릭하면 다양한 방식의 merge 방법이 존재하는 것을 볼 수 있다.

깃허브 merge 선택 화면

각 merge 방법들이 무엇인지 알아보도록 하자.

📋 목차


🫱🏻‍🫲🏼 Git Merge (feat. fast-forward)

git merge는 서로 다른 브랜치의 변경 사항을 하나의 브랜치에 통합하는 명령이다.
이 방식은 두 브랜치의 히스토리를 모두 보존하며, 보통 새로운 merge 커밋을 생성한다.

단, 대상 브랜치(main)가 feature 브랜치의 기반(commit) 상태로 아무런 추가 변경 사항이 없을 경우에는 fast-forward merge 가 발생한다. fast-forward merge는 별도의 merge 커밋 없이 브랜치 포인터만 앞으로 이동시켜 히스토리를 단순하게 유지한다.

✍🏻 커밋 기록

일반 Merge의 경우, feature 브랜치의 모든 커밋이 그대로 남고 새로운 merge 커밋이 추가된다.

반면, fast-forward Merge의 경우는 main 브랜치의 포인터만 이동되므로, merge 커밋이 생성되지 않고 feature 브랜치의 커밋들이 그대로 이어진다.

🪢 커밋 기록 예시

일반 Merge

Merge 전:

A --- B --- C        (main)
      \
      D --- E --- F  (feature)

Merge 후:

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

여기서 M 커밋은 merge 커밋으로, feature 브랜치의 커밋(D, E, F)과 main의 커밋(C)을 모두 포함한다. 따라서 main 브랜치에는 feature 브랜치의 각 커밋이 그대로 기록되어 있다.

fast-forward Merge:

fast-forward merge가 가능할 경우에 옵션을 사용하여 원하는 형태로 merge가 가능하다.

Merge 전:

A --- B --- C   (main)
        \
          D --- E  (feature)

Merge 후:

  • fast-forward 병합 (--no-ff 옵션 미사용):

    A --- B --- C --- D --- E   (main)
    

    위 경우, merge 커밋이 생성되지 않는다.

  • --no-ff 옵션 사용:

    A --- B --- C --- M   (main)
                  /
          D --- E  (feature)
    

    여기서 M 커밋은 feature 브랜치의 변경사항을 포함하는 merge 커밋으로, feature가 병합되었다는 사실을 명확하게 기록한다.

💬 명령어

일반 Merge

git checkout main
git merge feature

위 명령어는 feature 브랜치의 변경 사항을 main 브랜치에 병합하며, fast-forward가 가능한 경우 별도의 merge 커밋 없이 포인터만 이동한다.

강제 Merge (fast-forward 방지)

git checkout main
git merge --no-ff feature

위 명령어는 fast-forward가 가능한 상황에서도 merge 커밋을 강제로 생성하여 병합 기록을 명확하게 남긴다.

📝 실제 커밋 로그 예시

일반 Merge (merge 커밋 생성)

*   3e1d2f0 (HEAD -> main) Merge branch 'feature'
|\
| * 9c8b7a6 (feature) Add feature part 3
| * 8b7a6c5 Add feature part 2
| * 7a6c5b4 Add feature part 1
* | 6d5c4b3 Fix typo in main file
* | 5c4b3a2 Update documentation
* | 4b3a291 Initial commit

fast-forward Merge (merge 커밋 없이 포인터 이동)

* 9c8b7a6 (HEAD -> main, feature) Add feature part 3
* 8b7a6c5 Add feature part 2
* 7a6c5b4 Add feature part 1
* 6d5c4b3 Fix typo in main file
* 5c4b3a2 Update documentation
* 4b3a291 Initial commit

🔖 Git Rebase

git rebase는 feature 브랜치의 커밋들을 대상 브랜치(main)의 최신 커밋 이후로 재배치하는 명령이다.
이를 통해 커밋 히스토리가 선형으로 정리되어 깔끔하게 보인다. 다만, 이미 공유된 커밋에 대해 rebase를 수행하면 충돌이나 협업에 문제가 발생할 수 있으므로 주의하여 사용해야 한다.

✍🏻 커밋 기록

Rebase를 사용하면 feature 브랜치의 커밋들이 새로운 커밋(해시가 변경됨)으로 재작성되어 main 브랜치에 순서대로 기록된다. 이때 merge 커밋은 생성되지 않으며, 커밋 간의 관계가 단순한 선형 구조를 이룬다.

🪢 커밋 기록 예시

Rebase 전:

A --- B --- C        (main)
      \
      D --- E --- F  (feature)

Rebase 후 (feature 브랜치가 main의 최신 커밋 뒤로 재배치됨):

  • feature 브랜치가 main 브랜치의 최신 커밋(C) 뒤로 재배치됨을 강조

    A --- B --- C        (main)
                \
                D' --- E' --- F'  (feature)
    
  • feature의 구조

    A --- B --- C --- D' --- E' --- F'  (feature)
    

    feature 브랜치는 선형 구조를 가지게 된다.

이후 fast-forward 방식으로 main에 병합한다면, main 브랜치는 아래와 같이 된다.

A --- B --- C --- D' --- E' --- F'  (main)

feature 브랜치의 각 커밋(D’, E’, F’)이 하나씩 순서대로 main 브랜치에 들어간다.

rebase and merge시, 충돌 주의

다른 개발자가 로컬에서 아직 rebase 이전의 feature 브랜치를 기반으로 작업하여 추가 커밋을 진행한 상태라고 가정해보자.

rebase된 feature 브랜치(D’, E’, F’)와 기존 로컬 브랜치(D, E, F)의 히스토리 차이로 인해 병합 시 충돌이나 혼란이 발생할 수 있다. feature 브랜치의 커밋들이 rebase를 통해 해시가 변경된 새로운 커밋을 가지기 때문이다.

다른 개발자가 가진 rebase가 실행되지 않은 feature 브랜치에 새로운 커밋을 찍고 rebase된 브랜치(공유됨)에 merge를 실행할 경우 커밋의 해시 값이 다르기 때문에 충돌이 발생한다.

💬 명령어

  • Rebase 수행

    git checkout feature
    git rebase main
    

    위 명령어는 feature 브랜치의 커밋들을 main 브랜치의 최신 커밋 뒤로 재배치한다.

  • 충돌 해결 후 rebase 계속 진행

    git rebase --continue
    

    만약 rebase 과정에서 충돌이 발생하면, 충돌을 해결한 후 위 명령어로 rebase를 이어간다.

  • Rebase 완료 후 main에 통합 (fast-forward)

    git checkout main
    git merge feature
    

    rebase가 완료된 feature 브랜치는 main 브랜치에 fast-forward 방식으로 병합할 수 있다.

📝 실제 커밋 로그 예시

* 9c8b7a6 (HEAD -> main, feature) Add feature part 3 (rebased)
* 8b7a6c5 Add feature part 2 (rebased)
* 7a6c5b4 Add feature part 1 (rebased)
* 6d5c4b3 Fix typo in main file
* 5c4b3a2 Update documentation
* 4b3a291 Initial commit

✊ Git Squash Merge

git squash merge는 feature 브랜치의 여러 커밋들을 하나의 커밋으로 압축하여 대상 브랜치에 병합하는 방식이다.
이 방법을 사용하면 기능 개발 과정의 중간 커밋들을 합쳐서 깔끔한 커밋 이력을 유지할 수 있다. 단, 개별 커밋의 세부 기록은 남지 않으므로, 상세한 변경 이력을 확인하기 어렵다는 단점이 있다.

✍🏻 커밋 기록

Squash Merge를 수행하면, feature 브랜치의 여러 커밋이 하나의 커밋으로 압축되어 main 브랜치에 반영된다. 이 경우, feature 브랜치의 세부적인 커밋 내역은 사라지고, 하나의 통합 커밋으로 나타난다.

🪢 커밋 기록 예시

Squash Merge 전:

A --- B --- C         (main)
      \
      D --- E --- F   (feature)

Squash Merge 후:

A --- B --- C --- S   (main)

여기서 S 커밋은 feature 브랜치의 변경 사항(D, E, F)을 모두 합친 하나의 커밋이다. 따라서 main 브랜치에는 feature 브랜치의 개별 커밋들이 아닌, 하나의 압축된 커밋만 기록된다.

💬 명령어

git checkout main
git merge --squash feature
git commit -m "Squash merge: feature 브랜치의 변경 사항 통합"

위 명령어는 먼저 main 브랜치로 전환한 후 feature 브랜치의 모든 변경 사항을 하나로 합치고, 마지막으로 커밋 메시지를 작성하여 하나의 커밋으로 통합한다.

📝 실제 커밋 로그 예시

* abcdef0 (HEAD -> main) Squash merge: feature 브랜치의 변경 사항 통합
* 6d5c4b3 Fix typo in main file
* 5c4b3a2 Update documentation
* 4b3a291 Initial commit

위와 같이 Git Merge, Rebase, Squash Merge 방식은 각각의 목적과 상황에 따라 사용되며, 커밋 기록 방식에도 차이가 있다. 프로젝트의 관리 방식과 기록의 세부사항 유지 여부에 따라 적절한 방식을 선택하여 사용하면 된다.

📝 정리

병합 방식 설명 장점 단점
git merge 두 브랜치의 변경사항을 병합하며 별도의 merge commit 생성 작업 내역이 명확하게 남음 히스토리가 복잡해질 수 있음
rebase merge 커밋들을 대상 브랜치 위에 재배치하여 선형 히스토리 유지 깔끔하고 추적이 쉬운 히스토리 공유된 커밋에 사용 시 혼란 발생 가능
squash merge 여러 커밋을 하나로 합쳐 이력을 단순하게 정리 커밋 이력이 간결해짐 세부 변경 내역 확인이 어려워짐

📚 참고

매3개 | Git stash와 clean

👛 Git stash

git stash는 현재 작업 중인 변경 사항을 임시로 저장하는 명령어다. 작업 도중 코드 변경 사항이 있지만 바로 커밋할 수 없는 상황에서 유용하게 사용할 수 있다.

언제 사용할까?

  • 다른 브랜치로 이동해야 할 때: 작업 도중 브랜치를 변경해야 하지만 아직 변경 사항을 커밋할 준비가 안 된 경우 stash를 사용한다.
  • 작업 중 코드 백업: 임시로 코드 변경 사항을 저장하고, 나중에 복원하여 이어서 작업할 때 사용한다.
  • 실험적인 코드 작성: 특정 코드 시도를 stash에 저장해두고 이후 필요에 따라 되돌리거나 삭제할 수 있다.

사용 방법

변경 사항 임시 저장

변경 사항이 모두 stash에 저장되고 워킹 디렉터리가 깨끗한 상태가 된다.

git stash

아래는 git stash와 같이 사용할 수 있는 다양한 옵션들이다.

  • --keep-index 옵션
    스테이징된(인덱스된) 변경 사항은 유지하고 워킹 디렉터리 변경만 stash에 저장한다.
    기본적으로 git stash는 스테이징된 변경 사항도 stash에 포함하지만, 이 옵션은 스테이징된 변경 사항을 보존하고 워킹 디렉터리 변경만 저장하는 특징이 있다.

  • --all옵션
    워킹 디렉터리의 모든 변경 사항(추적된 파일 + 추적되지 않은 파일)을 stash에 저장한다.
    기본적으로 git stash는 추적된 파일만 저장하므로, 추가적으로 .gitignore에 포함된 파일이나 추적되지 않은 파일까지 임시 저장하려면 --all 옵션이 필요하다.

  • --include-untracked or -u 옵션
    추적 중이지 않은 파일을 같이 저장한다.
    기본적으로 git stash는 추적 중인 파일만 저장한다.

  • --patch 옵션
    수정된 모든 사항을 저장하지 않는다. 대신 대화형 프롬프트가 뜨며 변경된 데이터 중 저장할 것과 저장하지 않을 것을 지정할 수 있다.

    $ git stash --patch
    diff --git a/lib/simplegit.rb b/lib/simplegit.rb
    index 66d332e..8bb5674 100644
    --- a/lib/simplegit.rb
    +++ b/lib/simplegit.rb
    @@ -16,6 +16,10 @@ class SimpleGit
            return `#{git_cmd} 2>&1`.chomp
          end
        end
    +
    +    def show(treeish = 'master')
    +      command("git show #{treeish}")
    +    end
    
    end
    test
    Stash this hunk [y,n,q,a,d,/,e,?]? y
    
    Saved working directory and index state WIP on master: 1b65b17 added the index file
    

특정 메시지를 추가해 저장

저장된 stash 목록에 설명이 추가되어 구분하기 쉽다.

git stash save "작업 내용 설명 메시지"

저장된 stash 목록 확인

각 stash에 대한 식별 정보가 출력된다.

git stash list

stash 복원

가장 최근 stash를 워킹 디렉터리에 적용한다.

git stash apply
  • --index 옵션
    Staged 상태까지 적용한다.
    기본적으로 stash를 적용할 때 Staged 상태였던 파일을 자동으로 다시 Staged 상태로 만들어 주지 않는다. 따라서 해당 옵션을 사용해야 원래 작업하던 상태로 돌아올 수 있다.

특정 stash 적용

stash 목록에서 특정 인덱스(stash@{번호})에 해당하는 항목을 적용한다.

git stash apply stash@{2}

stash 삭제

특정 stash 항목을 삭제한다.
apply 옵션은 단순히 stash를 적용하는 것뿐이다. stash는 여전히 목록에 남아 있다. git stash drop 명령을 사용하여 해당 stash를 제거한다.

git stash drop stash@{0}

stash 복원 후 제거

stash를 적용한 후 해당 stash를 목록에서 제거한다.

git stash pop

모든 stash 삭제

모든 stash 항목을 삭제한다.

git stash clear

🧹 Git clean

git clean은 Git 저장소에서 추적되지 않은 파일(Untracked files)을 삭제하는 명령어다. 추적되지 않은 파일이란 Git이 관리하지 않는 파일(예: 새로 생성한 파일 또는 빌드 결과물)을 의미한다.

언제 사용할까?

  • 빌드 파일 제거: 빌드 과정에서 생성된 파일을 깔끔하게 제거할 때 사용한다.
  • 불필요한 파일 정리: 프로젝트에 더 이상 필요하지 않은 임시 파일이나 테스트 파일을 제거할 때 유용하다.
  • 코드 환경 초기화: 작업 환경을 깔끔하게 초기화하여 불필요한 파일이 문제를 일으키지 않도록 할 때 사용한다.

사용하는 방법

추적되지 않은 파일 삭제

untracked 파일을 삭제한다.

git clean -f

-f(force) 옵션은 파일 삭제를 강제하는 옵션이다.

기본적으로 git clean-f 옵션 없이 동작하지 않기 때문에 실제로 사용하는 경우가 없다. Git은 실수로 파일을 삭제하는 것을 방지하기 위해 이 명령어를 반드시 강제 옵션과 함께 사용하도록 설계했다.

디렉터리까지 삭제

디렉터리와 파일 모두 삭제한다. 해당 명령 옵션은 하위 디렉토리까지 모두 지워버린다.

git clean -f -d

-d (directory) 옵션은 디렉터리 삭제를 허용한다. 이 옵션이 없으면 파일만 삭제되며 디렉터리는 남는다.

삭제 대상 확인

삭제될 파일 목록만 미리 확인할 수 있다.

git clean -n

-n(dry run) 옵션은 실제로 파일을 삭제하지 않고 어떤 파일이 삭제될지 미리 보여준다.

강제 삭제

.gitignore에 포함된 파일과 디렉터리도 삭제한다.

git clean -f -d -x

-x(exclude .gitignore) 옵션은 .gitignore에 포함된 파일도 강제로 삭제한다.

특정 경로만 삭제

지정된 경로(예: build 폴더)만 정리한다.

git clean -f ./build

🍽️ stash와 clean 비교 정리

구분 git stash git clean
목적 변경 사항 임시 저장 추적되지 않은 파일 삭제
작업 대상 Git이 추적하는 변경 파일 Git이 추적하지 않는 파일
명령어 git stash, git stash pop git clean -f, git clean -df
사용 상황 브랜치 이동 또는 코드 백업 불필요한 파일 제거 및 환경 초기화
영향 변경 사항이 보존됨 삭제된 파일은 복구 불가

📝 정리

  • stash는 변경 사항을 보존하며 나중에 복원.
  • clean은 추적되지 않은 파일을 깔끔하게 삭제하여 작업 환경을 정리하는데 적합.

📚 참고

백준허브 크롬 확장프로그램 커스텀 방법

코딩 테스트를 준비하기 위해서 코테 문제 풀이 인증 스터디에 참가하게 되었다. 여기서 백준허브라는 크롬 확장자를 새롭게 알게 되었다.

예전부터 사용하던 깃허브 코딩테스트 레포지토리에 해당 확장자를 연결하여 사용하는데.. 세상에? 디렉토리 구조랑 파일명, 그리고 리드미가 생성… 되고 있다! 기존에 사용해오던 구조와 너무나도 달랐기 때문에 나는 바로 커스텀을 할 수 있는 방법을 찾게 되었다.

그리고 해당 글은 나 같이 커스텀하고 싶은 사람들을 위해 글을 적어둔다.

🎨 백준허브 크롬 확장 프로그램 커스텀

🚨 커스텀 방법을 알아보기에 앞서 주의할 점

백준허브 코드를 살펴보면 각 문제풀이 사이트(프로그래머스, 백준 등)의 폴더마다 파일명을 동일하지만 내부 함수명이 좀 다르다. (구조도 조금 다른 것 같기도)

따라서, 프로그래머스가 아닌 경우에는 다음 내용을 참고만 하는 것이 좋다. 실제로 나의 경우에도 백준 사이트만 커스텀한 분의 글을 그대로 따라했다가.. 버그가 발생했다.. 이건 아래 글 참고

그리고 꼭! 아래 글을 끝까지 정독하고 참고하기를 추천한다. 아니면.. 버그를 만날 수 있다.. 그러니 진짜 꼭 끝까지 글을 읽고 참고해야 한다!

그러면 이제 어떻게 백준허브를 커스텀 할 수 있는지 차근차근 살펴보자.

백준허브 깃허브 레포지토리를 fork

아래 링크가 백준허브 깃허브 레포지토리이다. 해당 링크로 접속하여 자신의 깃허브에 fork하면 된다.

🔗 GitHub - BaekjoonHub/BaekjoonHub: 백준 자동 푸시 익스텐션(Auto Git Push for BOJ)

로컬에 해당 fork한 레포를 clone하고 열어 주기

원하는 문제풀이 사이트 폴더의 parsing.js로 이동

나의 경우에는 프로그래머스에서 문제를 대부분 풀이하기 때문에, programmers 폴더의 parsing.js를 수정해줬다.

커스텀하고 싶은 내용 수정 | makeData 함수

이동한 파일의 makeData 함수를 살펴보면 쉽게 커스텀을 진행할 수 있다. 아래는 친절하게 주석으로 작성해두신 내용이다.

  - directory : 레포에 기록될 폴더명
  - message : 커밋 메시지
  - fileName : 파일명
  - readme : README.md에 작성할 내용
  - code : 소스코드 내용

makeData 함수에서 변경하고 싶은 부분을 본인의 입맛에 맞게 수정하면 된다.

나의 경우에는 거의 모든 것을 바꿔줬다. directory, message, fileName을 변경해줬는데, Readme.md는 같이 업로드 하지 않을 것이기 때문에 커밋 메세지에 Date(문제 풀이한 날짜 및 시각)가 포함될 수 있도록 수정해줬다.

이때, Date가 출력되는 포맷도 변경해주기 위해서 해당 폴더의 utils 폴더 내부에 있는 getDateString의 return 값도 변경해줬다.

Readme 업로드 막기 | uploadfunctions.js의 upload 함수

나의 경우에는 Readme.md 파일을 업로드하지 않을 예정이라 아래 코드 한 줄을 주석처리 해줬다. 그리고 이 주석은.. 엄청난 파장을 불러오는데..😱

const readme = await git.createBlob(readmeText, ${directory}/README.md); // readme 파일

확장 프로그램 이름 변경 | manifest.json의 name

커스텀한 내용의 확장 프로그램으로 새롭게 크롬에 설치를 해줘야, 커스텀한 내용이 적용된다. 그렇기 때문에 확장 프로그램 이름을 변경해주지 않으면 이게 커스텀한 건지… 안한건지.. 헷갈릴 수 있다. 내가 그랬다

그래서 아래와 같이 manifest.json 파일에서 name 부분을 원하는 이름으로 변경해주면 된다. 나는 뒤에 Custom만 추가로 넣어줬다.

"name": "백준허브(BaekjoonHub) Custom",

🐉 커스텀한 확장 프로그램 크롬에 적용하기

자, 이제 커스텀을 끝냈다. 그러면 이 커스텀한 코드를 어떻게 우리가 크롬에 사용할 수 있는지를 알아보자.

확장 프로그램 관리 페이지에 접속

본인의 크롬 브라우저에 다음과 같이 url을 넣어주면 확장 프로그램 관리 페이지에 바로 접근할 수 있다.

chrome://extensions/

개발자 모드 토글을 클릭

확장 프로그램 적용 방법

압축해제된 확장 프로그램을 로드합니다. 클릭

위 이미지를 참고하여 넘버링된 순서에 맞게 클릭해주면 된다.

clone했던 폴더 업로드

위에서 수정을 진행해줬던 폴더를 업로드 한다. 그럼 끝이다.

확장 프로그램 선택

커스텀 폴더가 잘 적용되었는지 확인하기

아래 이미지와 같이 이미지 하단에 빨간 아이콘이 존재하고 변경했던 확장 프로그램 이름이 잘 반영되어있다면? 커스텀했던 백준허브가 우리의 크롬 확장 프로그램으로 잘 들어온 것이다.

커스텀 확장 프로그램 적용 확인

이후에는 기존에 백준허브를 사용했던 것과 동일하게 원하는 레포지토리를 생성 혹은 연결해주면 끝이다. 사용 방법은 기존 백준허브와 완전히 동일하다.

그럼 어디 커스텀 백준허브가 잘 작동되었는지 확인해보자!

.

.

.

🚨 업로드 되지 않는 버그 발생

… 그렇다. 버그가 발생했다… 간단하게 뚝딱 될 줄 알았는데.. 어… 역시 뭘 하던 버그는 마주칠 수 밖에 없는 문제인 것 같다… 하하..

위에 내가 설명한 대로 수정을 진행하면 아래와 같이 설정한 레포지토리에 정상적으로 커밋되지 않는 문제가 발생한다.

버그1

원인을 찾기 위해서 수정했던 파일 중에 문제를 일으킬 만한 것을 살펴보았지만 아무리 봐도 없었다.. 그러던 중…

문제 원인 발견

서얼마.. readme를 주석처리 해서.. 이게 혹시 원인..?이란 생각이 들어서 주석을 해제하고 시도하니.. 다음과 같이 정상적으로 커밋이 되었다.

커스텀 성공1

커스텀 성공2

♻️ Readme 파일을 업로드하지 않도록 하기 (Retry)

일단 위에서 설명했던 5번의 코드는 주석을 진행해도 된다. 다만, 같이 해줘야 하는 작업이 있다.

treeSHA 상수 수정

아래와 같이 treeSHA의 상수 값을 수정해줘야 한다.

readme 값을 주석처리 했기 때문에 현재 readme라는 상수는 존재하지 않게 되었다. 그런데 treeSHA에서 readme를 사용하려고 하고 있다. 어라라? 존재하지 않는 값을 사용한다..? 역시.. 말이 안되는 행위이다..

따라서 treeSHA에서 readme 값을 제거해준다. 그러면 사라진 readme 라는 상수는 사용되지 않아 여기서 문제가 발생하지 않을 것이다.

// 변경전
const treeSHA = await git.createTree(refSHA, [source, readme]);
// 변경후
const treeSHA = await git.createTree(refSHA, [source]);

const commitSHA = await git.createCommit(commitMessage, treeSHA, refSHA);
await git.updateHead(ref, commitSHA);

+) 추가로 source 값 하나만 존재하니까 배열로 안 감싸도 되지 않을까? 라는 생각을 한 나는.. [source]가 아닌 source로 작성했는데 이러면 안된다.. 확장 프로그램에서는 커밋되었다는 초록색 check 표시가 뜨는데, 페이크이다.. 실제 레포지토리에 가면 커밋되어있지 않다.

updateObjectDataFromPath 주석

여기도 역시 readme라는 상수는 사라졌는데 사용하고 있으니 동일하게 주석처리를 해주면 된다!

/* stats의 값을 갱신합니다. */
updateObjectDatafromPath(stats.submission, `${hook}/${source.path}`, source.sha);
// updateObjectDatafromPath(stats.submission, `${hook}/${readme.path}`, readme.sha);

✌🏻 결과

원하는 디렉토리 형태와 파일명, 그리고 Readme.md 없이 잘 커밋되어있는 것을 확인할 수 있다.

커스텀 결과

실제 커스텀한 백준허브 깃허브 리포지토리

혹시 어떻게 코드를 변경했는지 참고하고 싶다면 아래 링크에 접속하여 참고하면 된다.

참고로 나의 경우에는 기존의 백준허브 코드를 지우지 않고 주석처리 해뒀다. 그래서 다소.. 코드가.. 좀.. 더러울 것이다.

🔗 GitHub - soi-ha/BaekjoonHubCustom: 백준 자동 푸시 익스텐션(Auto Git Push for BOJ) 커스텀 repo

✍🏻 생각 끄적이기

사실 readme 상수를 주석처리 하면서도 음.. 아래에서 사용하고 있는데 이거 문제 생길 것 같은데.. 근데 참고한 글은 냅다 주석해도 되었다는 걸 보면 괜찮겠지? 라는 생각을 가지고 그냥 냅다 따라했다. 그랬다가 나의 1시간을 순삭당했는데..

이번에 정말 온 몸으로 깨달은 것은 무작정 글을 믿고 따라하면 안된다는 것이다. 사람은 사고를 하는 동물 아닌가? 그러니 해당 글은 참고만 하고 생각을 하면서 개발을 해야 하는 것을 잊지 말아야겠다. 이래서.. 생각 없이 무작정 사용하는 것을 지양하고 무언가를 할 때 꼭 어떤 이유에서 이렇게 코드를 작성했는지 생각하라는게.. 이래서구나~ 라는 걸 깨닫게 되는 과정이었다.

그리고 해당 코드가 타입스크립트가 아니라 자바스크립트라.. 이게 생각보다 불편하더라. 해당 값의 형태가.. string인지.. 꼭 object 형태여야 하는 건지.. 이런 것을 알 수가 없었다. 타입추론이 안되니 굉장히 불편하구나를 1년동안 ts만 쓰면서 처음으로 깨달았다.

📚 참고 문서

매3개 | Git 저장소의 구조와 흐름 & 파일 상태 변화

🌿 Git?

Git은 분산 버전 관리 시스템으로, 개발자가 코드를 효율적으로 관리하고 협업할 수 있도록 돕는다. 이런 Git 저장소는 총 4가지로 작업 디렉터리, 스테이징 영역, 지역 저장소, 원격 저장소가 있다.

이 중에서 주요 영역은 작업 디렉터리, 스테이징 영역, 지역 저장소 3가지로 Git의 데이터 흐름을 파악하기 위해 꼭 알아야 한다. 이 3가지 영역은 Git의 주요 데이터 관리 구조로 Git이 추적(관리)하는 파일과 추적하지 않는 파일을 구분하고, 추적하는 파일들의 상태를 구분짓는다.

일단 Git의 데이터 흐름을 알아보기 전에 Git의 저장소 4가지가 어떤 역할을 하는지 알아보자.

🗄️ Git 저장소의 구조

1. 작업 디렉터리(Working Directory)

작업 디렉터리(Working Directory)는 작업 중인 파일들이 실제로 있는 공간으로, 사용자가 직접 수정하고 편집하는 파일이 위치한다.

git init 명령어를 통해 현재 디렉터리(컴퓨터에서 작업 중인 폴더)를 Git 저장소로 초기화한다. 초기화가 완료되면 Git은 변경된 파일을 추적하기 시작한다.

작업 디렉터리는 Working Tree라고도 부르는데, 이 두 가지 용어는 동일한 의미로 사용된다. Git 공식 문서에서도 두 용어가 같은 의미로 사용되며 혼용된다.

working tree Git 공식문서 설명

working tree Git 공식문서 설명

이는 Git의 상태를 트래킹하고 파일 시스템의 특정 상태를 나타내는데 있어서 같은 역할을 하기 때문이다. 따라서, 작업 디렉터리와 작업 트리 모두 “사용자가 직접 편집하는 파일들이 위치한 영역”을 지칭한다.

Image

작업 내용이 없을 경우 git status 명령을 했을 때 띄워지는 문구

2. 스테이징 영역(Staging Area)

스테이징 영역은 커밋(commit)하기 전에 변경된 파일을 임시로 저장하는 공간이다.

git add 명령어를 사용하여 작업 디렉터리에서 변경된 파일을 스테이징 영역으로 추가할 수 있다. 이를 통해 커밋 시 어떤 변경 사항이 포함될지 선택적으로 관리할 수 있다.

3. 지역 저장소(Local Repository)

지역 저장소는 사용자의 컴퓨터에 위치하며, 모든 버전 히스토리를 저장한다.

git commit 명령어를 통해 스테이징 영역에 있는 변경 사항을 지역 저장소에 기록한다. 지역 저장소는 .git 디렉터리로 관리되며, 버전 기록을 안전하게 보관한다.

4. 원격 저장소(Remote Repository)

원격 저장소는 GitHub, GitLab과 같은 플랫폼에서 호스팅되며, 협업을 위해 사용된다.

git push 명령어를 사용하여 지역 저장소의 데이터를 원격 저장소로 업로드할 수 있다. 반대로, 원격 저장소에 있는 최신 데이터를 지역 저장소로 가져오려면 git pull 명령어를 사용한다. 새로운 저장소를 처음 복제할 때는 git clone 명령어를 사용하여 원격 저장소의 내용을 지역 저장소로 복사한다.

쉽게 기억하기

  • 작업 디렉토리: 내가 실제로 일하는 공간.
  • 스테이징 영역: 커밋하기 전에 임시로 저장하는 곳.
  • 지역 저장소: 내 작업을 안전하게 저장하는 곳.
  • 원격 저장소: 협업 및 백업을 하기 위해 작업을 저장하는 곳.

🌊 Git의 데이터 흐름

Git 저장소에 대해 설명하면서 Git의 데이터 흐름에 대해서도 설명을 했다. 그러나 이렇게 글로 설명해서는 잘 이해되지 않을 것이다. 아래 그림을 보면서 간략하게 Git의 데이터 흐름에 대해서 다시 한번 살펴보자.

git 저장소의 구조와 흐름

  1. git init을 통해 현재 디렉토리를 Git 저장소로 초기화하고 변경된 파일을 추적한다.
  2. 작업 디렉터리에서 코드를 수정하고 변경 사항을 생성한다.
  3. git add 명령어로 변경 사항을 스테이징 영역에 추가한다.
  4. git commit 명령어로 스테이징 영역의 변경 사항을 지역 저장소에 저장한다.
  5. git push 명령어를 사용해 지역 저장소의 변경 사항을 원격 저장소로 전송한다.
  6. 다른 작업자의 변경 사항을 통합하려면 git pull 명령어를 사용한다.

+) 만약 새로운 저장소를 작업 중인 컴퓨터로 복제하고 싶은 경우, git clone 명령어를 사용하여 원격 저장소의 내용을 지역 저장소로 복사한다.

Git의 저장소 구조와 데이터 흐름을 이해했다면, 이제 작업을 진행하며 파일의 상태가 어떻게 변화하는지 알아볼 차례다. 파일의 상태 변화는 Git이 파일을 관리하는 과정을 이해하는 핵심으로, 효율적인 버전 관리를 위해 반드시 알아야 할 개념이다.

🕊️ Git에서의 파일 상태 변화

Git에서 파일의 상태 변화는 파일이 Git에서 어떻게 관리되고 있는지를 보여주는 중요한 요소다. 이 과정은 파일이 작업 디렉터리(Working Directory)와 스테이징 영역(Staging Area) 사이를 이동하며 상태가 변화하는 방식으로 나타난다.

아래 그림을 참고하여 어떻게 파일 상태 변화가 나타나는지 알아보자.

git의 파일 상태 변화

1. Untracked 상태

작업 디렉터리에 새로 추가된 파일은 처음에는 Git에 의해 추적되지 않는 상태인 Untracked로 분류된다. 이 상태는 Git이 파일을 아직 버전 관리에 포함하지 않았음을 의미한다.

untracked file

  • git add 명령어

    git add 명령어를 실행하면 Untracked 상태의 파일이 스테이징 영역에 추가되고, Tracked 상태로 전환된다.

2. Tracked 상태

Tracked 상태는 Git이 해당 파일을 버전 관리하고 있음을 의미한다.

tracked file

Tracked 상태의 파일은 아래 세 가지 하위 상태로 나뉜다.

  • Unmodified: 마지막 커밋 이후 파일에 변경이 없는 상태. git add를 실행하지 않은 상태의 파일이 여기에 해당.
  • Modified: 파일이 수정되었으나 아직 스테이징 영역에 반영되지 않은 상태.
  • Staged: 수정된 파일이 git add 명령어로 스테이징 영역에 추가된 상태.

3. Unmodified → Modified

Tracked 상태의 파일이 수정되면, 파일은 Modified 상태로 변경된다. 이 상태는 파일이 변경되었으나 아직 스테이징 영역에 반영되지 않았음을 나타낸다.

modified file

4. → git add 명령어

수정된 파일에 대해 다시 git add 명령어를 실행하면, 파일은 다시 Staged 상태로 전환된다. Staged 상태의 파일은 이후 커밋(commit) 시 기록된다.

📝 정리

  • 작업 디렉터리 (Working Directory): 실제로 파일을 수정/편집하는 공간
  • 스테이징 영역 (Staging Area): 커밋 전 변경 사항을 임시 저장하는 공간
  • 지역 저장소 (Local Repository): 모든 버전 히스토리를 저장하는 공간
  • 원격 저장소 (Remote Repository): 협업 및 백업용 저장 공간 (GitHub, GitLab 등)
  • Git의 핵심 흐름: 작업 디렉터리 → 스테이징 영역 → 지역 저장소 → 원격 저장소
  • Untracked: Git이 추적하지 않는 새 파일
  • Tracked 상태
    • Unmodified: 마지막 커밋 이후 변경 없음
    • Modified: 파일 수정됨
    • Staged: 수정 파일이 git add로 스테이징 영역에 반영

📚 참고

매3개 | Git: HEAD와 detached HEAD

📩 HEAD?

HEAD는 Git에서 현재 작업 중인 브랜치를 가리키는 포인터이다. 일반적으로 HEAD는 특정 브랜치(예: main, develop)를 가리키며, 해당 브랜치의 마지막 커밋을 기준으로 작업을 진행한다.

💘 detached HEAD?

detached HEAD는 HEAD가 브랜치를 가리키는 대신, 특정 커밋을 직접 가리키는 상태를 말한다. 즉, 브랜치와 연결되지 않은 채 특정 커밋에서 작업을 시작한다.

어떻게 detached HEAD 상태가 될 수 있는지 예시를 통해 알아보자.

dtached HEAD 상태로 만들기

git checkout <commit-hash>

위 명령어를 실행하면 Git은 브랜치를 기준으로 작업하지 않고, <commit-hash>로 지정된 특정 커밋에서 작업을 시작한다. 이제 HEAD는 브랜치가 아닌 특정 커밋을 가리키게 된다.

그렇다면 detached HEAD 상태에서 작업을 하면 어떤 일이 일어나는 걸까?

detached HEAD 상태에서 작업의 특징

1. 임시 상태

새로운 커밋을 생성하면, 이 커밋은 기존 브랜치와 연결되지 않는다. 브랜치로 연결하지 않으면, 이후 Git 작업 중 이 커밋을 잃어버릴 위험이 있다.

2. 기본 브랜치로 돌아가면 변경 사항이 사라질 수 있음

git checkout main

detached HEAD 상태에서 다른 브랜치로 이동하면, 이전에 작업한 내용이 그대로 버려질 수 있다.

git checkout <commit-hash>
echo "Changes" > file.txt
git add file.txt
git commit -m "detached commit"

git checkout main

만약 detached HEAD 상태에서 작업한 내용을 커밋하고 기본 브랜치로 돌아왔다면, 해당 커밋은 보이지 않게 될 수 있지만, 완전히 사라지지는 않는다.

그렇다면 어떻게 detached HEAD에서 작업한 내용을 저장해둘 수 있을까?

detached HEAD 상태에서 작업 보존

detached HEAD 상태에서 작업을 유지하려면 브랜치를 생성하거나 변경 사항을 명시적으로 저장해야 한다.

방법 1: 새로운 브랜치 생성

git checkout -b <new-branch>

detached HEAD 상태에서 브랜치를 생성하면, 해당 브랜치에 현재 상태가 저장된다.

방법 2: stash로 저장

git stash

작업 내용을 stash로 저장한 후, 다시 브랜치로 돌아가서 pop을 통해 복원할 수 있다.

이렇게 detached HEAD에 대해서 알아보았다. 그렇다면 이건 도대체 언제 사용하는 걸까?

detached HEAD 상태가 필요한 경우

  • 특정 커밋 기반으로 새로운 작업을 시작하고 싶을 때
  • 과거 커밋의 상태를 확인하거나 임시로 작업하려 할 때
  • 특정 태그나 커밋을 기반으로 빌드하거나 디버깅하려 할 때

위와 같은 경우에 detached HEAD를 사용하게 된다. 자, 이제 detached HEAD가 뭔지 알아보았으니 어떻게 사용하는지 예시를 통해 알아보자.

예시로 이해하기

  1. 현재 브랜치 상태

    현재 HEAD는 main 브랜치를 가리킨다.

    git checkout main
    
  2. detached HEAD 상태로 전환

    HEAD가 특정 커밋(1234abcd)을 가리키고, main 브랜치와의 연결이 끊긴다.

    git checkout 1234abcd
    
  3. detached HEAD 상태에서 커밋 생성

    이 커밋은 브랜치와 연결되지 않아 나중에 잃어버릴 위험이 있다. 그러나 완전히 사라지는 것은 아니다.

    echo "Test" > file.txt
    git add file.txt
    git commit -m "Detached HEAD commit"
    
  4. 브랜치를 생성하여 커밋 보존

    커밋을 안전하게 새로운 브랜치에 저장할 수 있다.

    git checkout -b <new-branch>
    

여기서 만약? detached HEAD 상태에서 작업한 커밋을 브랜치를 생성하여 바로 저장하지 않고 그냥 기존 브랜치(main)로 변경해버렸다면? 어떻게 다시 detached HEAD 상태에서 작업한 커밋을 찾아서 저장할 수 있을까?

잃어버린 커밋 찾고 저장하기

절차는 다음과 같다.

  1. 잃어버린 커밋 해시 찾기
  2. 해당 커밋 해시를 기반으로 새로운 브랜치 생성

끝이다! 2번 절차는 위에서 이미 알아봤으니 우리는 1번 절차만 어떻게 하는지 알면 된다.

잃어버린 커밋 해시 찾기

git log

or

git reflog

git log 명령을 사용하여 최근 커밋들을 확인하거나, git reflog를 사용해 최근의 모든 작업을 확인할 수 있다.

  • git log의 경우에는 HEAD되어 있는 브랜치에서 작업했던 내용들만 확인할 수 있다.
  • git reflog는 최근에 작업했던 모든 내용들. 즉, 현재 HEAD된 브랜치가 무엇이던 간에 브랜치에 상관없이 작업했던 모든 내용(checkout하거나 commit 하거나 등)들과 hash 값을 확인할 수 있다.

이렇게 잃어버린 커밋의 hash 값을 찾았다면 이 것을 기반으로 새로운 브랜치를 생성하면 된다.

git switch -c <new-branch> <잃어버린 커밋의 hash>

📝 정리

상태 HEAD가 가리키는 위치 브랜치와의 연결 여부
일반 브랜치 상태 특정 브랜치 (main) 연결됨
detached HEAD 상태 특정 커밋 (1234abcd) 연결되지 않음

📚 참고