ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 왕초보를 위한 Git 명령어 모음집 (2)
    IT 이야기 2024. 2. 16. 10:00

     먼저 읽으면 좋은 글
    👉 왕초보를 위한 Git 명령어 모음집 1편


    이 글을 읽는 법

    Git에는 수많은 명령어가 있습니다. 이번 글에서는 개발 팀에 입사한 초보 개발자가 협업하면서 겪게 될 시나리오를 토대로 다음과 같은 Git 명령어를 정리합니다:

    • Git 최초 설정: git config
    • 로컬 저장소와 원격 저장소의 개념 이해하기: git init, git clone, git remote, git push, git fetch, git pull
    • 로컬 저장소의 세 가지 단계: git status, git add, git commit, git diff, git restore, git reset, git stash
    • 커밋과 브랜치의 개념 이해하기: git switch, git merge, git cherry-pick
    • 커밋 로그 그래프로 원하는 커밋 검색하기: git log, git show, git config alias.*, git checkout

    각 섹션은 다음과 같은 흐름으로 진행됩니다:

    • 학습 목표: 어떤 목표를 달성할 수 있는지 설명합니다.
    • 사전 지식: 명령어를 실습하기 전에 필요한 사전 지식을 설명합니다.
    • 명령어 실습: Git 명령어를 실습합니다.
    • 문제 풀이: 실습한 명령어를 활용하여 문제를 풀어봅니다.

    목차

    왕초보를 위한 Git 명령어 모음집 2탄

    1. 커밋과 브랜치의 개념 이해하기
    2. 커밋 로그 그래프로 원하는 커밋 검색하기

     

    1.  커밋과 브랜치의 개념 이해하기

    Git은 프로젝트 버전을 관리할 때 특정 시점에 대한 파일과 폴더의 전체 상태를 스냅샷으로 저장합니다. 이 특정 시점의 스냅샷을 의미하는 단위가 바로 커밋(commit)입니다. 개발자는 커밋을 통해 언제든지 원하는 버전으로 돌아갈 수 있습니다.

    브랜치(branch)는 Git에서 커밋을 가리키는 포인터입니다. 브랜치를 사용하면 마치 나뭇가지가 다양한 방향으로 갈라지듯, 프로젝트의 개발 흐름을 다양한 경로로 분기시켜서 여러 작업을 동시에 진행할 수 있습니다.

    학습 목표

    • 브랜치를 생성하여 작업을 커밋할 수 있습니다.
    • 작성한 커밋을 수정할 수 있습니다.
    • 특정 브랜치의 전체 커밋을 나의 작업 브랜치로 가져올 수 있습니다.
    • 특정 브랜치의 일부 커밋을 나의 작업 브랜치로 가져올 수 있습니다.

    사전 지식

    커밋은 커밋 해시라는 고유한 식별자를 가집니다. 예를 들어, b6c57a24와 같은 형태입니다. 커밋은 생성될 때 이전 커밋으로부터 어떤 변경(diff)이 있었는지 기록합니다. 모든 개별 커밋은 이전 커밋으로부터 생성된 것이기 때문에 각 커밋은 이전 커밋(=부모 커밋)을 가리키는 형태로 표현합니다.

    브랜치(branch)는 Git에서 커밋을 가리키는 포인터입니다. 각 브랜치는 특정 커밋을 기점으로 시작하며, 새로운 커밋을 추가할 때마다 해당 브랜치는 최신 커밋을 가리키는 포인터의 역할을 계속해서 수행합니다. 시간이 흘러 여러 커밋이 쌓이면 각 브랜치는 특정 프로젝트 기점으로부터 새로운 이력을 가진 독립적인 작업 단위가 됩니다.

    참고로 Git에는 HEAD 브랜치라는 특별한 브랜치가 있습니다. HEAD 브랜치는 현재 체크아웃된 커밋을 가리키는 포인터입니다. 예를 들어, 위 커밋 그래프 그림을 기준으로 현재 체크아웃 된 브랜치가 main 브랜치라면 HEAD 브랜치는 main 브랜치의 최신 커밋 E를 가리킵니다. 현재 fix-sidebar 브랜치에 체크아웃되어 있다면 HEAD 브랜치는 fix-sidebar 브랜치의 최신 커밋 Z를 가리킵니다.


    명령어 실습

    이슈를 처리하기 위한 작업 브랜치를 만들 차례입니다. 그 전에 분기점이 될 main 브랜치를 최신화하겠습니다.

    $ git switch main
    $ git pull

    불필요한 충돌을 방지하려면 항상 main 브랜치를 원격 저장소와 최신화하고 나서 새로운 작업 브랜치를 만드는 것이 좋습니다.

    이제 작업 브랜치를 만들고 해당 브랜치로 이동하겠습니다.

    $ git switch -c fix-sidebar
    • git switch -c <branch>: switch 명령어는 브랜치를 이동하는 명령어입니다. --create 옵션의 약어인 -c 옵션을 넣으면 새로운 브랜치를 생성하면서 해당 브랜치로 이동합니다.

    브랜치를 전환하거나 특정 커밋으로 이동하는 것을 Git에서 체크아웃(checkout)이라고 합니다. 여기서 말하는 이동은 작업 폴더의 상태를 특정 브랜치 또는 특정 커밋의 스냅샷 상태로 바꾸는 것을 의미합니다. 해당 기능을 수행하는 git checkout 명령어는 이후 섹션에서 다루겠습니다.

    작업을 완료하고 커밋하겠습니다.

    $ git add .
    $ git commit -v
    • git add .: 현재 디렉토리의 모든 파일을 스테이지에 추가합니다.
    • git commit -v: -v 옵션을 사용하면 커밋 메시지를 작성할 때 변경한 내용(diff)을 함께 보여줍니다.

    커밋 메시지를 작성할 때 변경한 내용을 한 번 더 살펴보는 이유는 더 나은 커밋 메시지를 작성하기 위함입니다. 커밋 메시지를 잘 작성하면 프로젝트 이력 관리에 큰 도움이 되므로 신경 써서 작성하는 습관을 기르는 것을 권장합니다. 좋은 커밋 메시지에 관한 예시는 My Favourite Git commit 문서를 추천합니다.

    그런데 깜빡 잊고 디버깅 코드를 제거하지 않았다고 가정하겠습니다. 디버깅 코드를 제거한 변경은 이전 커밋에 포함시키고 싶고 새 커밋으로 추가하고 싶지는 않습니다. 이럴 때는 디버깅 코드를 제거한 변경을 스테이지에 추가하고, --amend 옵션을 사용해서 현재 커밋을 덮어쓰면 됩니다:

    $ git add .
    $ git commit --amend -v

    --amend 옵션은 이미 완료된 현재 커밋을 수정하는 옵션입니다. 스테이지에 있는 변경 사항을 현재 커밋에 추가하고, 커밋 메시지도 함께 수정할 수 있습니다.

    참고로 커밋을 변경하면 커밋 해시도 변경됩니다. 여기서 말하는 변경은 코드의 변경뿐만 아니라 커밋 작성자, 커밋 작성 시간, 부모 커밋 같은 커밋과 관련된 모든 변경을 포함합니다.

    이번에는 다른 브랜치에서 작업한 전체 내용을 나의 작업 브랜치로 가져오는 방법을 알아보겠습니다.

    가끔씩 PR(Pull Request)을 올렸는데 충돌이 발생하는 경우가 있습니다. PR을 올렸을 때 충돌이 발생하는 상황에 대한 한 가지 예시를 살펴보겠습니다.

    예를 들어, 3일 전에 main 브랜치를 기점으로 fix-sidebar 브랜치를 생성해서 작업을 완료하여 PR을 올렸습니다. PR은 GitHub에서 사용하는 용어로 Pull Request의 약어입니다. 내가 작업한 브랜치를 원격 저장소의 main 브랜치에 병합(merge)할 수 있도록 요청하는 것입니다. 참고로 GitLab에서는 동일한 기능을 MR(Merge Request)이라는 용어로 사용합니다.

    그런데 3일 사이에 main 브랜치가 업데이트 되었습니다. 그 업데이트 중에서 util.ts 파일을 수정한 변경이 있었습니다. 이 파일은 fix-sidebar 브랜치에서도 수정한 동일 파일이었습니다. 결과적으로 동일한 코드를 서로 다르게 수정했기 때문에 fix-sidebar 브랜치를 main 브랜치에 병합하려고 하면 충돌이 발생하는 상황이 된 것입니다.

    충돌을 해결하려면 main 브랜치는 이미 반영되었으니 작업 브랜치를 고치면 됩니다. main 브랜치를 fix-sidebar 브랜치로 가져와서 어떤 충돌이 있는지 확인하고 해결해서 다시 PR을 올리면 됩니다.

    먼저 main 브랜치를 최신화합니다.

    $ git switch main
    $ git pull

    fix-sidebar 브랜치로 이동한 후에 main 브랜치를 병합(merge)합니다:

    $ git switch fix-sidebar
    
    $ git merge main
    자동 병합: util.ts
    충돌 (내용): util.ts에 병합 충돌
    자동 병합이 실패했습니다. 충돌을 바로잡고 결과물을 커밋하십시오.

    git merge main 명령어를 실행하면 현재 체크아웃된 브랜치인 fix-sidebar 브랜치에 main 브랜치를 병합합니다.

    충돌일 발생하여 자동 병합이 실패했다고 합니다. 충돌이 발생한 파일을 열어서 확인하고 충돌을 해결합니다. 그러면 충돌을 해결한 변경이 생기겠죠? git add 명령어로 충돌을 해결한 파일을 스테이지에 추가하고, git commit 명령어로 커밋하면 충돌을 해결한 머지 커밋이 생성됩니다.

    이제 충돌을 해결한 머지 커밋을 원격 저장소에 업로드하면 PR이 자동으로 업데이트되고 더 이상 충돌이 발생하지 않습니다.

    $ git add .
    
    $ git commit -v
    [fix-sidebar 001a9c8] Merge branch 'main' into fix-sidebar
    
    $ git push

    이번에는 다른 브랜치의 커밋을 가져올 때 일부 커밋만 나의 브랜치로 복사하는 방법을 알아보겠습니다.

    예를 들어, 브랜치를 생성해서 작업을 하고 있는데 공통 모듈이 필요한 상황입니다. 그런데 알고 보니 먼저 다른 작업을 시작한 동료가 관련 변경을 이미 처리한 상황입니다. 굳이 중복 작업할 필요 없도록 동료가 작업한 특정 커밋만 나의 작업 브랜치로 가져오고 싶습니다.

    동료가 작업한 브랜치가 원격 저장소에 push 되어 있다면 나의 로컬 저장소로 해당 브랜치를 내려받습니다.

    $ git fetch origin <동료 브랜치 이름>

    git cherry-pick 명령어를 사용해서 작업이 반영된 특정 커밋만 가져오면 완료됩니다.

    $ git cherry-pick <commit>


    문제 풀이

    Q. 특정 브랜치에서 커밋을 여러 개 가져오고 싶습니다. 어떻게 하면 될까요?

    A. git cherry-pick <commit-1> <commit-2> <commit-n>과 같이 가져올 커밋을 여러 개 지정하면 됩니다.

    Q. 특정 브랜치에서 가져올 커밋이 여러 개인데 일일이 지정하는 게 불편합니다. 범위를 지정해서 가져올 수 없나요? 어떻게 하면 될까요?

    A. git cherry-pick <start-commit>~..<end-commit> 명령어를 사용하면 됩니다.

    커밋의 범위를 지정하려면 A..B와 같이 두 개의 점(dobule dot, ..)을 사용할 수 있습니다. A..B의 의미는 B에는 있지만 A에는 없는 커밋을 의미합니다.

    예를 들어, 커밋 그래프가 아래와 같은 경우라고 합시다.

    C1 <-- C2 <-- A <-- C3 <-- C4 <-- B

    B에 있는 커밋은 [C1, C2, A, C3, C4, B]입니다. A에 있는 커밋은 [C1, C2, A]입니다. 결과적으로 B에는 있지만 A에는 없는 커밋 A..BC3, C4, B가 됩니다.

    # B
    C1 <-- C2 <-- A <-- C3 <-- C4 <-- B
    
    # A
    C1 <-- C2 <-- A
    
    # A..B
                        C3 <-- C4 <-- B

    따라서 cherry-pick으로 커밋 A를 포함해서 가져오려면 A~..B와 같이 틸드(~)를 사용해서 A의 부모 커밋으로 지정해야 합니다.

    위 예시 기준으로 A~..BA, C3, C4, B가 됩니다.

    # B
    C1 <-- C2 <-- A <-- C3 <-- C4 <-- B
    
    # A~
    C1 <-- C2
    
    # A~..B
                  A <-- C3 <-- C4 <-- B

    Q. 현재 작업 브랜치를 원격 저장소에 업로드한 작업 브랜치와 동일하게 만들고 싶습니다. 어떻게 하면 될까요? ?

    A. git reset --hard origin/<branch> 명령어를 실행하면 됩니다.


    2.  커밋 로그 그래프로 원하는 커밋 검색하기

    작업의 모든 변경은 커밋으로 기록됩니다. 코드를 추가하거나 삭제한 작업 모두 변경이기 때문에 삭제된 코드 또한 삭제한 이력이 커밋으로 기록됩니다. 결국 Git으로 프로젝트를 관리하는 순간부터 모든 변경 사항은 커밋에 담겨 있습니다. 따라서 커밋을 잘 검색할 수 있다면 프로젝트의 히스토리를 쉽게 찾아볼 수 있습니다.

    학습 목표

    • 프로젝트 이력을 확인할 수 있는 커밋 로그 그래프를 출력할 수 있습니다.
    • 명령어 별칭을 사용해서 길게 입력해야 하는 명령어를 단축 명령어로 실행할 수 있습니다.
    • 커밋 로그 그래프를 통해 원하는 커밋을 찾아낼 수 있습니다.
    • 커밋 간에 무엇이 변경되었는지 확인할 수 있습니다.

    사전 지식

    내가 원하는 프로젝트 버전으로 전환하려면, 내가 원하는 버전이 어떤 커밋에 있는지 확인할 수 있어야 합니다. 커밋 목록과 커밋에 대한 구체적인 정보는 다음과 같이 확인할 수 있습니다:

    • git log: 프로젝트의 모든 커밋 목록을 확인할 수 있습니다.
    • git show <commit>: <commit>에 해당하는 자세한 정보를 확인할 수 있습니다.
    • git diff <commit-1> <commit-2>: <commit-1><commit-2> 사이에 변경된 내용을 확인할 수 있습니다.

    Git 명령어를 사용하다 보면 옵션을 너무 많이 입력해야 하거나 두 가지 이상의 명령어를 매번 번거롭게 입력해야 해서 불편함을 느낄 때가 있습니다. 이런 경우에는 명령어 별칭(alias)을 만들어서 해결할 수 있습니다. 명령어 별칭은 다음과 같이 git config 명령어를 사용해서 설정에 추가할 수 있습니다:

    $ git config --global alias.<alias-name> <command>


    명령어 실습

    git log 명령어를 실행해서 프로젝트에 기록된 모든 커밋 로그를 확인하겠습니다:

    $ git log
    commit b6c57a24891c15f4b71679f6f6faef30952f5156 (HEAD -> fix-sidebar)
    Author: datalater <the7mincheol@gmail.com>
    Date:   Sun Jan 21 14:22:45 2024 +0900
    
        fix: fix sidebar
    
        Fix showing sidebar when darkmode is turned off
    
    commit ad349a4605906bf840141a9a35405c55a2154377
    Author: datalater <the7mincheol@gmail.com>
    Date:   Sun Jan 21 14:22:31 2024 +0900
    
        chore: set prettier config
    
    commit 321d13f2ff3bd6500dbafe1180965c969441bf1c
    Author: datalater <the7mincheol@gmail.com>
    Date:   Sun Jan 21 14:09:04 2024 +0900
    
        fix: wrong usage of draft

    커밋 해시, 커밋 작성자, 커밋 날짜, 커밋 메시지가 보입니다. 사이드바를 고친 가장 최근 커밋에서 코드를 어떻게 수정했는지 보기 위해 git show 명령어를 실행합니다:

    git show b6c57a24

    참고로 git show 명령어 뒤에 커밋을 지정하지 않으면 현재 커밋(HEAD)을 기본값으로 적용해서 실행합니다.

    여러 커밋 간에 어떤 변경 사항이 있었는지 확인하려면 git diff 명령어를 사용하면 됩니다.

    $ git diff 321d13f2 300d4877
    diff --git a/packages/.prettierrc.json b/packages/.prettierrc.json
    index 26125a5c..ac260452 100644
    --- a/packages/.prettierrc.json
    +++ b/packages/.prettierrc.json
    @@ -8,7 +8,7 @@
       "jsxSingleQuote": false,
       "trailingComma": "none",
       "quoteProps": "as-needed",
    -  "arrowParens": "always",
    +  "arrowParens": "avoid",
       "endOfLine": "lf",
       "bracketSpacing": true,
       "bracketSameLine": false
    ...

    git diff <commit-1> <commit-2> 명령어를 실행하면 <commit-1> 기준으로 <commit-2> 사이에 어떤 변경이 생겼는지 보여줍니다. + 기호가 붙은 줄은 이전 커밋에는 없었던 새롭게 추가된 코드 줄이고, - 기호는 삭제된 코드 줄입니다. git diff 명령어 뒤에는 커밋말고 브랜치 이름을 지정해도 됩니다. 브랜치가 곧 커밋을 가리키기 때문에 같은 원리로 동작합니다.

    그런데 git log 명령어는 커밋에 있는 자세한 정보를 여러 줄에 걸쳐 보여주기 때문에 프로젝트의 전체 이력을 한눈에 확인하기에는 쉽지 않습니다.

    git log에서 제공하는 몇 가지 옵션을 적용하면 핵심 정보만 간추려서 커밋 로그를 그래프 형태로 확인할 수 있습니다:

    $ git log --oneline --decorate --graph
    * b6c57a24 (HEAD -> fix-sidebar) fix: fix sidebar
    * ad349a46 chore: set prettier config
    * 321d13f2 fix: wrong usage of draft
    * 7ccf2053 chore: set vscode mdx format setting
    * 4bfb5966 feat: emphasize on problem-solving skills
    ...
    • --oneline 옵션은 커밋 메시지를 한 줄로 보여줍니다.
    • --decorate 옵션은 커밋에 태그나 브랜치가 있으면 함께 보여줍니다.
    • --graph 옵션은 커밋 그래프를 그려줍니다.

    커밋이 한 줄로 나오니까 더 많은 커밋이 한눈에 보입니다.

    그런데 여러 명이 협업하는 프로젝트에서 커밋 메시지만 봐서는 버전 정보를 분간하는 것이 쉽지 않습니다. --pretty 옵션을 사용해서 누가 작성했고 언제 작성된 커밋인지 함께 표시하겠습니다:

    $ git log --oneline --decorate --graph --pretty=format:"%C(red)%h%C(auto)%d %s %C(magenta)(%cr)%C(bold green) %an"
    * b6c57a24 (HEAD -> fix-sidebar) fix: fix sidebar (8주 전) datalater
    * ad349a46 chore: set prettier config (5일 전) datalater
    * 321d13f2 fix: wrong usage of draft (5일 전) datalater
    * 7ccf2053 chore: set vscode mdx format setting (5일 전) datalater
    * 4bfb5966 feat: emphasize on problem-solving skills (5일 전) datalater

    프로젝트 이력이 훨씬 더 잘 보이네요.

    Nextra 프로젝트를 클론받아서 위 명령어를 실행하면 아래와 같은 커밋 로그 그래프를 출력합니다. 프로젝트에서 언제 무엇을 변경했는지 한눈에 보이네요. 이제 커밋 메시지를 보면서 내가 원하는 버전으로 이동하려면 어떤 커밋을 선택해야 하는지 쉽게 알 수 있습니다.

    그런데 명령어가 너무 길죠? 명령어를 입력할 때마다 이렇게 길게 써야 한다면 상당히 번거로울 것입니다.

    이때 Git 설정에서 제공하는 별칭(alias) 기능을 사용하면 나만의 Git 커스텀 명령어를 만들 수 있습니다. 예를 들어, 위에서 입력한 매우 긴 명령어를 git l처럼 단축어로 사용하고 싶다면 아래와 같이 별칭 설정을 추가하면 됩니다. 설정을 글로벌 스코프로 적용하고 싶지 않다면 --local 옵션으로 입력하세요.

    $ git config --global alias.l "log --color --graph --decorate --date=format:'%Y-%m-%d' --abbrev-commit --pretty=format:'%C(red)%h%C(auto)%d %s %C(magenta)(%cr)%C(bold green) %an'"

    이제 명령어를 길게 입력할 필요 없이 git l을 사용하면 동일한 결과를 얻을 수 있습니다.

    $ git l
    * b6c57a24 (HEAD -> fix-sidebar) fix: fix sidebar (8주 전) datalater
    * ad349a46 chore: set prettier config (5일 전) datalater
    * 321d13f2 fix: wrong usage of draft (5일 전) datalater
    * 7ccf2053 chore: set vscode mdx format setting (5일 전) datalater
    * 4bfb5966 feat: emphasize on problem-solving skills (5일 전) datalater

    Git 별칭을 사용해서 생산성을 높이는 방법은 이전에 작성한 Git, GitHub 명령어 사용 꿀팁에 자세히 나와있으니 참고하세요.

    프로젝트의 커밋 히스토리를 한눈에 파악할 수 있게 된 덕분에 커밋을 찾기 한결 편해졌습니다. 이전 섹션에서 배웠던 cherry-pick이나 reset 명령어를 사용하려면 원하는 커밋이 어떤 것인지 알아야 했는데요. 앞으로는 git l 명령어를 사용해서 빠르게 찾아낼 수 있습니다.


    문제 풀이

    Q. 프로젝트에서 예전에 봤던 코드가 있는데 현재는 삭제되어 작업 폴더에서 검색되지 않습니다. 어떻게 하면 될까요?

    A. git log 명령어에서 제공하는 -S 옵션 또는 -G 옵션을 사용하면 됩니다.

    git log -S <문자열> 또는 git log -G <정규표현식> 명령어를 사용하면 프로젝트의 모든 커밋을 대상으로 검색합니다.

    예를 들어 커밋의 변경 사항 중에서 더 이상 쓰지 않게 된 useClickAway 리액트 훅을 찾고 있거나, debounce 함수를 import 하는 구문을 찾거나, 종료되어 삭제한 이벤트 페이지 코드를 찾고 싶다면 아래와 같이 실행하면 됩니다. git log를 사용해도 되지만 더 간결하게 보기 위해서 위에서 만들었던 git l 명령어로 찾겠습니다:

    $ git l -S 'useClickAway' -p
    
    $ git l -G 'import \{ debounce' -p
    
    $ git l -S '출시 이벤트' -p

    -p 옵션(=--patch)을 붙이면 커밋에 기록된 코드 변경을 diff 형식으로 보여주기 때문에 검색어가 포함된 맥락을 확인할 수 있습니다.

    Q. 삭제한 코드가 담긴 파일을 그대로 복구하고 싶어요. 어떻게 하면 될까요?

    A. git show <commit>:<file> 명령어를 사용해서 파일의 원문을 확인하고, git checkout <commit> -- <file> 명령어를 사용해서 파일을 복구하면 됩니다.

    파일 경로는 프로젝트 루트 기준으로 절대 경로를 입력하면 됩니다:

    $ git show d7b23bd0:packages/nextra-theme-docs/src/components/sidebar.tsx

    이전 섹션에서 작업 폴더의 상태를 특정 브랜치나 특정 커밋의 스냅샷으로 변경하는 것을 체크아웃이라고 했던 것 기억하시나요? git checkout <commit>을 입력하면 작업 폴더를 해당 커밋 상태로 완전히 변경합니다. 하지만 git checkout <commit> -- <file>과 같이 파일 경로를 명시하면 해당 커밋의 전체 파일 스냅샷을 가져오는 게 아니라 지정한 파일의 스냅샷만 작업 폴더에 가져옵니다.

    $ git checkout d7b23bd0 -- packages/nextra-theme-docs/src/components/sidebar.tsx

    마무리

    지금까지 배운 명령어를 토대로 실제 프로젝트에서 Git을 사용하는 흐름을 그림으로 정리하고 글을 마치겠습니다.

    함께 읽으면 좋은 글


    글 작성: 조민철(Grepp 프론트엔드 개발자)
    편집자: Tami

     

    댓글

Programmers