Git 뉴비를 위한 기초 사용법 - 버전 관리

    Git 뉴비를 위한 기초 사용법 - 버전 관리


    이번 포스팅에서는 저번 포스팅인 Git 뉴비를 위한 기초 사용법 - 시작하기에서 설명했던 기본적인 명령어보다 좀 더 나아가서 몇 가지 개념과 명령어를 더 공부해보려고 한다. 저번 포스팅에서는 리모트 서버에서 소스를 클론하고 수정한 후 다시 리모트 서버에 업데이트하는 과정에 대해 집중해서 설명했다면 이번 포스팅에서는 Git의 메인 주제인 버전 관리에 대해 더 다뤄볼 예정이다.

    그럼 저번 포스팅과 마찬가지로 간단한 용어와 개념에 대한 설명한 후, 그 개념들을 사용하기 위한 명령어를 설명하도록 하겠다.

    용어와 개념 알아보기

    Git은 버전을 효율적으로 관리하기 위해 몇 가지 개념을 제시하고 있다. 현재 나의 버전 상태를 의미하는 HEAD, 작업 공간인 브랜치(Branch), 브랜치를 합치는 머지(Merge)리베이스(Rebase)등이 그렇다. 그리고 이런 기능들을 사용하다보면 가끔 Git의 에러와 마주하게 되는데, 필자같은 경우는 처음 개발자로 일을 시작했을 때 코딩하다가 발생하는 에러보다 Git에서 발생하는 에러가 더 무서웠던 기억이 있다. 솔직히 코딩하다가 나는 에러는 고치는 과정이 쉽든 어렵든간에 그냥 필자가 고치면 해결되지만, Git에서 나는 에러는 고친답시고 이것 저것 건드리다가 잘못 건드리면 왠지 소스가 가루가 되어 날아갈 것 같아서 무서웠다.

    하지만 Git을 사용한 지 6년쯤 지난 지금 다시 그때를 생각해보면 Git에 대해서 잘 몰랐기 때문에 더 무서웠던 것 같다. 내가 어떤 기능을 사용했을 때 소스가 어떻게 되는 지 정확히 알고 있지 않은 상태였기 때문에 그런 걱정도 들었던 것이다. 혹시 이 글을 읽는 독자 분들 중에서도 필자와 같은 경험이나 생각을 하신 분들이 있을 것이기 때문에 필자가 알고 있는 Git의 개념들을 최대한 알기 쉽게 풀어보려고 한다.

    Merge Conflict

    필자 생각에 Git을 사용하다가 가장 많이 마주치는 에러는 아무래도 머지 컨플릭트(Merge Conflict)인 것 같다. 에러라고 하기에는 조금 애매하긴 하지만 어쨌든 정상은 아닌 상황이기 때문에 처음 마주하면 굉장히 당황스럽고 뭐가 뭔지 헷갈린다. 컨플릭트는 말 그대로 소스의 충돌이 발생한 상황이기 때문에 주니어든 시니어든 가리지 않고 평등하게 발생하고, 또한 평등하게 당황하게 된다.

    왜냐면 컨플릭트는 논리적인 에러가 아니라 내 작업물과 다른 사람의 작업물이 충돌한 상황이기 때문에 스스로 혼자 해결하기 힘든 경우가 많기 때문이다. 그럼 컨플릭트, 즉 충돌이 정확히 어떤 상황을 말하는 지 알기 위해 실제 협업 상황에서 발생할 수 있는 예시를 함께 살펴보자.

    철수영희는 사장님으로부터 지각한 사람의 명단을 만들어서 관리해 달라는 부탁을 받았다. 그래서 철수와 영희는 다음과 같은 텍스트 파일을 만들어서 지각자를 관리하기 시작했다.

    7월 25일 지각자 명단
    
    나연
    채영
    사나
    쯔위

    자, 이제 철수와 영희는 사장님으로부터 지각자를 관리할 수 있는 권력을 부여받았고, 매일 이 파일에 지각자를 입력해야한다. 하지만 철수와 영희는 별로 사이가 안 좋기 때문에 이 둘은 서로 커뮤니케이션을 하지 않고 각자 맘대로 파일을 수정하는 방식으로 지각자 입력 작업을 진행하게 된다.

    그러던 중 철수와 영희는 사무실에 심어놓은 자신들의 정보원으로부터 7월 25일의 세번째 지각자인 사나가 사실 지각이 아니였다는 정보를 입수했다. 근데 문제는 이 정보원들이 가져온 정보가 서로 달랐던 것이다.

    철수의 정보원: 야 7월 25일에 지각한 사람 있잖아, 사나가 아니고 미나래!
    영희의 정보원: 영희야, 7월 25일에 사나가 지각한게 아니고 지효가 지각한거라는데?

    이 둘은 자신의 정보원을 100% 신뢰하기 때문에 바로 각자 지각자 명단 파일을 수정하기 시작했다.

    시간 철수 영희
    1 지각자.txt사나미나로 변경 지각자.txt사나지효로 변경
    2 리모트 저장소에 변경 사항 커밋 후 푸쉬 계속 작업 중
    3 철수 퇴근 >_< 작업을 마치고 리모트 저장소에 푸쉬 시도
    4 리모트 저장소의 상태가 갱신되었으니 Pull부터 하라는 에러 발생. 엥?
    5 리모트 저장소의 변경 사항을 로컬로 Pull함
    6 철수가 수정한 부분과 영희가 수정한 부분이 충돌!
    From https://github.com/evan-moon/conflict-test
     * branch            test       -> FETCH_HEAD
    Auto-merging 지각자.txt
    CONFLICT (content): Merge conflict in 지각자.txt
    Automatic merge failed; fix conflicts and then commit the result.

    이렇게 다른 사람과 내가 같은 부분을 수정하게 되면, Git은 어떤 것이 맞는 소스인지 알 방법이 없다. 이런 상황에서 Git은 어떤 부분이 충돌났는지 표시하여 사용자에게 알려주기만 하고 나머지는 사용자가 알아서 수정하라고 맡겨버리는데, 이런 상황이 바로 병합 충돌(Merge Conflict)이다.

    철수와 영희 처럼 같은 브랜치에서 작업한다는 것은 소스의 변경 사항을 계속 히스토리를 공유한다는 것이다. 즉, 주기적으로 리모트 저장소로부터 상대방이 작업한 것을 Pull로 가져와서 내 로컬 브랜치에 병합해야한다는 것인데, 이 과정에서 충돌이 발생할 가능성이 높다.

    7월 25일 지각자 명단
    
    나연
    채영
    <<<<<<< HEAD
    지효
    =======
    미나
    >>>>>>> 35058b46325bb61112efd52f4019f907c561328d
    쯔위

    이때 <<< HEAD===사이에 들어있는 상단 부분이 현재 브랜치에서 내가 수정한 내용이다. 영희는 사나지효로 수정했기 때문에 해당 부분에 지효라는 이름이 들어가 있다.

    그리고 ===부터 >>> 커밋 해쉬사이의 내용은 어떤 커밋에서 수정된 내용과 충돌이 발생했는지 알려준다. 이 예시에서는 철수가 사나미나로 수정한 부분이 될 것이다.

    Git은 그냥 버전 관리만 해주는 친구이기 때문에 이런 상황에서 “사나는 사실 지각자가 아니였기 때문에 다른 사람으로 변경해야했다”와 같은 비즈니스 히스토리는 모른다. 그렇기 때문에 Git은 둘 중에 어떤 것이 맞는 소스인지도 당연히 모를 수 밖에 없다. 그래서 사용자에게 선택을 맡기는 것이다. 이 상황에서 영희는 다음 세가지 선택지를 가질 수 있다.

    1. 철수의 변경 사항을 무시
    2. 자신의 변경 사항을 무시
    3. 두 변경 사항 모두 반영

    보통 이런 상황에서는 철수를 불러서 사나 대신 미나를 추가한 이유가 무엇인지 물어본 다음 결정해야하지만 영희는 철수와 사이가 좋지 않으니 그냥 철수의 커밋을 날려버릴 수도 있겠다. (실제 상황에서 함부로 이러면 혼납니다)

    여러 개의 Branch를 사용하는 이유

    브랜치(Branch)저번 포스팅에서 한번 간단하게 설명했다. 이미 저번 포스팅에서 기초적인 브랜치의 개념에 대해서 한번 언급하고 넘어갔기 때문에 이번에는 “왜 여러 개의 브랜치를 사용해야 하는가?”에 대한 이야기를 해보려고 한다.

    기본적으로 Git은 혼자 만의 작업이 아닌 여러 명이 함께 작업하는 협업 상황을 상정하고 만들어졌다. 아무리 Git이 리모트 레파지토리와 로컬 레파지토리로 소스를 분산해서 관리하는 분산 버전 관리 시스템이지만 여러 사람이 한번에 같은 어플리케이션의 코드를 수정하고 있는 상황에서는 방금 위에서 설명한 머지 컨플릭트가 자주 발생하게된다. 그래서 보통 사용자들은 브랜치로 주제에 맞는 작업 공간을 따로 나누어서 히스토리를 관리하는 것이다.

    이렇게 브랜치를 나누어도 결국 언젠가 소스를 병합해야 하기 때문에 컨플릭트가 발생할 확률은 있지만 적어도 작업 중간중간에 계속 해서 컨플릭트를 수정해야하는 일은 많이 줄일 수 있다. 그래서 개발자들은 “어떻게 해야 효율적으로 여러 개의 브랜치를 관리할 수 있을까?”라는 고민을 하게 되는데, 이때 나온 것이 바로 브랜치 전략이다. 브랜치 전략 중 대표적인 것은 Git flow가 있는데, 이건 그냥 유명한 전략 중 하나일 뿐이기 때문에 어떤 브랜치 전략을 가져갈 것인지는 그 조직이 결정하면 된다.

    그럼 브랜치 전략이 어떤 것인지 알아보기 위해 대표적인 브랜치 전략인 Git flow를 한번 간략하게 살펴보도록 하자.

    전략적인 브랜치 관리, Git flow

    Git flow는 기본적으로 masterdevelop 브랜치를 가지고 시작하게 된다. 이때 master는 항상 운영되고 있는 소스의 상태를 가지고 있어야하며, 절대 master 브랜치에는 바로 커밋을 할 수 없다. 그리고 develop 브랜치는 팀이 현재 개발을 진행하고 있는 브랜치이다. 그리고 develop 브랜치에서 각자 개발을 맡고 있는 기능 별로 feature 브랜치를 생성해서 실제 개발을 진행하게 된다.

    git flow1

    모바일에서는 이 그래프의 내용이 잘 안보일 수 있으니 브랜치 이름에 색을 입혀서 설명하도록 하겠다.

    이 그래프를 보면 master 브랜치에 프로젝트의 시작을 의미하는 커밋이 찍힌 후 develop 브랜치가 생성되었고, develop에서부터 기능 개발을 담당하는 브랜치들이 분기하고 있는 모습을 볼 수 있다.

    에반은 feature/add-typescript 브랜치를 생성한 후 어플리케이션에 타입스크립트를 붙히는 작업을 하고, 다니엘은 feature/social-login 브랜치를 생성한 후 소셜 로그인 연동 작업을 하고 있다. 그 후 개발이 끝나는 대로 develop 브랜치에 해당 브랜치들을 차례로 머지하고 있는 모습을 볼 수 있다.

    다니엘이 에반보다 develop 브랜치에 머지한 시점이 늦기 때문에 만약 에반과 다니엘이 같은 부분을 변경했다면 이때 컨플릭트가 발생하게 된다. 하지만 적어도 에반과 다니엘이 각자 기능을 개발하고 있을때는 컨플릭트가 발생하지 않기 때문에 좀 더 기능 개발에 집중할 수 있게 되는 것이다.

    이렇게 개발을 쭉쭉 진행하다가 배포를 해야할 시점이 오면 master로부터 release브랜치를 생성한다. 필자의 직장같은 경우는 release/release-1.0.0과 같이 배포 버전을 브랜치 이름에 표기하는 네이밍 컨벤션을 따르고 있다. 이 release 브랜치는 온전히 배포 만을 위한 브랜치이기 때문에 해당 버전의 배포가 끝나면 버려진다.

    git flow2

    개발이 종료되고 1.0.0 버전을 배포하기 위해 release/release-1.0.0이라는 노란색 브랜치를 master로부터 하나 생성했다. 그 후 다음 버전에 배포될 기능들을 가지고 있는 develop 브랜치를 release/release-1.0.0 브랜치에 머지하고 스테이징 서버에 배포하는 등 최종 테스트를 한 다음, 조직원 모두가 해당 버전의 배포에 동의한다면 master 브랜치에 해당 브랜치를 머지하고 버전명으로 태그를 단다.

    이때 이런 궁금증이 생기는 분이 있을 것이다.

    그럼 핫픽스는 어떻게 하나요? develop 브랜치에서 브랜치를 분기하면 현재 버전의 기능 개발이 끝날 때까지 기다려야하는데…

    이런 경우 develop 브랜치에서 hotfix 브랜치를 분기하여 master로 머지 후 긴급 배포를 하게되면 develop 브랜치에 들어있는 아직 배포되지 말아야할 기능들까지 배포되기 때문에, 핫픽스는 예외적으로 master에서 분기해서 다시 master로 머지할 수 있다.

    git flow3

    그래프가 조금 복잡해졌지만 검정색 라인인 hotfix/fix-main-page에만 집중해보자. master 브랜치로부터 갈라져나와서 한 개의 커밋이 찍히고 다시 master로 머지되는 것을 볼 수 있다. 이때는 정식 릴리즈가 아닌 핫픽스 릴리즈이므로 Sementic Version 룰에 따라 0.0.1 버전의 태그를 달아주었다.

    hotfix/fix-main-page 브랜치가 머지되고 배포가 되었다는 것은 현재 운영 환경에서 돌아가고 있는 소스가 변경되었다는 것이므로 develop 브랜치에도 해당 변경 사항을 반영해줘야한다. 그렇기 때문에 핫픽스 담당자는 배포를 하고 나면 develop 브랜치를 직접 최신화하는 것이 좋다. 그리고 동료들에게 “지금 핫픽스 배포가 끝났으니 각자 작업하시는 브랜치에서 develop 브랜치를 Pull하여 최신화 해주세요~“라고 알려주면 더더욱 좋을 것이다.

    물론 이런 복잡한 브랜치 전략 없이도 그냥 잘 돌아가는 조직도 있다. 하지만 같은 소스를 만지는 개발자가 많아지면 많아질수록 어느 정도의 룰조차 없다면 원활한 협업이 진행되기는 힘들 수도 있기 때문에 대부분의 규모있는 조직에서는 각자의 상황에 맞는 브랜치 전략을 세워서 버전 관리를 진행하고 있다.

    브랜치를 왜 하나로만 운영하지 않는지 이해가 되었다면, 이제 이 브랜치와 버전 히스토리들을 가지고 놀 수 있는 몇 가지 유용한 기능들을 더 살펴보도록 하자.

    두 개의 브랜치를 합쳐보자

    각자의 브랜치에서 작업을 계속 진행하다보면 언젠가 두 브랜치를 합쳐야 하는 날이 다가온다. 이때 두 개의 브랜치를 합치는 행위를 브랜치 병합(Branch Merge)이라고 한다. Git은 Merge, Merge and Squash, Rebase 각각 특색있는 3개의 브랜치 병합 기능을 제공한다. 결국 이 3개의 명령어 모두 두 개의 브랜치를 합친다는 행위는 같지만, 합치는 방법도 다르고 버전 히스토리도 다르게 남기 때문에 적재적소에 이 기능들을 잘 이용한다면 팀원들에게 이쁨받는 깃쟁이가 될 수 있을 것이다.

    Merge

    merge icon

    머지(Merge)는 제일 기본적인 브랜치 병합 기능으로, 합치려고 하는 대상 브랜치의 변경 사항을 타겟 브랜치에 모두 반영하면서 머지 커밋(Merge commit)을 남긴다.

    $ git checkout master
    $ git merge feature

    일반적인 머지는 이미 많은 분들이 알고 있을테니 깊게 설명하지 않고 빠르게 넘어가겠다.

    Merge squash

    merge squash icon

    이번에는 두 개의 브랜치를 병합할 때 사용하는 머지 명령어의 --squash 옵션을 한번 알아보자. --squash 옵션은 해당 브랜치의 커밋 전체를 통합한 커밋을 타겟 브랜치에 머지하는 옵션이다.

    $ git checkout master
    $ git merge --squash feature

    일반 머지는 머지가 되는 대상 브랜치의 모든 커밋이 남아있는 상태에서 타겟 브랜치로 합쳐지지만 머지 스쿼시는 대상 브랜치의 모든 커밋을 모아서 하나의 커밋으로 합치고 타겟 브랜치에 머지하는 방식이다. 사실 이 기능의 정확한 이름은 Merge "and" Squash이다. 즉, 스쿼시도 머지와 같이 독립된 하나의 개념이라는 것이다. 스쿼시는 커밋을 여러 개 합친다는 개념이기 때문에 하단에 후술할 rebase 명령어와 함께 사용하여 현재 브랜치의 커밋을 합칠 때도 사용한다.

    $ git rebase -i HEAD~~

    위 명령어는 HEAD부터 HEAD~~(전전) 커밋까지의 히스토리를 변경하겠다는 의미이다. 이 명령어를 입력하면 vim이 실행되고 아래와 같은 내용이 표시된다.

    pick 9a54fd4 commit의 설명 추가
    pick 0d4a808 pull의 설명을 추가
    
    # Rebase 326fc9f..0d4a808 onto d286baa
    #
    # Commands:
    #  p, pick = use commit
    #  r, reword = use commit, but edit the commit message
    #  e, edit = use commit, but stop for amending
    #  s, squash = use commit, but meld into previous commit
    #  f, fixup = like "squash", but discard this commit's log message
    #  x, exec = run command (the rest of the line) using shell
    #
    # If you remove a line here THAT COMMIT WILL BE LOST.
    # However, if you remove everything, the rebase will be aborted.
    #

    위의 텍스트에 표시된 커밋들의 맨 앞에 있는 pick 문자를 s또는 squash로 변경하면 두 개의 커밋이 합쳐진다.

    Rebase

    rebase icon

    리베이스(Rebase)머지(Merge)와 마찬가지로 브랜치를 다른 브랜치로 합칠 수 있는 기능이다. 단 머지와 차이가 있다면 바로 합치는 방식이다. 머지는 말 그대로 두 개의 브랜치를 하나로 합치는 기능이기 때문에 A 브랜치의 변경 사항 전부를 B 브랜치에 푸쉬하는 것과 동일하다. 그렇기 때문에 머지를 사용하여 브랜치를 합치게 되면 반드시 머지 커밋(Merge commit)이 남게 된다.

    $ git checkout feature
    $ git merge master
    [출처] https://dzone.com/articles/merging-vs-rebasing

    그렇기 때문에 머지는 어느 시점에 어떤 브랜치가 머지 되었는 지 커밋을 통해 알기 쉽다는 장점이 있다. 그러나 단점은 불필요한 커밋이 생성된다는 것이다. 이 단점은 작업 중인 브랜치가 별로 많지 않을 때는 나타나지 않지만 브랜치가 많아지면 나중엔 커밋 로그가 머지 커밋으로 뒤덮혀있는 광경을 볼 수도 있게 된다.

    반면 리베이스는 단순히 합치는 것이 아니라 말 그대로 브랜치의 베이스를 변경하는 것이다. 방금 전 예시의 feature 브랜치를 master로 리베이스하게 되면 마치 feature 브랜치의 변경 사항들이 master의 변경 사항이었던 것처럼 히스토리가 기록된다.

    $ git checkout feature
    $ git rebase master
    [출처] https://dzone.com/articles/merging-vs-rebasing

    리베이스의 장점은 바로 깔끔한 커밋 히스토리를 만들어 준다는 것이다. 머지 커밋이 남지 않고 애초에 master에서 수정한 것 마냥 히스토리가 남기 때문에 깔끔하게 일자로 쭉 떨어지는 이쁜 히스토리를 볼 수 있다. 하지만 리베이스의 단점은 바로 이 커밋 끼워넣기 때문에 발생하는 문제이다.

    [출처] https://dzone.com/articles/merging-vs-rebasing

    필자가 만약 feature 브랜치를 master로 리베이스했다고 가정해보자. 이때 필자가 feature 브랜치를 생성한 이후에 master에 반영된 커밋들은 모두 맨 끝으로 이동하고 중간에 feature 브랜치의 커밋들을 끼워넣게 된다. 즉, 필자가 보고 있는 master의 상태는 feature의 변경 사항들이 반영되어 있는 히스토리를 가지고 있지만 다른 사람의 master는 아직 예전 master의 히스토리와 함께 일하고 있다는 것이다.

    그럼 두개의 master를 강제로 병합해줘야하는데 병합 자체는 푸쉬할때 --force 옵션을 주면 되지만 문제는 이게 굉장히 혼란스러운 상황이라는 것이다. 쉽게 말해서 커밋 히스토리가 꼬이게 되고 사무실의 여기저기서 “어? 이거 왜 이래? 왜 푸시 안돼?”라는 소리가 들려오기 딱 좋은 상황이다.

    그래서 master로의 병합은 머지 스쿼시를 사용하고 develop으로의 병합 때 리베이스를 사용하거나 하는 경우도 있다. 머지는 머지 커밋을 발생시키며 히스토리가 미래로 나아가기 때문에 이런 문제가 발생할 확률이 적지만, 리베이스는 과거를 변경하는 것이기 때문에 문제가 생기기 쉬운 것이다.

    뭐 여러모로 둘 다 장단점이 있으니 잘 골라서 사용하도록 하자.

    Cherry Pick

    체리픽(Cherry Pick)은 다른 브랜치에서 어떤 하나의 커밋만 내 브랜치로 가져오는 기능이다. 체리픽이 하는 일을 보면 대상 브랜치의 커밋 하나를 가져와서 현재 브랜치에 병합하는 행위라고 느껴지지만 히스토리를 보면 병합되는 그림이 아니라 그냥 해당 커밋을 그대로 복사해와서 내 브랜치에 커밋되는 형태로 기록된다.

    $ git checkout master
    $ git cherry-pick 35058b4 # 가져올 커밋 해쉬

    물론 체리픽을 사용할 때도 현재 브랜치의 소스와 충돌이 날 가능성은 있기 때문에 가져오기 전에 충돌을 수정할 수도 있다는 마음의 준비는 필요하다. 체리픽은 잘 쓰면 은근히 꿀 기능인데, 바로 이런 상황 때문이다.

    1. A 브랜치에서 철수가 기능 개발 중
    2. B 브랜치에서 영희가 기능 개발 중
    3. 디자이너가 영희에게 리뷰 별점 아이콘과 디자인을 변경해달라고 요청
    4. 영희가 B 브랜치에서 디자이너의 요구 사항을 반영
    5. 근데 B 브랜치보다 A 브랜치가 먼저 배포되야 함
    6. 디자이너가 철수에게 A 브랜치에 왜 리뷰 별점 디자인 반영안됐냐고 물어봄

    제일 좋은 상황은 철수가 디자이너의 요구 사항을 반영하는 것이겠지만, 막 정신없이 일을 하다보면 그렇게 술술 풀리는 경우만 있는 게 아니기 때문에 이런 문제가 발생하긴 한다.

    이때 철수는 영희가 작업하고 있는 B 브랜치에서 리뷰 별점 아이콘이 수정된 커밋을 A 브랜치로 체리픽함으로써 이 상황을 쉽게 해결할 수 있게 된다. 이런 사람 애매해지게 만드는 상황은 생각보다 자주 발생하기 때문에 체리픽에 익숙해지는 것을 추천한다.

    사실 이 상황은 필자가 얼마 전에 겪은 상황인데 철수가 필자이고 영희가 동료 프론트엔드 개발자였다. 그래서 동료 개발자분과 B 브랜치의 커밋 로그를 봤는데, 리뷰 별점 아이콘만 수정된 커밋이 아니라 다른 변경 사항도 함께 묻어있는 커밋 밖에 없어서 디자이너와 딜을 할 수 밖에 없었고, “그럼 B 브랜치가 배포될 때 한꺼번에 같이 반영해주세요~“라는 결론으로 무사히 넘어갈 수 있었다. (디자이너님 감사감사…)

    작업하던 사항을 임시로 저장해보자

    Stash

    스태쉬(Stash)는 현재 작업 중인 변경 사항들을 잠시 스택에 저장할 수 있는 명령어이다. 이 명령어는 아직 마무리되지 않은 작업이 있는데 다른 브랜치로 체크아웃 해야하는 경우에 유용하게 사용할 수 있다.

    $ git stash # 현재 변경 사항들을 스택에 저장
    $ git stash list # 스태쉬 목록을 확인
    $ git stash apply # 가장 최근의 스태쉬를 다시 불러온다

    또는 직접 스태쉬 이름을 지정할 수도 있다. 스태쉬의 이름을 지정하지 않으면 스택에 들어간 순서(First In Last out)대로만 스태쉬를 가져올 수 있으므로 왠만하면 이름을 지정하는 것을 추천한다. 필자는 주로 스태쉬 이름을 브랜치 이름과 동일하게 지정하는 편이다.

    $ git stash branch-name # 스태쉬 이름을 branch-name으로 지정하고 스택에 저장
    $ git stash apply branch-name # branch-name 이름을 가진 스태쉬를 불러온다

    실제로 회사에서 개발을 하다보면 갑자기 긴급한 버그 픽스 건이 들어온다거나 아니면 PO들이 이슈의 우선 순위를 다시 정리하면서 기존에 작업을 하고 있던 브랜치에서 다른 브랜치로 건너가야하는 경우는 꽤나 빈번하게 발생한다. (특히 버그 픽스…) 이때 다른 브랜치로 넘어가기위해 작업하던 것을 그대로 커밋하게 되면 해당 브랜치에서 함께 개발하고 있는 다른 팀원들에게 피해가 갈 수 있으니 반드시 변경 사항을 스태쉬하도록 하자.

    이미 커밋한 내용 되돌리기

    개발을 진행하다보면 가끔 커밋을 다시 되돌려야 하는 경우도 생긴다. 보통 실수로 인해서 이런 상황이 발생하는 것을 많이 봤는데, 배포되지 말아야 할 기능이 release 브랜치에 껴서 들어간 경우를 제일 많이 본 것같다. 이런 상황에서 그 기능의 코드를 일일히 찾아 손으로 지우는 것은 너무 위험하기 때문에 Git을 사용하여 커밋을 되돌리게 된다. 이때 사용하는 기능이 바로 ResetRevert이다.

    Reset

    리셋(Reset)은 지정한 커밋 당시로 돌아가는 것이다. 아예 시간을 되돌린다고 생각하면 된다. 즉, 리셋을 사용하게되면 지정한 커밋 이후의 히스토리는 모두 사라지게 된다. 예를 들어 이런 히스토리가 있다고 생각해보자.

    * 19061e7 - 맛없는 식당을 찾은 죄로 여자친구한테 이별 통보를 받았다.
    |
    * e50aff9 - 여자친구가 맛이 없다고 한다.
    |
    * 2d57c29 - 알리오 올리오를 주문했다.
    |
    * c04f8f6 - 찾아본 식당에 방문했다.
    |
    * 7d9d953 - 여자친구와 함께 갈 좋은 식당을 찾았다!

    필자는 여친과 함께 방문할 좋은 식당을 찾아서 기대감을 안고 알리오 올리오를 주문했지만 너무 느끼하고 맛이 없어서 결과적으로 여친한테 차이고 말았다.

    그래서 필자는 너무 슬픈 나머지 기억을 지우고 싶어서 알리오 올리오를 주문하기 전으로 돌아가려고 한다. 이때 사용할 수 있는 명령어가 reset이다. 돌아가고 싶은 커밋을 지정하면 해당 커밋 이후의 히스토리는 모두 삭제하고 과거로 돌아갈 수 있다.

    $ git reset --hard c04f8f6
    
    # 식당을 방문했을 때로 돌아갔다!
    * c04f8f6 - 찾아본 식당에 방문했다.
    |
    * 7d9d953 - 여자친구와 함께 갈 좋은 식당을 찾았다!

    우리는 reset 명령어를 사용할 때 3개의 옵션을 사용할 수 있는데, 바로 hard, soft, mixed이다. 이 옵션들은 히스토리를 삭제한다는 것은 전부 동일하지만 삭제된 내용을 처리하는 방식이 조금씩 다르다.

    hard: 지정한 커밋 이후의 히스토리가 삭제되고 삭제된 내용들은 그대로 사라진다.
    soft: 지정한 커밋 이후의 히스토리가 삭제되고 삭제된 내용들은 스테이지로 이동한다.(add한 상태로 변경)
    mixed: 지정한 커밋 이후의 히스토리가 삭제되고 삭제된 내용들은 스테이지에 올라가지 않은 상태가 된다.(다시 add 해줘야 함)

    필자는 방금 위에서 hard 옵션을 사용했기 때문에 식당을 방문했던 커밋 이후의 잊고 싶었던 기억을 모두 깔끔하게 삭제할 수 있었다. 만약 옵션을 지정하지 않고 reset 명령어를 사용하면 mixed 옵션으로 작동한다. 그리고 만약 이미 되돌리고자 하는 히스토리가 리모트 저장소에 푸쉬까지 된 상태라면 리셋 후 히스토리를 푸쉬할 때 --force 옵션을 사용해야한다.

    Revert

    리벗(revert) 또한 리셋처럼 히스토리를 다시 되돌리고 싶을 때 사용하는 명령어이다. 리셋이 지정한 커밋 이후의 모든 히스토리를 없애버렸다면 리벗은 특정 커밋의 변경 사항을 되돌리는 기능이다. 이때 해당 커밋을 되돌린다고 해서 히스토리에서 그 커밋을 삭제하는 것이 아니라, 되돌리고자 하는 커밋의 내용을 반전시키는 것이다.

    $ git revert 35058b4 # 특정 커밋을 되돌린다
    $ git revert 35058b4..c04f8f6 # 커밋의 범위를 지정하여 되돌린다
    $ git revert HEAD # 현재 헤드가 위치한 커밋을 되돌린다

    만약 35058b4 커밋에서 A.js2번 라인에 a라는 글자가 추가되었다고 하면 git revert 35058b4를 사용했을때 A.js2번 라인에서 a를 다시 삭제하는 것이다. 즉, 추가된 사항은 제거하고 제거된 사항은 다시 추가한다. 말 그대로 지정한 커밋의 변경 사항을 반전하고 다시 커밋하는 것이다. 그렇기 때문에 리벗은 리셋과 다르게 히스토리를 삭제하지 않고 하나의 커밋이 추가되는 형태로 히스토리가 남는다.

    * 35058b4 - Revert 맛없는 식당을 찾은 죄로 여자친구한테 이별 통보를 받았다. # 여친한테 차인 히스토리만 리벗하자
    |
    * 19061e7 - 맛없는 식당을 찾은 죄로 여자친구한테 이별 통보를 받았다.
    |
    * e50aff9 - 여자친구가 맛이 없다고 한다.
    |
    * 2d57c29 - 알리오 올리오를 주문했다.
    |
    * c04f8f6 - 찾아본 식당에 방문했다.
    |
    * 7d9d953 - 여자친구와 함께 갈 좋은 식당을 찾았다!

    위의 예시에서 필자는 여친한테 차인 커밋을 다시 리벗했지만 히스토리 상에는 필자의 흑역사가 고스란히 남아있다.(다시 말하지만 실제 상황 아닙니다) 보통 필자는 리벗을 자주 사용하지는 않지만 가끔 테스트용으로 넣었던 console.log가 껴서 들어가거나 할 때 해당 커밋을 리벗 해본 적은 있다. 리셋리벗 둘 다 변경 사항을 되돌리는 기능이지만, 되돌리는 방법은 완전 다르니 적재적소에 잘 사용해보도록 하자.

    마치며

    지금 Git에 대한 포스팅을 두 편 연속으로 작성했는데도 아직 Git에 대해서 전부 설명하지 못했다. 그 만큼 Git은 정말 다양한 기능들로 사용자가 효과적으로 버전 관리를 할 수 있도록 도와주는 도구라고 할 수 있다. 위에서 한번 언급했듯이 어플리케이션의 버전을 관리한다는 특성 때문에 Git이 어떻게 작동하는 지, 내가 이 기능을 사용하면 버전이 어떻게 되는 지 알지 못한다면 사실 두려울 수 밖에 없다.

    “내가 손가락 하나 잘못 놀려서 다른 사람들이 작성한 코드가 날아가면 어떡하지…?”라는 생각은 필자도 해봤고 지금 Git을 겁나 잘쓰시는 많은 개발자 분들도 한번씩은 다 해본 생각일 것이다.(사실 맘 먹고 리셋하고 강제로 푸쉬하지않는 이상 그럴 일이 별로 없다)

    하지만 Git에서 말하는 개념이나 기능의 이름들이 생소해서 그렇지 알고보면 Git의 기능들이 작동하는 방법 자체는 그렇게 복잡하지 않다. 그리고 이 복잡성은 제대로 관리되지 않았던 버전 히스토리 그래프도 한 몫 한다고 생각한다. 솔직히 이리저리 꼬여있는 그래프를 보면 “오 이거 해볼만 한데?”라는 생각은 별로 안드는 것 같다.

    포스팅을 작성하면서 최대한 쉽게 써보려고 했는데 독자분들이 이해가 잘 가셨는 지 모르겠다. 사실 Git은 필자 또래의 개발자 분들보다 나이가 조금 있으신 선배들이 더 어려워하시는 것 같다.

    지금까지 SVN을 주로 사용해서 버전을 관리하다가 갑자기 처음보는 개념들이 우수수 떨어지는 Git이 대세라고 하니 공부하기도 쉽지 않을 것 같다고 하시는 선배들의 이야기를 몇 번 들어본 적 있다.

    사실 모든 프로그래밍이 그렇듯, Git도 글로 읽는 것보다는 직접 몇번 해본 다음에 히스토리 그래프가 어떻게 변했는 지도 보고 소스가 어떻게 변했는 지 보는 게 제일 이해가 잘 된다. Github처럼 무료 저장소를 제공해주는 서비스도 많으니, 연습용 레파지토리를 하나 만든 다음 그 안에서 간단한 코드나 텍스트 파일을 변경해보면서 직접 연습해보시는 걸 추천한다.

    이상으로 Git 뉴비를 위한 기초 사용법 - 버전 관리 포스팅을 마친다.

    Evan Moon

    🐢 거북이처럼 살자

    개발을 잘하기 위해서가 아닌 개발을 즐기기 위해 노력하는 개발자입니다. 사소한 생각 정리부터 튜토리얼, 삽질기 정도를 주로 끄적이고 있습니다.