본문 바로가기
공부/개발 & 컴퓨터

한 번에 끝내는 Git 사용법 - 이론편

by algosketch 2021. 9. 25.

이 글을 읽기 전에 한 번에 끝내는 Git 사용법 - 실전편 (feat. GitHub) 내용을 먼저 숙지하고 있어야 합니다.

23-08-29 : git switch 명령어에 대한 부연 설명이 추가되었습니다.

 

git add

 git commit 은 Staged 된 파일들만 커밋한다. Git repository 에서 파일들의 상태에 대해 알아보자.

Untracked → Staged

 Git repository 에 있는 파일들은 네 가지 상태로 나눌 수 있다. Untracked, Unmodified, Modified, Staged. Untracked 를 제외하고는 다 추적되고 있는 파일들이다. Untracked 상태에 있는 파일들은 Git 기록이 남지 않는다. Untracked 인 파일을 추적하도록 만들려면 다음 명령어를 사용하면 된다.

git add <file-name>

 파일을 각각 추가하려면 경로를 모두 작성해야하고 번거롭기 때문에 보통 모든 파일을 의미하는 . 을 입력한다.

# git add * 은 .gitignore 에 있는 파일도 추가될 수 있다.
git add .

 git add 의 첫 번째 역할은 Untracked 상태에서 Staged 상태로 바꾸는 것이다. 이 상태에서 커밋하면 새롭게 추가된 Tracked 파일들이 모두 커밋된다. 커밋된 파일들은 Unmodified 상태로 변경된다. 만약 일부 파일만 Staged 에 추가하면 나머지 파일은 Untracked 상태로 남게 된다.

Modified → Staged

 커밋 이후 파일을 수정하게 되면 Modified 상태로 변경된다. 마찬가지로 Modified 파일도 커밋하기 위해서는 Staged 상태로 만들어 줘야 한다. 위에서와 같은 명령어를 사용하고, 마찬가지로 나는 git add . 을 자주 사용한다.

git add .

 git add . 을 사용하면 Untracked 파일과 Modified 파일을 모두 Staged 상태로 바꿔준다. 이 파일들은 git commit 을 통해 현재 상태를 저장할 수 있다. 따라서 git add 의 두 번째 역할을 Modified 파일을 Staged 상태로 바꿔주는 것이다. 커밋 타이밍을 놓쳤을 경우 git add . 을 하지 않고 각각의 파일을 나눠서 커밋하는 경우도 있다.

 아래 명령어를 통해 .gitignore 에 있는 파일과 Unmodified 파일을 제외한 파일들의 상태를 확인할 수 있다.

git status

 git status 결과는 위와 같이 나온다. 초록색 글씨로 나오는 것은 Staged 파일이고 빨간색 글씨로 나오는 것을 Staged 되지 않은 파일들이다. git add . 이후 다시 확인하면 아래와 같이 나온다.

 new file 은 Untracked 파일이 Staged 된 경우이다. 한 가지 재밌는 점은 Staged 된 파일을 수정하면 Modified 상태, Staged 상태 모두 해당된다. 이 상태에서 커밋하면 Staged 이후에 변경한 내용을 반영되지 않는다.

git branch

git commit

 commit 을 하게 되면 현재 파일들을 만들어 저장한다. 이전 커밋 이후로 파일이 변경되지 않았다면, 이전 커밋 때 사용한 파일의 포인터만 갖고 있는다. 그래서 커밋도 기본적으로 이전 커밋에 대한 포인터를 갖게 된다.(이전 커밋을 참조한다.)

 커밋 순서는 1, 2, 3 이지만, 참조 관계는 3->2, 2->1 이다. commit 3 은 commit 1 을 알 수 있지만 commit 1 은 commit 3 을 모른다.

