[일반] Git과 Github 사용 설명서

Shadis·2024년 2월 12일

일반

목록 보기
6/14

Git이란

Git은 버전관리, 백업 등 개발자가 소프트웨어를 유지보수 할 수 있게 도와주는 프로그램이다. 소프트웨어를 개발하다 보면 소프트웨어를 백업해두어야 하거나 버전 관리를 해야하는 경우가 발생한다. 업데이트 과정에서 백업해 둔 이전 버전의 소프트웨어로 돌아가야 할 수도 있고, 기존의 버전에서 분리된 새로운 버전에서 새로운 기능을 실험해야 할 수도 있다. Git은 이런 문제에 마주한 개발자가 코드를 유지보수 할 수 있게 도와주는 도와준다.

Commit

Git에서 하나의 버전 즉, 위의 사진에서 둥근 node 하나를 commit이라고 한다.

commit을 하나 만들 때마다 전체 코드를 복사해서 저장하는 것은 저장용량을 많이 차지하므로 비효율적이다. Git은 저장공간을 효율적으로 사용하기 위해 전체 코드를 저장하는 것이 아닌 이전 commit에서 어떤 부분이 변경되었는지를 기록한다. 이를 위해 Git은 HEAD가 가리키고 있는 commit의 코드를 Git cache에 저장하고 있다.

Git의 화살표


위의 사진에서 commit들의 순서가 어떻게 될까? 화살표의 방향 때문에 C3 -> C2 -> C1 -> C0 라고 생각할 수 있겠지만 정답은 C0 -> C1 -> C2 -> C3 이다. Git에서 화살표는 다음 commit을 나타내는 것이 아니라 부모 commit을 가리키고 있다.

Local Git

Git Working Space

git에는 working directory, staging area(index), repository 총 3가지 작업 영역이 있다.

working directory는 개발자가 파일이나 디렉토리를 직접 추가, 변경, 삭제하는 작업공간 그 자체를 말하고 repository는 commit을 저장하는 공간을 말한다.

조금 이색적인 작업 영역은 staging area(index)인데 이 작업영역은 임시 저장소 같은 공간이다. repository에 commit을 저장하기 전에 임시로 commit으로 저장할 변경사항들을 저장하고 관리하는 공간이다. 이 공간에서 commit으로 넘길 파일들은 남기고 commit으로 넘기지 않을 파일들은 staging area에서 삭제시켜 다시 working directory로 보내는 등 commit으로 저장할 변경사항을 관리할 수 있다.

File Status

Git 저장소에 있는 파일들은 untracked, unmodified, modified, staged 총 4가지 상태 중 하나의 상태를 갖는다.

untracked를 제외하고는 git에 등록되어 있는 상태를 말하는데 git에 등록되었다는 말은 git이 파일의 변경사항을 추적한다는 것이다. git이 추적하는 파일의 변경사항의 기준은 HEAD가 가리키고 있는 commit으로 이 commit과 비교하여 생긴 변경사항을 추적한다.

untracked는 말 그대로 git에 등록되어있지 않아 git이 변경사항을 추적하지 않는 상태의 파일이다. 오해하면 안되는게 .gitignore에 있는 파일과는 달리 git이 untracked인 상태의 파일이 있는 것을 인지는 하고 있다. 다만 파일에 변경사항이 생기더라도 git은 어떤 변화가 일어났는지 신경쓰지 않는다.

unmodified는 git에 등록되어 있지만 HEAD가 가리키는 commit과 비교하여 어떤 변화도 없는 상태이다.

modified는 git에 등록되어 있는 파일이 HEAD가 가리키는 commit과 비교하여 변경사항이 있는 상태이다.

staged는 git add 명령어를 통해 working directory에서 staging area로 옮겨진 상태이다.

Branch와 HEAD

HEAD는 Git에서 현재 작업중인 commit 혹은 Branch를 가르키는 화살표이다. 즉, HEAD는 나 자신이다.

앞서 말했듯 Git을 사용하는 이유 중 하나는 기존의 버전에서 분리된 새로운 버전을 만들기 위함이다. 이를 위해 Branch를 사용한다. Brnach는 영단어로 나뭇가지라는 뜻을 가지고 있어서 분기라고 생각할 수 있지만 Git에서는 그냥 참조라고 생각하는 것이 올바르다.

