0. 서론

갑작스럽지만, 대학생이 되어 워드 프로그램을 켜고 레포트를 작성한다고 가정해 보자. 작성 중 이전에 작성한 글이 더 나았다는 생각이 들거나, 작업 중 무언가 문제가 생기면 워드에서 언제든지 Ctrl+z를 통해 글의 내용을 이전 상태로 되돌릴 수 있고, 다른 이름으로 복사를 통해 동일한 내용의 같은 파일을 둘 이상 생성할 수도 있다. 프로그래밍 세계에서도 이와 같은 개념이 존재하는데, 바로 버전 관리(Version Control)이다.


버전 관리는 혼자서 프로그래밍을 할 때도 유용하지만 둘 이상의 프로그래머가 협업을 할 때 그 진가를 발휘하는데, 각자의 브랜치를 따서 작업을 하거나, 다른 이들의 코드를 비교하며 더 나은 품질의 제품을 만들 수 있도록 도와주기도 하기 때문이다.


이러한 버전 관리는 그 구조에 따라 몇 가지로 분류되는데, 그 중 Git이라는 분산 버전 관리 시스템은 많은 프로그래머들에게 사랑받고 있는 버전 관리 방식이다.


그러나 이 Git이라는 도구는 처음 사용하는 과정에서 그 난해함과 무한 충돌에 의해 많은 프로그래머들을 좌절시키기도 하는데, 이는 Git의 버전 관리 구조를 제대로 숙지하지 못한 프로그래머의 탓이 훨씬 크다.


특히 이러한 미숙한 프로그래머가 팀과 협업을 진행할 경우 실수로 프로젝트의 버전 이력을 망치거나 작업을 해야 할 귀중한 시간을 Git 관리 실수를 만회하는 데 낭비하는 모습을 보이게 된다. 물론 이러한 실수가 잘못은 아니다. 모든 프로그래머들은 Git에 적응하는 과정에서 이러한 실수를 수도 없이 겪었으니까. 하지만 결국 프로젝트 전체의 진행 속도가 늦어지는 것은 사실이며, 실수를 한 프로그래머가 그날 밤 몸을 비틀며 이불을 뻥뻥 차게 될 것이라는 것도 사실이다.


따라서 이번 포스트에는 그 동안 다양한 협업을 진행하면서 팀원들에게 알려주고 싶었던 Git의 구조와 명령어, 그 용법을 도식과 함께 소개해 보고자 한다.


만약 이 글을 보며 직접 Git을 실습해 보고 싶다면, GUI형태의 깃 클라이언트 대신 CLI형태의 깃 클라이언트(대표적으로 Git Bash, 터미널 등)를 이용해 보는 것을 권장한다.

참고로 작성자는 Windows11 환경에서 Windows Terminal을 이용하였다.


1. TL;DR

이번 포스트에서는 아래 개념들을 알아 볼 것이다.

  • Git
  • 이력
  • git init
  • git add
  • git status
  • git commit
  • git log
  • commit id
  • HEAD

2. 시작

Git은 분산 버전 관리 구조를 이용하는데, 이는 각 사용자들이 자신의 로컬 저장소(Repository)를 생성해 작업을 진행하고, 완료한 작업을 공용 서버 저장소에 업로드하는 구조를 지닌다.

따라서 가장 먼저 할 일은 Git 로컬 저장소를 생성하는 일이다.

원하는 폴더에서 터미널을 켠 후, 아래 명령어를 입력해 보자.

git init

다음과 같은 결과가 나왔다면 성공적으로 빈 로컬 저장소가 생성된 것이다!

>git init
Initialized empty Git repository in C:/xxx/.git/	

이제 로컬 저장소는 아래와 같은 상태가 되었다.

하얀 바탕에 main / main branch라는 글씨가 써져 있다(참고로, 로컬 환경에 따라 main이 아닌 master로 생성되었을 수도 있다. 이를 변경하는 방법은 구글링으로 매우 쉽게 찾을 수 있으므로 이 포스트에서 다루지 않겠다. 또한 브랜치라는 개념은 추후 포스트에서 살펴보겠다. 지금은 그냥 현재 작업을 하고 있는 구역이라고만 생각하자).

당연하다. 우리는 아직 로컬 저장소를 생성하기만 했을 뿐 어떠한 이력도 저장하지 않았으니 말이다.

잠깐, 여기서 혹시 의문을 가진 사람이 있을 것이다. 이력이라니? 저장은 보통 파일을 저장하는 것이 아닌가?

여기서 한 가지 중요한 개념을 설명하고 가야 한다. 바로 이력이다.

2.1. 이력

Git은 이력을 관리하는 툴이다. 즉, 그 저장소에서 현재와 이전의 차이를 기록한다는 뜻이다.

아래 그림을 보자.