git branch

 git branch 는 이런 커밋에 분기를 나누는 것이다. 

 위에서부터 각각의 가로 선은 하나의 브랜치이다. 화살표는 시간의 순서와 병합을 나타낸다. 브랜치 이름은 각각 master, dev, topic 이다. master 는 기본 브랜치로 default 라고 부른다. default branch 라고 부르지만 특별한 브랜치는 아니다. 원하면 dev 브랜치를 default 로 변경해도 상관 없다. (git init 을 통한 default branch 이름은 기본적으로 master 라고 지어지고, GitHub 에서 만들었다면 main 브랜치로 지어진다.)

 commit 과 포인터 관점에서 브랜치를 살펴보면 아래와 같은 모양이 된다.

 위 그림에서 dev 는 commit 2 에서 파생되어 하나의 변경사항 commit 4 를 기록했다. 그와 별개로 master 브랜치도 commit 3 을 기록했다. commit 5 는 master 브랜치에 dev 브랜치를 합친 것이다. commit 2 에는 A 라는 파일이 있고, commit 3 에서는 B 라는 파일을 만들고, commit 4 에서는 C 라는 파일을 만들었다면 commit 5 는 A, B, C 모든 파일을 갖고 있다.

 git branch 는 아래 명령어를 통해 만들 수 있다.

# 아래 명령어는 git branch <branch-name> 과 git checkout <branch-name> 을 합친 명령어이다.
# <branch-name> 브랜치로 이동하는데, 브랜치가 없다면 만들고 이동하라는 뜻이다.
git checkout -b <branch-name> # git switch -c <branch-name> 으로도 가능하다. (23-08-29 추가)

 

 현재 작업하고 있는 브랜치를 git checkout 이라는 명령어로 변경할 수 있다. 현재 작업하고 있는 커밋을 HEAD 라고 부른다. 위의 그림에서 master 브랜치는 commit 5 의 포인터를 갖고 dev 브랜치는 commit 4 를 갖고 있다. (master 에 dev 를 병합한다고 해서 dev 가 사라지는 것은 아니다.) 현재 작업하고 있는 브랜치가 master 라면 HEAD 는 commit 5 를 포인터를 갖고, dev 라면 commit 4 를 포인터로 갖는다.

 이쯤돼서 git status 명령어의 결과를 다시 살펴보자. 첫 줄에 on branch dev 라고 나오는 것은, 현재 작업하고 있는 브랜치가 dev 브랜치라는 뜻이다. 현재 브랜치는 git status 뿐만 아니라 git branch 명령어를 통해서도 확인할 수 있다.

git checkout

 git checkout 명령어는 결국 HEAD 의 위치를 변경하는 명령어로 이해할 수 있다. 따라서 아래 명령어도 가능하다.

git checkout 5eb33cf

 여기서 16진수 숫자는 hash 값인데 원래는 저거보다 훨씬 길지만 일부만 작성해도 된다. 이 명령어를 통해 과거 커밋으로 돌아가는 것도 가능하고, 과거 커밋에서 새로운 브랜치를 만드는 것도 가능하다.

git switch

git checkout 명령어가 너무 많은 기능을 포함하고 있어 git switch와 git restore로 기능이 나누어졌다. 기존의 checkout 명령어도 물론 사용 가능하다. 그중 switch는 branch를 변경하는 기능으로 사용할 수 있다.

git merge

 master 브랜치에 dev 브랜치를 병합하려면 다음 명령어를 사용하면 된다.

git checkout master # git switch master 로도 가능하다. (23-08-29 추가)
git merge dev

 이전에 설명할 때는 commit 3 에서 변경한 파일과 commit 4 에서 변경한 파일이 서로 다른 아름다운 경우를 예로 들었다. 이런 경우 사람이 따로 작업할 필요가 없이 곧바로 commit 5 라는 merge commit 이 생긴다. 이런 아름다운 경우를 fast forward 라고 부른다. 같은 파일을 수정하더라도 수정한 위치가 다르다면 fast forward 가 적용된다. 편리한 기능이지만 가끔 이 기능이 원치 않게 적용되는 경우도 있으니 주의하자.