Branch와 HEAD의 관계

HEAD -> Brnach -> commit

Branch 생성 방법

git branch new_branch

위의 명령어를 실행하면 new_branch라는 이름의 새로운 Branch가 생성된다. 여기에서 오해하기 가장 쉬운 점이 새로운 Branch를 생성시키자마자 새로운 분기가 생기는 것이 아니라는 점이다. 새로운 분기를 생성시키기 위해서는 Branch를 생성시킨 이후 새로운 commit을 만들어야 한다. 때문에 앞에서 Branch를 새로운 분기가 아니라 참조로 생각하는 것이 올바르다고 한 것이다. Branch를 분기라고 생각하면 하나의 commit에 여러개의 Branch가 존재하는 것이 어색할 것이다.

Branch 조회

git branch

branch 목록을 조회할 수 있다.

git branch -r
git branch -a

git branch 명령어는 기본적으로 로컬 branch만 조회할 수 있다. -r 옵션을 추가하면 원격 branch만 조회할 수 있고, -a 옵션을 추가하면 로컬 branch와 원격 branch 모두 조회할 수 있다.

HEAD 이동

git checkout new_branch

checkout 명령어는 HEAD를 움직이는 명령어이다. commit으로 이동할 수도 있고 Branch로 이동할 수도 있다.

Branch 이동

git branch -f thisBranch targetCommit
git branch -f -f thisBranch targetCommit

branch 명령어에 -f 속성을 추가하면 Branch를 움직이는 명령어가 된다. thisBrnanch를 targetCommit으로 옮기는 명령어이다.

하지만 주의할 점이 있다. checkout된 branch는 branch -f 명령어를 통해 움직이지 않는다. 다른 commit으로 chekcout 한 이후 옮기거나 -f 옵션을 하나 더 사용하여 강제로 branch를 움직여야 한다. branch가 움직임에 따라 HEAD가 따라 옴직이는 것을 막기 위함이다.

git reset thisBranch^^

reset 명령어는 현재 HEAD가 가리키고 있는 Branch를 이전 commit으로 되돌릴 때 사용한다.

앞의 명령어들에 대한 설명을 통해 Branch를 움직일 때는 HEAD가 같이 따라가지만 HEAD 움직일 때는 Branch가 움직이지 않는 것을 알 수 있다.

branch를 옮길 때에는 주의할 점이 하나 있다. branch를 부모 commit으로 옮길 때에는 branch가 없이 버림받는 자식 commit이 생기지 않도록 주의해야 한다.

HEAD를 이동시키는 것과 Branch를 이동시키는 것의 차이점

reset 명령어를 사용해서 Branch를 이동시키면 Branch를 가리키고 있는 HEAD도 branch를 따라 이동한다. 하지만 checkout 명령어를 사용하여 HEAD를 이동시키면 HEAD가 가리키고 있는 Branch는 가만히 있고 HEAD만 따로 이동한다.

branch 삭제

git branch -d myBranch

원하는 branch를 삭제할 때 사용한다.

부모 commit으로 branch를 옮기는 행동이 버림받은 자식 commit을 만들어 해당 commit을 버릴 수 있는 위험한 행동인 것과 마찬가지로 branch를 삭제하는 것 또한 branch가 없는 버림받은 자식 commit을 만들 수 있다.

git branch -D myBranch

따라서 버림받은 자식 commit이 있을 것 같은 branch를 삭제할 때에는 위의 명령어를 사용해야 한다.

Log

git log

commit 로그를 보기 위한 명령어이다. 단독으로는 잘 사용되지 않고 주로 아래의 옵션들과 함께 사용된다. learngitbranching.js.org와 같은 깔끔하고 전체적으로 로그를 기대하기는 힘들다. CLI 환경에서 로그를 보여줄 뿐만 아니라 현재 HEAD가 가리키고 있는 commit의 직계 조상 log밖에 보여주지 않는다. 원하는 branch나 commit의 log를 보기 위해서는 해당 branch나 commit으로 이동해야 하거나 아래의 -all 옵션을 사용해야 한다. 또한, 직계 조상의 log만 보여주기 때문에 branch를 부모 commit으로 올릴 때에는 자식 commit이 버림받는 것을 주의해야 한다.

Log 명령어의 옵션

git log --graph