로컬 저장소에 왼쪽과 같이 index.htmlinput.txt가 존재했다고 가정하자. 이 때 사용자가 input.txt를 삭제하고 app.py를 추가한 후 Git에 이력을 저장한다면 Git은 두 파일을 저장하는 것이 아닌 기존 상태와 새로운 상태의 차이인

- input.txt
+ app.py

를 저장한다는 것이다.

이러한 특징으로 인해 실제 각 시점의 파일을 저장하는 것보다 매우 가볍게 버전 관리를 수행할 수 있으며, 이력을 역으로 적용하는 것 만으로 이전 상태로 돌아갈 수 있게 된다.

그럼 이제 예시로 든 상태를 직접 실습해 보기 위해 로컬 저장소를 위 이미지와 동일하게 조성해 보자.

2.2. git add

일단 우선 해야 할 일은 input.txt 와 index.html을 로컬 저장소에 추가하는 것이다. 파일 안에 내용은 비워도 되고, 자신이 원하는 내용을 작성해도 된다.

그 다음 터미널에서 다음을 입력한다.

git add input.txt
git add index.html

각 명령어를 입력하는 과정에서 어떠한 오류 메시지 없이 조용히 처리되었다면 성공이다.

git add 는 저장소에서 변경이 일어난 이력을 커밋을 위해 포장(Staging)하는 명령어이다. 즉,

git add input.txt

는 input.txt라는 파일에 일어난 변경 이력을 커밋하기 위해 Staging한다는 의미이다. 중요한 점은 add input.txt가 input.txt 파일 자체를 add하는 것이 아니라는 것이다!

즉, 만약test.cpp라는 파일이 로컬 저장소에서 삭제되었다 하더라도 우리는

git add test.cpp

를 이용할 수 있으며, 이 경우 test.cpp가 삭제되었다는 이력이 Staging된다.

만약 위와 같이 파일 하나하나를 staging하는 게 번거롭다면 다음 명령어를 이용해 변경 이력 전체를 추가할 수도 있다.

git add .

2.3. git status

이제 아래 명령어를 터미널에 입력해 보자.

git status

그럼 아마 아래와 같은 응답이 터미널에 출력될 것이다.

> git status

On branch main

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
  		new file: 	input.txt
  		new file:	index.html

>

이는 현재 브랜치가 main 브랜치이고, 커밋을 진행한 적이 없으며, 커밋되기 위한 변경 이력으로 input.txtindex.html이 저장소에 추가(stage)되었다는 것을 뜻한다.

이 실습에서 알 수 있듯 git status라는 명령어는 현재 브랜치가 무엇인지, 커밋을 얼마나 수행했는지와 같은 개괄적인 상태와 커밋을 위해 포장된(Staging) 이력들을 한눈에 확인할 수 있게 해준다.

2.4. git commit

git status를 통해 staging이 정상적으로 진행된 걸 확인했으니 이제 이력을 실제 저장소에 반영해 보자.

git add는 이력을 커밋하기 위한 준비일 뿐 실제 저장소에 반영하기 위해선 커밋을 수행해야만 한다.

아래 명령어를 터미널에 입력해 보자.

git commit -m "First Commit"

그럼 다음과 같은 응답이 터미널에 출력될 것이다.

> git commit -m "First Commit"

[main (root-commit) f40fe2d] First Commit
 2 files changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 index.html
 create mode 100644 input.txt

>

2개의 파일 변경 이력이 커밋되었고, 그 중 0개 파일의 내용이 증가(insertions)하였으며 0개의 파일은 내용이 감소(deletions)하였다 라고 알려준다.

이는 커밋이 잘 완료되었다는 뜻이기도 하다.

이제 정상적으로 변경 이력이 저장소에 추가되었다!

잠시 add부터 commit까지의 과정을 도식으로 정리해 보자.

이력을 커밋하는 과정은 택배를 보내는 과정에 비유할 수 있다.


git add는 택배를 보낼 물품(이력)들을 배송 박스에 포장하는 과정이며, git commit은 포장한 배송 박스를 택배사에 전달하는 과정이다.


택배사에 전달한다면 특별한 일이 없는 이상 물품이 도착지까지 전달될 것이므로, 이는 이력이 저장소에 잘 적용된다는 것을 의미한다.


또한, 택배를 보낼 때 운송장에 배송 메모를 작성하듯이 각 커밋에도 해당 커밋이 어떠한 작업에 대한 커밋인지를 사용자가 직접 메모를 남길 수 있는데, 이러한 메모를 커밋 메시지(Commit Message)라 하고 앞서 예시에서의 “First Commit”이 바로 커밋 메시지이다.

2.5. Commit ID, git log

커밋이 완료되었다면 이제 로컬 저장소는 아래와 같은 상황이 되었다.

main 브랜치에 첫 번째 커밋을 의미하는 C1 커밋이 추가되었으며, 이는 작성자가 임의로 붙인 이름이다.

