GitHub는 소프트웨어 개발 프로젝트를 위한 소스코드 관리 서비스이다
협업시 필수인 소스 코드 열람과 더불어 간단한 버그 관리 및 SNS기능까지 지니고있어
개발자에겐 필수 서비스로 자리잡고있다.
요즈음 스타트업이나 기업에서는 GitHub를 사용한 버전관리를 실시하는 곳도 다수 존재한다고한다.
github링크
https://github.com/
기초적인 개념 알아보기
Git은 분산 버전 관리 시스템이기 때문에 리모트 서버에 있는 소스를 수정하려면 로컬 환경으로 소스를 클론(Clone)하는 과정이 필요하다. 말 그대로 모든 소스를 복사하여 사용자의 컴퓨터로 받아오는 것이다.
이후 Git은 로컬 환경의 파일을 추적하고 있다가 사용자가 소스를 수정하면 그 변경 사항을 감지한다. 그 후 사용자는 자신이 리모트 서버에 변경 사항을 반영하고 싶은 파일이나 소스 코드의 라인을 고른 뒤 리모트 서버에 업로드한다.
source tree
맨 위의 origin/master
는 리모트 서버의 버전, 맨 밑의 master
는 필자 컴퓨터의 버전을 의미한다.
자 일단 Git에 대한 기본적인 개념은 이게 끝이다. 리모트 서버에 있는 파일을 내 컴퓨터로 복붙한 다음 수정해서 다시 리모트 서버로 업데이트한다는 것. 이때 사용자가 자신이 변경한 로컬의 소스를 서버의 소스에 업로드하는, 즉 서버로 밀어올리는 행위를 Push라고 부르고 사용자가 서버의 소스를 자신의 클라이언트로 가져오는 행위를 Pull 또는 Fetch라고 하는 것이다. 쉽지 않은가?
하지만 처음 Git을 접하면 평소에 접해보지 못했던 remote, origin, repository와 같은 용어들이 튀어 나오기 때문에 당황할 수 있다. 그럼 이 용어들이 무엇을 뜻하는지부터 간단하게 알아보자.
Remote / Origin
우선 Remote는 말 그대로 리모트 서버 자체를 의미한다. 이 리모트 서버라는 개념이 잘 이해가 안되시는 분은 우리가 자주 사용하는 구글 드라이브나 N드라이브와 같은 클라우드 스토리지를 사용하는 것을 떠올리시면 된다. 전 세계 어딘가에 있는 서버에 우리의 소스를 저장하는 것이다.
이때 이 서버를 제공해주는 대표적인 업체가 Github, Bitbucket, GitLab과 같은 회사들이다. 이 회사들이 Git을 만든 게 아니라 Git이라는 시스템에 필요한 리모트 서버와 Git을 좀 더 편리하게 사용할 수 있는 기능들을 제공하는 것이다.
Git을 사용할 때는 내가 어떤 리모트 서버에 변경 사항을 업로드 할 것인지 정해야하는데, 반드시 하나의 리모트 서버만 사용할 수 있는 것이 아니기 때문에 내가 사용하는 리모트 서버의 이름을 정해줘야한다. 이때 주로 사용하는 관례적인 이름이 바로 Origin이다.
보통은 한 개의 리모트 서버만 운용하는 경우가 대다수이기 때문에 많은 사람들이 Remote와 Origin을 혼용해서 부르곤 한다.
Repository
레파지토리(Repository, Repo)는 저장소라는 뜻으로, 리모트 서버 내에서 구분되는 프로젝트 단위라고 생각하면 된다. 우리가 구글 드라이브를 사용할 때도 하나의 디렉토리에 모든 파일을 다 때려넣지않고 몇 개의 디렉토리를 만들고 용도에 따라 파일을 나눠서 구분하는 것과 동일하다.
일반적으로 한 개의 레파지토리는 하나의 프로젝트를 의미하지만 경우에 따라서 레파지토리 하나에 여러 개의 프로젝트를 구성하기도 한다.
https://github.com/user/repository.git
https://user@bitbucket.org/group-name/repository.git
레파지토리를 클론받을 때는 해당 레파지토리를 가리키는 URL이 필요한데, 레파지토리의 이름은 URL의 맨 마지막에 .git 확장자를 가지는 방식으로 표현된다.
Branch
브랜치는 일종의 독립된 작업을 진행하기 위한 작업 공간의 개념이다. 맨 처음 Git을 초기화했을 때 기본적으로 master라는 이름의 브랜치가 하나 생성된다. 그 후 개발하는 기능 또는 버그 픽스에 따라서 브랜치를 새로 생성하고 거기서 작업한 후에 나중에 다시 master로 합치는 것이다.
branches
master 브랜치에서 다른 브랜치를 분리한 모습
이 브랜치 개념은 Git에 익숙하지 않은 분들에게 잠깐 설명하고 넘어가기에는 직관적으로 이해가 잘 안될 수 있는 개념이기 때문에 추후 다른 포스팅에서 다시 설명하겠다. 일단 지금은 이 3가지 정도만 기억해두자.
Git을 초기화하면 기본적으로 master 브랜치가 생긴다. 이 친구가 메인 브랜치 역할을 한다.
브랜치는 어떤 브랜치에서 분리시키는 것이고, 분리된 브랜치는 분리될 당시의 부모 브랜치 상태를 그대로 가지고 있다.
개발자는 각각의 브랜치에서 개발을 진행한 뒤 나중에 다시 master 브랜치로 변경 사항을 합친다.
필수 명령어들을 알아보자
만약 여러분이 혼자서만 프로젝트의 버전 관리를 한다면 단순히 리모트 서버의 레파지토리에서 소스를 받아와서 변경한 후 다시 리모트 서버로 업로드하는 과정만으로도 프로젝트를 진행하는데는 사실 아무 문제가 없다.
하지만 Git은 애초에 혼자서 개발하는 상황보다는 여럿이서 함께 소스를 수정하며 개발하는 협업 상황을 상정하고 만들었기 때문에 협업에서 발생할 수 있는 여러가지 곤란한 상황들을 타파하기 위한 많은 기능을 가지고 있다.
Git은 기본적으로 CLI(Command Line Tools)을 통해 사용하고 commit, fetch, branch와 같은 여러가지 명령어를 사용하여 이 기능들을 사용할 수 있게 해준다.
그럼 이번에는 Git을 사용하여 버전을 관리하기 위해 기본적으로 알아야 하는 몇 가지 명령어를 한번 살펴보자.
리모트 서버와 연동하기
clone
clone은 말 그대로 리모트 서버의 레파지토리에서 클라이언트로 파일을 복붙하는 행위를 말한다. 이때 클론을 수행하기 위해서는 어떤 레파지토리에서 파일을 가져올 것인지에 대한 정보가 필요한데, 이 정보는 위에서 설명했듯이 URL로 표현한다. HTTPS 프로토콜이나 SSH 프로토콜을 사용하여 소스를 클론할 수 있는데, 보통 HTTPS를 많이 사용한다.
clone
보통 Github과 같은 리모트 서버 제공업체들은 레파지토리를 쉽게 클론할 수 있도록 눈에 잘 띄는 버튼을 만들어 놓고 해당 레파지토리의 URL을 제공하는 경우가 많다. 사용자는 단지 저 URL을 복사한 다음 Git의 clone 명령어를 사용해서 레파지토리를 클론하기만 하면 된다.
$ cd ~/dev/evan # 원하는 작업 디렉토리로 이동
$ git clone https://github.com/evan-moon/test-repo.git
원하는 작업 디렉토리로 이동한 뒤 clone 명령어를 사용하여 레파지토리를 클론하게되면 현재 위치에 레파지토리 이름과 동일한 디렉토리가 생성되고 그 내부에 리모트 서버의 소스가 전부 복사된다. 위 예제의 경우 ~/dev/evan 디렉토리 내부에 test-repo 디렉토리가 생성되고 해당 레파지토리의 소스가 복사될 것이다.
이제 이 복사된 소스를 맘대로 수정하거나 파괴해도 리모트 서버에 업로드만 하지 않는다면, 같은 리모트 서버를 보고 있는 다른 사람이 영향을 받을 일은 절대 없기 때문에 안심하고 맘대로 만지작거려도 된다.
pull
pull 명령어는 리모트 서버의 최신 소스를 가져와서 로컬 소스에 병합(Merge)해주는 명령어이다. 만약 우리가 처음 소스를 클론한 후에 다른 사람이 리모트 서버를 상태를 갱신했더라도 리모트 서버가 우리에게 그 변경된 사항을 알려주지는 않기 때문에 우리가 직접 서버에 문의를 날려야 하는 것이다.
또한 pull은 단순히 리모트 서버에서 로컬로 소스를 가져온다의 개념보다는 가져와서 합친다의 개념이기 때문에 브랜치끼리도 pull을 통해 소스를 합칠 수 있다.
$ git pull # 현재 내 로컬 브랜치와 같은 이름을 가진 리모트 서버 브랜치가 타겟
$ git pull origin master # origin 리모트 서버의 master 브랜치가 타겟
오픈소스에 관심이 많은 분이라면 Pull Request라는 단어를 들어보았을 것이다. 이 Pull Request는 “내가 작업한 브랜치를 가져가서 합쳐줘~“라는 의미이다.
필자는 처음에 이게 왜 Merge Request가 아니라 Pull Request인지 이해가 잘 안갔었는데, 나중에 와서 생각해보니 최종적으로 두 브랜치의 소스를 합치는 행위를 하는 주체가 요청을 한 사람이 아니고 요청을 받은 사람이기 때문에 요청을 받은 사람이 브랜치를 가져가서 합친다는 관점에서 보면 적절한 네이밍인 것 같다는 생각을 했었다.
fetch
fetch는 리모트 서버의 최신 이력을 내 클라이언트로 가져오되 병합은 하지 않는 명령어이다.
$ git fetch
fetch 명령어를 사용하면 다른 사람들이 리모트 서버에 새로 업데이트한 모든 내역을 받아올 수 있다. 이제 그 내역을 보고 내 로컬에 있는 버전이 리모트 서버에 있는 버전보다 이전 버전이라면 pull 명령어를 사용하여 내 컴퓨터의 소스 코드를 갱신하면 된다.
그럼 이 명령어가 pull의 하위 호환이 아닌가? 라는 생각이 들 수도 있는데, pull과 fetch는 조금 용도가 다르긴 하다. pull 같은 경우는 일단 묻지도 따지지도 않고 바로 리모트 서버의 최신 소스를 가져와서 내 로컬 소스에 합쳐버리기 때문에 조금 위험하긴 하다. 뭐 예를 들면 지금 리모트 서버의 최신 소스가 버그가 있는 상태일 수도 있지 않은가?
그래서 필자같은 경우 보통 로컬 소스와 리모트 소스의 변경 사항을 미리 비교해보고 싶을 때 fetch를 사용한다. 그리고 fetch를 잘 이용하면 이런 얌생이도 가능하다.
#!/bin/bash
git fetch --all -p; git branch -vv | grep ": gone]" | awk '{ print $1 }' | xargs -n 1 git branch -d
이 쉘스크립트는 필자가 예전에 만들어 놨던 친구이다. fetch를 통해 리모트 서버의 최신 내용을 받아온 뒤, branch 명령어를 사용하여 리모트 서버에서는 삭제되었지만 로컬에는 남아있는 브랜치를 찾아서 싹 다 지워주는 스크립트이다. 참고로 로컬에는 있지만 리모트에서 삭제된 브랜치는 브랜치 이름 뒤에 : gone이라는 문구가 붙어있기 때문에 구분이 가능하다.
$ git branch -vv
* master fa0cec5 [origin/master] 마스터 브랜치에욤
test 1f3578f [origin/test: gone] 리모트에선 죽은 브랜치
test2 fa0cec5 로컬에서 만들어지고 리모트에 업데이트는 안된 브랜치
fetch 명령어와 branch 명령어의 특성을 잘 이용하면 이런 꿀 스크립트를 만들 수도 있다.
변경 사항을 리모트 서버에 업데이트하기
자, 지금까지는 리모트 서버의 내용을 로컬과 연동하는 명령어를 살펴봤다면 이제는 내 로컬에서 변경한 소스를 리모트 서버로 업로드하는 명령어들을 살펴볼 차례이다. 필자는 이 과정을 설명할 때 보통 택배로 예를 주로 드는 편이기 때문에 이 포스팅에서도 택배를 포장하고 배송하는 과정에 빗대어서 설명을 진행하겠다.
add
add
원하는 변경사항만 골라 담는 add 명령어
자, 평화로운 중*나라에서 중고 거래를 했다고 생각해보자. 물론 집에 있는 모든 물건을 보내는 혜자같은 분도 계시겠지만 일반인이라면 그러지 않기 때문에 우리는 상대방한테 물건을 보내기 전에 어떤 물건을 보낼 것인지 부터 정해야한다. 이때 add 명령어가 어떤 물건들을 포장할 것인지 고르는 과정을 담당한다.
$ git add . # 현재 디렉토리의 모든 변경사항을 스테이지에 올린다
$ git add ./src/components # components 디렉토리의 모든 변경사항을 스테이지에 올린다
$ git add ./src/components/Test.vue # 특정 파일의 변경사항만 스테이지에 올린다
$ git add -p # 변경된 사항을 하나하나 살펴보면서 스테이지에 올린다
이때 선택된 변경 사항들은 스테이지(Stage)라고 불리는 공간으로 이동하게 된다. 이때 git add <경로> 명령어는 해당 경로 안에 있는 변경 사항을 전부 스테이지에 올리게 되는데, 이게 영 불안하다 싶은 사람은 -p 옵션을 줌으로써 변경 사항을 하나하나 확인하면서 스테이지에 올릴 수도 있다.
이렇게 스테이지에 담긴 변경 사항들은 git status 명령어를 사용하여 확인해볼 수 있고, status 명령어에 추가적으로 -v 옵션을 사용하면 어떤 파일의 어떤 부분이 변경되었는지도 함께 볼 수 있다.
$ git add ./soruce
$ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: source/_drafts/git-tutorial.md
commit
commit
변경 사항들을 포장하는 commit 명령어
add를 사용하여 원하는 변경사항을 스테이지에 올렸다면 이제 스테이지에 있는 변경 사항들을 포장할 차례이다. 이때 이 포장하는 행위를 commit이라고 한다. 커밋은 Git에서 상당히 중요한 부분을 차지하는 행위인데, 바로 Git이 하나의 커밋을 하나의 버전으로 정의하기 때문이다. 그렇기 때문에 특정 버전으로 어플리케이션을 변경이라는 기준도 당연히 바로 이 커밋이 된다.
$ git log --graph
* commit 20f1ea9 (HEAD -> master, origin/master, origin/HEAD)
| Author: Evan Moon <bboydart91@gmail.com>
|
| 회원가입 기능 개발 끝!
|
* commit ca693fd
| Author: Evan Moon <bboydart91@gmail.com>
|
| 회원가입 비밀번호 입력 폼 추가
|
* commit f9b6e2d
| Author: Evan Moon <bboydart91@gmail.com>
|
| 회원가입 이메일 입력 폼 추가
|
위의 그래프 상에서 필자의 어플리케이션의 현재 상태는 어떤 커밋일까?
그래프 상에서 HEAD가 20f1ea9 회원가입 기능 개발 끝! 커밋에 위치해 있으므로 현재 필자의 어플리케이션은 회원가입 기능까지 개발이 완료된 상태라는 것을 알 수 있다.
그리고 그래프를 자세히 보면 각각의 커밋들은 20f1ea9과 같은 고유한 해쉬 값을 가지고 있는데, 이 해쉬 값을 사용하여 어떠한 커밋으로든 자유자재로 이동할 수 있다. 예를 들면 회원가입 비밀번호 입력 폼 추가 커밋의 해쉬 값을 사용하여 git checkout ca693fd 명령어로 회원가입 비밀번호 입력 폼이 추가된 시점으로 이동할 수 있다는 것이다. 즉, 시간여행이 가능하다!
이러한 커밋의 기능을 제대로 활용하기 위해서 커밋은 반드시 실행 가능한 단위로 해야한다. 더 쉽게 말하자면 특정 커밋으로 버전을 변경했을 때 어플리케이션이 제대로 실행되지 않고 에러가 발생하면 안된다는 것이다.
그리고 위의 예제에서 볼 수 있듯이 커밋에는 메세지를 담을 수 있다. 이 메세지는 이 커밋으로 인한 변경 사항이 무엇인지 직접적으로 표현해주는 유일한 수단이므로 좋은 커밋 메세지를 작성하기 위한 고민은 필수다. 다행히도 이미 많은 개발자 분들이 좋은 커밋 메세지를 어떻게 작성해야 하는지에 대해 포스팅을 해주셨기 때문에 구글에서 한번 검색해보면 수두룩하게 나온다.
참고로 커밋 메세지는 꼭 영어여야 할 필요는 없다. 조직에 따라서 커밋 메세지를 영어로만 작성하도록 강제하는 룰이 있을 수는 있지만, 사실 커밋 메세지는 결국 커뮤니케이션 수단이므로 언제 누가 보더라도 알아보기 쉽게만 작성하면 장땡이다. 그러니까 영어가 익숙하지 않은데도 불구하고 굳이 영어를 고집할 필요는 없다. 오히려 같이 협업하는 팀원들이 영어에 익숙하지 않다면 그 또한 불필요한 커뮤니케이션 비용이 될 수 있다.
또한 커밋은 아직 리모트 서버에 파일을 전송하는 것이 아니라 사용자의 클라이언트 내에서 수행되는 과정이므로 인터넷에 연결이 되어 있지 않아도 변경 사항을 커밋하는 것은 아무런 지장이 없다.(비행기에서 코딩할 때도 커밋까지는 가능하다!)
사실 필자는 Git 뉴비일 때 이 커밋이라는 단어가 조금 헷갈렸었는데, 많은 개발자 분들이 commit과 push를 같은 의미로 사용하기 때문이었다. 하지만 이 두 명령어는 엄연히 다른 역할을 하기 때문에 되도록이면 구분해서 사용하도록 하자.
push
push
변경 사항들을 리모트 서버로 배송하는 push 명령어
커밋을 통해 포장된 변경 사항들은 push 명령어를 사용하여 리모트 서버로 업로드 된다. 이때는 커밋된 변경 사항들을 실제 리모트 서버에 전송하는 것이기 때문에 반드시 네트워크에 연결이 되어있어야 한다. 그리고 푸쉬할 때 반드시 A 로컬 브랜치는 A 리모트 브랜치에만 푸쉬해야 한다는 룰 따윈 없기 때문에 커밋들을 리모트 서버로 푸쉬할 때는 Git에게 “어떤 리모트 서버의 어떤 브랜치로 푸쉬할 것인지”도 함께 알려줘야 한다.
$ git push origin master # origin 리모트 서버의 master 브랜치로 푸쉬해줘!
근데 이게 브랜치 이름이 master정도면 그래도 브랜치 이름을 매번 입력해줄만 하지만 feature/SD-0000-request-api-refactoring 정도 되면 브랜치 이름을 매번 입력한다는 것이 귀찮을 수도 있다.
그래서 Git은 브랜치를 자동으로 추적할 수 있는 기능 또한 제공해준다.
$ git push --set-upstream origin master
--set-upstream 옵션을 사용하고 처음 한번만 브랜치 이름을 입력해주면 그 이후로는 git push 명령어만 입력해도 자동으로 처음 입력했던 브랜치로 변경 사항을 푸쉬할 수 있다.
이렇게 해서 리모트 서버의 레파지토리에서 소스를 내 컴퓨터에 받아온 뒤 파일을 변경하고, 그 변경 사항을 리모트 서버에 다시 업데이트하는 clone -> 파일 수정 -> add -> commit -> push과정을 한번 살펴보았다.