commit들의 부자관계를 명시적으로 볼 수 있게 해주는 옵션이다.

git log --oneline

git log를 사용하면 commit 각각의 저자, 생성일자와 같은 정보가 함께 나온다. 하지만 oneline 옵션을 사용하면 저자와 생성일자가 나타나지 않고 ID와 commit 메시지만 나와 더 깔끔하게 commit들의 관계를 볼 수 있다.

git log --all

현재 HEAD가 가리키고 있는 commit 뿐만 아니라 모든 branch의 commit을 볼 때 사용한다. 오해하지 말아햐 하는 것은 모든 commit을 보여주는 것이 아닌 모든 branch의 조상 commit을 보여주는 것이다. 따라서 버려진 자식 commit은 보여지지 않는다.

Merge

분리된 개발환경에서 새로운 기능을 완성하여 메인 분기로 가져오기 위해서는 새로운 분기를 메인 분기로 Merge 해야한다. 개인적으로 Git을 제대로 배우기 이전에 가장 어려운 부분이였다. 하지만 Branch를 분기가 아닌 참조로 이해하니까 Merge가 한층 이해하기 쉬워졌다.

Merge 하는 방법

git merge bugFix

HEAD가 가리키는 commit과 bugFix가 가리키는 commit 각각 부모 commit과의 차이점을 저장하고 있을 것이다. merge는 이 두 차이점을 합쳐 하나의 commit으로 만들어내는 것이다. 도식적으로 보면 두개의 분기가 하나의 분기로 이어지는 것이다.

주의할 점은 2개의 Branch가 1개의 Branch로 합쳐지는 것이 아니라는 점이다. Branch를 분기라고 생각하면 안되는 또 하나의 이유이다. Branch를 분기라고 생각하면 Merge 했을 때 2개의 Branch가 1개의 Branch로 합쳐진다고 오해할 수 있다. 앞서 말했듯 Branch는 분기가 아닌 참조이다. HEAD로 가리키고 있는 현재 작업중인 Branch에 대상이 되는 Branch의 내용이 적용되면서 도식적으로는 2개의 분기가 합쳐지는 것처럼 보이지만 대상이 되는 Branch(참조)는 없어지지 않는다.

Merge Conflict

merge 도중에 충돌이 발생해서 commit을 생성하지 못할 수도 있다. 그렇다면 merge conflict가 발생하는 경우는 어떤 경우일까? merge를 수행할 2개의 commit은 어느 하나의 commit으로부터 분리되었을 것이다. 그리고 merge를 수행할 각각의 commit들에는 공통조상인 commit으로부터 변경되어온 내역들이 쌓여있을 것이다. merge conflict란 그 각각의 변경내역이 충돌을 일으키는 것을 의미한다.

HEAD)		main)

111111		111111
000000		111111
111111		111111

HEAD가 가리키는 commit과 main이 가리키는 commit의 어떤 파일의 내용이 위와 같을 때, merge를 수행하면 merge conflict가 발생했다는 메시지와 함께 파일의 내용이 아래와 같이 변경된다.

111111
<<<<<< HEAD
000000
======
111111
>>>>>> main
111111

유저는 파일의 내용을 참고하여 어떻게 conflict를 해결할지 결정하면 된다.

직관적으로 생각하면 conflict를 해결하기 위해서는 conflict가 발생하여 merge를 실패했으니까 merge를 다시 해야 한다고 생각할 수도 있지만 conflict를 해결한 이후 merge가 아닌 commit을 해야한다.

merge conflict가 발생했다고 merge가 실패로 종료되는 것이 아니다. merge의 중간 과정일 뿐이다. conflict가 발생하면 git은 유저의 working directory(유저가 작업중인 환경)에 있는 파일에 conflict가 발생한 부분을 >>>>, ====, <<<< 으로 마킹을 한다. 이후 git 내부적으로 현재 merge가 진행중이며 유저가 conflict를 해결하고 있다는 정보를 저장한다. 유저가 conflict를 해결하고 commit을 보내면 conflict를 해결중이라는 이 내부 정보를 확인하여 이번 commit을 merge commit으로 여기며 이번 commit의 부모 commit으로 merge에 사용된 2개의 commit을 등록하여서 git log --graph 명령어를 통해 git log를 확인하면 merge가 발생한 것을 확인할 수 있도록 한다.