conflict

 아름답지 않은 경우는 어떤 경우일까? commit 3 의 수정사항과 commit 4 의 수정사항이 겹치는 경우이다. 이런 경우를 conflict (충돌)이라고 한다. 다음 예제를 보자. 각각의 커밋에서 파일의 내용이라고 생각하면 된다.

# commit 2
algosketch 는 __입니다.

# commit 3
algosketch 는 똑똑합니다.

# commit 4
algosketch 는 멍청합니다.

 이 경우 master (commit 3)에 dev (commit 4) 를 합치면 commit 5 가 생기기 전에 다음과 같은 파일로 변경된다.

<<<<<<< HEAD
algosketch 는 똑똑합니다.
=======
algosketch 는 멍청합니다.
>>>>>>> b2b94dc

=== 를 기점으로 위는 HEAD (master)가 수정한 내용이고, 아래는 병합한 브랜치(dev)가 수정한 내용이다. 실제로 파일의 내용이 이 상태로 변경되기 때문에 <<< >>> === 를 포함해 필요 없는 내용을 모두 지워야 한다. 필요없는 내용을 모두 지워 원하는 상태로 만드는 것을 resolve 라고 한다. 충돌이 일어난 파일들을 git status 를 입력했을 때 both modified 라는 상태로 나온다. resolve 이후에는 해당 파일을 resolve 했다는 의미로 아래 명령어를 입력한다.

git add <file-route>

 git add 의 마지막 세 번째 역할은 resolve 확인이다. 파일 경로를 작성하는 것은 귀찮기 때문에 나는 충돌이 일어난 모든 파일을 해결하고 마지막에 git add . 을 입력한다.

 브랜치를 병합하는 방법은 merge 이외에도 rebase 가 있다. merge 와의 차이점은 merge 는 merge 커밋을 남기는 반면, rebase 는 마치 하나의 브랜치에서 작업한 것처럼 commit log 를 깔끔하게 남길 수 있다. 개인 프로젝트에서는 커밋 로그 관리를 위해 rebase 를 사용하는 경우도 있지만, 협업 프로젝트에서는 대부분 merge 를 사용한다. 따라서 rebase 에 대한 방법을 따로 설명하지 않았다.

더보기

협업 프로젝트에서 rebase 를 지양하는 이유는 merge 로 병합된 브랜치를 나중에 rebase 로 변경했을 때, 다른 사람이 이 사실을 모르고 pull 하면 중복된 커밋이 생기는 등 커밋 로그가 꼬일 수 있다. git pull --rebase 처럼 rebase 옵션과 함께 pull 하면 이 상황을 방지할 수 있다. alias 를 통해 git pull --rebase 를 기본 옵션으로 사용하는 경우도 있다.

협업, 브랜치 종류

 협업 프로젝트에서는 자신만 사용하는 브랜치를 만들고 주기적으로 공용 브랜치에 merge 한다.

 서비스 출시 전 브랜치는 master 만 두어도 상관 없다. 출시 이후에는 master 브랜치와 develop 브랜치로 나눈다. master 브랜치는 현재 서비스 중인 브랜치이고, develop 브랜치는 현재 개발중인 브랜치이다. 특정 문제를 해결하기 위해 잠깐 만들고 없애는 topic 브랜치를 만들 수도 있다. 같은 문제에 대해 여러 가지 방안을 제안할 수도 있으므로 여러 개의 topic 브랜치를 만들기도 한다.

 필수로 알아야할 내용은 이 정도면 될 것 같다. 심화편에서는 Git 공부 이후 실제로 Git 을 쓰면서 생겼던 문제나, 그냥 쓰고 있던 git pull 의 진짜 모습, 내가 사용하는 마이너한(?) 명령어 정도를 소개할 것이다.