실제 각 커밋들은 자신만의 고유한 hash형태의 ID를 가지는데, 이를 Commit ID라 한다.

커밋 id를 확인하기 위해선 해당 커밋 로그를 살펴보면 되는데, 커밋 로그를 살펴보려면 터미널에 아래와 같이 입력하면 된다.

git log

말 그대로 로그를 출력하라는 의미이며, 아래와 같은 응답이 나올 것이다.

> git log

commit f40fe2d0d9cc430ef0d1fccd14c0c8f26171e037 (HEAD -> master)
Author: Im Yongsik <lvhi7121@gmail.com>
Date:   Wed Sep 14 12:54:58 2022 +0900

    First Commit

>

여기서 commit f40fe2d0d9cc430ef0d1fccd14c0c8f26171e037 에 적혀있는 f40fe2d0d9cc430ef0d1fccd14c0c8f26171e037이 바로 해당 커밋의 id이다.


너무 길어서 이걸 대체 어떻게 쓸까 싶지만, 실제로는 커밋 아이디의 앞 6~7글자만 명시하면 사용할 수 있다. 즉, 위 예시에서 실제 커밋 ID를 사용할 때에는 f40fe2 까지만 입력해 주면 된다는 뜻이다.

위 이미지는 실제 Github상에서 커밋 메시지가 표시된 모습을 나타내는데, 노란색으로 밑줄 쳐진 7글자(6306d34)가 커밋 ID를 나타내고 있다.

3. Git의 이력 관리 구조와 HEAD

이제 맨 처음 예시와 같은 상황을 만들기 위해 마저 진행해 보자. 여기서는 지금까지 포스트를 잘 읽었다면 스스로 할 수 있을 테니 직접 해 보는 것도 좋다.

우리가 이제 해야 할 일은 로컬 저장소에서 index.html을 삭제하고 app.py파일을 추가한 후 다시 한 번 커밋하는 것이다.

정답은 아래와 같다.

git add index.html
git add app.py
git commit -m "Second Commit"

이제 git log를 입력하면 아래처럼 나타날 것이다.

> git log

commit 66163a2e3ba5350bec08b6990b8e19425013e676 (HEAD -> master)
Author: Im Yongsik <lvhi7121@gmail.com>
Date:   Wed Sep 14 13:22:55 2022 +0900

    Second Commit

commit f40fe2d0d9cc430ef0d1fccd14c0c8f26171e037
Author: Im Yongsik <lvhi7121@gmail.com>
Date:   Wed Sep 14 12:54:58 2022 +0900

    First Commit
    
>

이제 맨 처음 예시와 동일한 상태가 되었다! 이를 도식으로 나타내면 다음과 같다.

C1커밋 뒤에 C2커밋이 추가되었고, C2커밋이 C1커밋을 가리키고 있다.

여기서 만약 임의의 커밋을 한번 더 진행하면 어떻게 될까? 바로 아래처럼 될 것이다.

여기서 알 수 있는 것은 커밋을 진행할 때마다 해당 커밋은 커밋 직전의 상태 뒤에 연결되게 된다.

이는 마치 트리 자료구조의 형태를 가지는데, 직전 상태가 부모 노드이고 커밋 후 상태가 자식 노드가 되는 것이다.

이러한 점에서 Git은 트리 구조를 띈다고 할 수 있다.

그리고 더 살펴볼 점이 main 화살표와 HEAD 화살표인데, main화살표는 실제론 main브랜치의 리프 노드를 가리키는 포인터이고, HEAD 화살표는 현재 로컬 저장소에 적용된 이력을 가리키는 포인터이다.

따라서 만약 우리가 추후 배울 기능들을 이용해 로컬 저장소의 상태를 C3커밋 이후에서 C1커밋 이후 상태로 되돌린다면 아래처럼 될 것이다.

HEAD에 대한 내용은 앞으로도 몇번 더 다룰 예정이지만, 이 HEAD를 옮기는 것으로 우리는 언제든지 특정 시점의 저장소 상태로 돌아갈 수 있게 된다.

이제 조금은 Git의 구조가 이해가 되었으리라 생각한다.

4. 결론

Git의 기초적인 개념들을 설명하고자 글을 작성했는데, 머리에 있는 내용을 글로 쭈욱 옮기다 보니 혹시나 너무 난해하게 설명해 이해가 어려운 내용이 있다면 댓글을 통해 지적해 주길 바란다.


또, Git을 빠르게 배우는 방법은 글쓴이 개인적으로는 불편하더라도 CLI형태의 Git Client를 이용하며 프로젝트를 진행해 보는 것이라고 생각한다. 따라서 이 포스트를 통해 실습을 해 보는 동안만큼은 편한 GUI 대신 터미널과 같은 CLI 형태 클라이언트를 사용해 보는 것을 적극 권장한다.