Rebase

개발을 하던 도중 어떤 식으로 개발을 진행하면 좋을지 실험해보기 위해 여러 분기를 만들어 실험하고 결국에는 하나의 분기를 선택할 것이다. Rebase는 어떤 분기를 복사하여 그 분기의 commit들을 그대로 다른 분기에 붙여넣는 명령어이다.

Rebase 사용 방법

git rebase main

두 분기를 통합시키는 명령어로 Merge 외에 Rebase라는 명령어도 있다. 위의 명령어를 실행하면 현재 Branch가 가리키고 있는 commit을 복사하여 main Branch 밑에 새로운 commit으로 추가시킨다.

Rebase가 Merge와 다른 점

Rebase와 Merge 모두 두개의 분기를 하나로 통합시킨다는 공통점이 있지만, Rebase는 하나의 분기를 복사하여 다른 분기에 붙여넣는 개념이라면 Merge는 두개의 분기를 하나의 분기로 이어붙이는 개념이다.

.gitignore

Git 저장소를 만들면 .gitignore라는 숨김파일이 만들어진다. 이 파일에 추가된 디렉토리나 파일들은 untracked인 경우 staged가 되는 것을 방지하고 untracked인 것을 더이상 추적하지 않는다.

.gitignore을 적용시키기 위해서는

개발을 하다가 중간에 추적하지 않았으면 하는 파일을 .gitignore에 적용시키더라도 git은 계속해서 그 파일을 추적할 것이다. 왜냐하면 .gitignore의 작동원리는 .gitignore에 등록된 파일이 untracked인 경우 staged가 되는 것(git에 등록되는 것)을 방지하는 것이지 이미 unmodified, staged 상태의 파일인 경우 git에 의해 변경사항을 추적당하는 것은 막지는 못한다.

git을 이용하다보면 working directory에 있는 전체 파일을 stage area로 옮기는

git add .

명령어를 자주 사용하는데 이때 git이 추적하지 않았으면 하는 파일이 추적당하는 것을 방지하기 위해 주로 사용한다.

그렇기 때문에 이미 git에 등록된 파일들의 경우 더이상 git에 의해 추적되지 않게 하기 위해서는 해당 파일을 untracked 상태로 바꾸어야 한다.

git rm -r --cached <해당 파일>

명령어로 해당 파일만 untracked 상태로 바꿀 수 있다.
또한,

git rm -r --cached .

명령어로 모든 파일을 untracked로 바꾼 다음 git add . 명령어를 통해 .gitignore에 등록된 파일들을 제외한 나머니 파일들을 git에 등록할 수도 있다.

Remote Git

Clone

git clone {URL}

git clone은 Github의 코드들을 로컬에 다운받는 명령어로 단순히 다운받는 것에서 그치지 않고 Github에 등록된 git 정보들(log, branch)도 불러와서 git 작업환경을 만들어준다.

Remote Branch (origin/main)

원격 Branch는 가장 최근 Github와 로컬 Git을 동기화하였을 때 Github의 main Branch가 가리키던 commit에 해당하는 로컬 commit을 가리킨다. 더 쉽게 말하자면, Github와 가장 최근 동기화가 완료된 로컬 commit을 나타내는 branch이다.

원격 Branch는 일반적인 로컬 Branch와는 다른 저만의 특징이 있다.

Remote Branch를 두는 이유

Remote Branch를 따로 두는 이유를 생각해보면 Remote Branch의 특징을 자연스럽게 생각해볼 수 있다.

로컬 Git을 두고 있는 환경이 항상 인터넷에 연결되어있을리도 없고 일정 시간마다 Github를 체크하는 것은 트레픽 낭비이다. 그래서 로컬 Git이 Github와 연결되는 경우는 push, pull, fetch 명령어를 통해 로컬 Git이 Github와 연결될 때이다. 따라서, Remote Branch를 두어 로컬 Git에서 원격 Github와 어디까지 동기화되어있는지를 나타낸다.

Remote Branch의 특징

Chekcout 명령어를 통해 HEAD를 원격 Branch로 이동시키려고 해도 HEAD가 원격 Branch가 아닌 원격 Branch가 가리키던 commit을 가리킨다. 따라서 원격 Branch를 Local Git에서 혼자 옮길 수는 없다. 그리고 원격 Branch를 옮기기 위해서는 Github와 통신을 해야 한다.

