서비스를 운영하다 보면 "그 버그 언제 배포한 버전에서 생긴 거예요?" 같은 질문을 자주 받아요. 커밋 해시(commit hash, 8f9dd89처럼 생긴 알아보기 힘든 문자열)로 대화하면 서로 헷갈리죠. 그래서 특정 커밋에 v1.0.0 같은 이름표를 붙여둬요. 이게 태그(tag)예요. "이 시점이 1.0.0 정식 출시본입니다" 하고 도장을 콱 찍어두는 거죠. 릴리스(release, 사용자에게 배포하는 버전)를 관리하는 가장 기본적인 도구예요.

80.1 태그가 뭐예요? 브랜치랑 뭐가 다르죠?

브랜치(branch)는 계속 자라나는 작업 흐름이에요. 새 커밋을 얹으면 브랜치가 가리키는 위치도 앞으로 쭉쭉 움직이죠. 반면 태그는 한 번 붙이면 그 자리에 딱 고정돼요. 움직이지 않아요. 그래서 "이 커밋이 바로 그 버전"이라는 표식으로 딱 맞아요.


버전 이름은 보통 v1.4.2처럼 세 자리로 써요. 이걸 유의적 버전(semantic versioning, 의미를 담은 버전 규칙)이라고 해요. 앞에서부터 MAJOR.MINOR.PATCH인데, 큰 구조가 바뀌어 기존 사용자 코드와 호환이 깨지면 맨 앞자리, 새 기능이 추가되면 가운데, 버그만 고치면 맨 뒷자리를 올려요. 팀원끼리 "이번 건 마이너 올리자" 하면 다들 무슨 뜻인지 바로 알아듣죠.


예를 들어볼게요. 1.4.2 상태에서 로그인 버그 하나를 고쳐 배포하면 1.4.3이 돼요. 여기에 알림 기능을 새로 얹으면 1.5.0이 되고요(뒷자리는 0으로 초기화해요). 그러다 결제 방식을 완전히 갈아엎어서 예전 방식이 더는 안 통하게 되면 2.0.0으로 확 올려요. 이렇게 숫자만 봐도 "아, 앞자리가 올랐으니 조심해서 붙여야겠다" 하고 감이 오죠. 아직 정식 출시 전이라면 v1.0.0-beta처럼 뒤에 꼬리표를 달아 시험판임을 표시하기도 해요.

80.2 태그는 어떻게 붙이나요?

태그에는 두 종류가 있어요. 가벼운 태그(lightweight)와 주석 태그(annotated)예요. 가벼운 태그는 그냥 이름표만 붙이는 거예요.


$ git tag v1.0.0


반면 주석 태그는 -a를 붙이고 메시지까지 남겨요. 누가, 언제, 왜 이 버전을 냈는지 기록이 같이 저장되죠.


$ git tag -a v1.1.0 -m "로그인 릴리스"


실무에서 정식 릴리스에는 무조건 주석 태그를 쓰세요. 나중에 git show로 열어보면 태거(tagger, 태그를 붙인 사람)와 메시지가 다 나와요.


$ git show v1.1.0
tag v1.1.0
Tagger: dev <[email protected]>

로그인 릴리스


가벼운 태그는 이런 정보가 없어서, 그냥 내가 잠깐 표시해두는 임시 북마크 정도로만 쓰는 걸 추천해요.


참고로 태그는 "지금 커밋"에만 붙는 게 아니에요. 지난 커밋에 깜빡하고 태그를 안 달았다면, 커밋 해시를 뒤에 붙여서 나중에라도 붙일 수 있어요.


$ git tag -a v0.9.0 -m "직전 배포" 8f9dd89


맨 뒤의 8f9dd89가 그 커밋 해시예요. 이렇게 과거 시점에도 도장을 찍을 수 있으니, 배포하고 나서 "아 태그 깜빡했다" 싶어도 당황하지 마세요.

80.3 태그 목록 보고, 그 버전으로 돌아가려면?

붙여둔 태그 목록은 git tag로 봐요. 메시지까지 같이 보고 싶으면 -n을 붙이고요.


$ git tag -n
v1.0.0 feat: 첫 화면 추가
v1.1.0 로그인 릴리스