Remote Branch가 존재하는 이유를 생각해보면 자연스럽게 생각해낼 수 있다.

Fetch, Pull

Fetch

git fetch

Fetch 명령어는 Github의 commit을 로컬 Git에 다운로드하는 명령어이다.

앞서 설명한대로 원격 branch는 Github와 가장 최근 동기화된 commit을 가리킨다. 즉, 원격 branch까지는 로컬 Git과 Github의 내용이 동기화되어있다는 것이다. 그리고 우리는 fetch 명령어를 통해 Github의 main branch가 가리키는 commit을 다운로드 하고 싶다. 따라서 원격 branch의 자식 commit부터 Github의 main branch까지의 직계 commit들을 원격 branch 아래에 저장된다. 직계 commit들만을 저장하는 이유는 그 외의 commit들은 원격 branch와 main branch를 잇는데 필수적이지 않기 때문이다.

Pull

git pull;
git fetch; git merge origin/main;

pull 명령어는 단순히 fetch와 merge를 합친 명령어이다.

왜 굳이 두 명령어를 합쳐 pull을 만들었을까? fetch 명령어만 이용하면 원격 branch는 최신화되지만 main branch는 동떨어지게 된다. 로컬 Git에서는 원격 branch가 아닌 main branch를 이용하기 때문에 main branch가 있는 분기를 원격 branch가 있는 분기와 합친다는 개념으로 생각하면 편할 것 같다.

git pull --rebase;

fetch 이후 merge가 아닌 rebase를 통해 분기들을 합칠 수도 있다.

Push

git push

push 명령어는 로컬 Git의 commit을 Github로 업로드할 때 사용한다.

앞서 설명한대로 원격 branch는 Github와 가장 최근 동기화된 commit을 나타낸다. 그리고 우리는 push 명령어를 사용하는 순간 HEAD가 가리키는 branch를 업로드하려고 한다. 그렇기 때문에 push 명령어를 사용하면 원격 branch의 자식 commit들부터 main branch까지의 직계 commit들을 Github의 main branch 아래에 업로드한다. 직계 commit들만을 업로드하는 이유는 그 외의 commit들은 원격 branch와 main branch를 잇는데 필수적이지 않기 때문이다.

로컬 Git과 Github의 conflict

앞서 말했듯이 로컬 Git과 Github는 항상 연결되어있지 않고 로컬 Git이 push, pull, fetch 등의 명령어로 Github에 연결을 요청할 때에만 연결된다. 또한, Github는 나 뿐만 아니라 동료 개발자들과 함께 사용하는 저장소이기 때문에 로컬 Git과 Github의 commit이 아래의 사진처럼 서로 다를 수 있다.

이런 상황을 바탕으로 pull을 사용했을 때와 push를 사용했을 때 conflict가 발생하는 상황이 다르다.

pull 명령어를 사용했을 때 발생하는 conflict

pull 명령어를 사용하는 도중 충돌이 발생할 경우 fetch는 단순히 commit들을 다운로드하는 과정이기 때문에 왠만하면 conflict가 발생하지 않는다. 하지만 merge를 수행하는 과정에서 conflict가 발생할 수 있다.

push 명령어를 사용했을 때 발생하는 conflict

push를 하다가 conflict가 발생하는 근본적인 이유는 push하려는 commit의 내용이 다르기 때문이다. 그리고 commit의 내용이 다른 이유는 부모 commit이 다르기 때문이다.

C3는 C3인 것이지 부모 commit이 다르다고 commit의 종류가 달라진다는 것이 이해가 안될 수도 있다. 이 포스트의 초반부에서 설명했듯이 Git은 새로운 commit을 만들 때 부모 commit과의 차이점만을 저장함으로써 저장공간을 절약한다. 따라서 어떤 부모 commit을 가지고 있냐에 따라 자식 commit이 저장하고 있는 부모 commit과의 차이점이 다르기 때문에 부모 commit이 무엇이냐에 따라 commit의 내용이 달라질 수 있다는 것이다.

이런 충돌을 해결하기 위해서는 fetch, rebase, merge를 이용하여 부모 commit의 내용을 동일하게 만들어주어야한다.

profile
HGU 20 김민석

0개의 댓글