"1.0.0 시절 코드가 어땠는지 좀 봐야겠다" 싶으면, 태그 이름으로 그 시점으로 이동할 수 있어요.


$ git checkout v1.0.0


그러면 작업 트리가 딱 그 버전 상태로 바뀌어요. 옛날 버전에서 재현되는 버그를 확인할 때 정말 유용하죠. "이 버그가 원래부터 있었나, 최근에 생겼나"를 버전별로 오가며 확인할 수 있으니까요. 구경이 끝나면 git checkout main으로 원래 브랜치로 돌아오면 돼요. 태그 시점은 잠깐 들여다보는 용도지, 거기서 바로 코드를 고치는 자리는 아니라는 것만 기억하세요.

80.4 원격 저장소에 태그도 올려야 하나요?

네, 여기서 많이들 놓쳐요. git push는 커밋은 보내지만 태그는 안 보내요. 태그는 따로 밀어줘야 팀원들과 배포 서버가 그 버전 이름을 알 수 있어요. 태그 하나만 올릴 땐 이름을 지정해요.


$ git push origin v1.0.0
To ../remote.git
* [new tag] v1.0.0 -> v1.0.0


쌓인 태그를 한꺼번에 올리려면 --tags를 써요.


$ git push origin --tags
* [new tag] v1.1.0 -> v1.1.0


혹시 버전 이름을 잘못 붙여서 원격에서 지워야 한다면 --delete로 지울 수 있어요.


$ git push origin --delete v1.0.0
- [deleted] v1.0.0


참고로 이건 원격에서만 지운 거라, 내 컴퓨터에는 태그가 그대로 남아있어요. 로컬 태그까지 지우려면 git tag -d v1.0.0을 따로 쳐야 해요. 원격과 로컬이 별개라는 걸 알아두면, 태그가 지운 줄 알았는데 자꾸 되살아나는 것 같은 혼란을 피할 수 있어요.


깃허브 같은 곳은 이렇게 올라온 태그를 기준으로 릴리스 노트를 자동으로 만들어주기도 해요. 그래서 태그를 꼬박꼬박 올려두면 배포 이력이 저절로 정리돼요. 반대로 태그를 안 올리면, 깃허브 릴리스 화면이 텅 비어서 "우리 서비스 몇 번이나 배포했더라?"를 아무도 대답 못 하는 상황이 생기죠.

80.5 지금 이 커밋이 어느 버전인지 알 수 있나요?

배포하다 보면 "지금 서버에 올라간 코드가 대체 몇 버전이야?" 궁금할 때가 많아요. 이럴 때 git describe --tags가 답을 줘요.


$ git describe --tags
v1.1.0-2-g8f9dd89


이 한 줄이 정보 덩어리예요. 가장 가까운 태그가 v1.1.0이고, 그 뒤로 커밋이 2개 더 쌓였고, 지금 커밋 해시가 8f9dd89(앞의 g는 git이라는 표시)라는 뜻이에요. 정확히 태그 시점이면 그냥 v1.1.0만 딱 나오고요. 배포 스크립트에서 이 값을 뽑아 화면 하단 버전 표기나 오류 로그에 넣는 팀도 많아요. 사용자가 "이 화면에 v1.1.0-2 라고 떠요"라고 알려주면, 우리는 정확히 어느 커밋에서 문제가 났는지 바로 짚어낼 수 있죠. 태그 하나가 이렇게 디버깅 시간을 확 줄여줘요.

80.6 오늘 정리

정리할게요. 정식 릴리스에는 git tag -a v1.0.0 -m "메시지"로 주석 태그를 붙이고, 버전은 MAJOR.MINOR.PATCH 규칙으로 매겨요. 목록은 git tag -n, 그 시점 구경은 git checkout 태그이름, 원격에는 git push origin --tags로 따로 올려야 해요. 지금 버전이 궁금하면 git describe --tags고요. 태그는 공짜인데 나중에 사고 났을 때 "어느 버전부터인지" 추적하는 값어치가 어마어마해요. 배포할 때 태그 하나 찍는 습관, 꼭 들이세요. 처음엔 가벼운 태그, 주석 태그 구분이 번거롭게 느껴져도, "정식 배포는 주석 태그" 이 원칙만 지키면 반은 먹고 들어가요.