Git 태그: git tag로 버전을 관리하고 릴리스 표시하기
git tag는 특정 커밋에 고정된 이름표로, 보통 v1.2.0 같은 릴리스 버전을 표시합니다. 주석 태그는 git tag -a v1.2.0 -m "메시지"로 만들고, 태그는 git push로 자동 전송되지 않으므로 git push origin v1.2.0이나 git push --tags로 따로 보냅니다.
git tag는 특정 커밋에 붙이는 이름표입니다. 브랜치와 달리 새 커밋이 생겨도 움직이지 않습니다. 1년 뒤에도 v1.2.0은 같은 코드 스냅샷을 가리킵니다. 그래서 태그는 릴리스에 쓰이고, CI/CD 시스템은 태그가 생길 때 빌드와 배포를 실행하는 경우가 많습니다.
가벼운 태그와 주석 태그
Git의 태그는 두 종류이며, 차이가 중요합니다.
| 가벼운 태그(lightweight) | 주석 태그(annotated) | |
|---|---|---|
| 생성 | git tag v1.0.0 | git tag -a v1.0.0 -m "..." |
| 정체 | 커밋을 가리키는 포인터일 뿐 | Git 데이터베이스의 별도 객체 |
| 작성자, 날짜, 메시지 | 없음 | 있음 |
| GPG/SSH 서명 | 불가 | 가능(-s) |
기본적으로 git describe가 인식 | 아니요 | 예 |
| 사용하는 경우 | 개인적인 임시 표시 | 릴리스와 공개하는 모든 것 |
릴리스에는 주석 태그를 쓰세요. 누가, 언제 버전을 냈는지 기록되고, 서명도 할 수 있습니다.
태그 만들기
현재 커밋에 태그 달기
git tag -a v1.2.0 -m "릴리스 1.2.0: 보고서 내보내기, API 타임아웃 수정"
-m을 생략하면 Git이 에디터를 열어 메시지를 쓰게 합니다. 변경 목록을 적기에 편리합니다.
이전 커밋에 태그 달기
릴리스할 때 태그를 달지 않았어도 괜찮습니다. 어떤 커밋에든 태그를 달 수 있습니다.
git log --oneline -5
# 7be41d0 feat: экспорт отчётов
# 1c02e8f fix: таймаут запросов
git tag -a v1.1.0 1c02e8f -m "릴리스 1.1.0"
서명된 태그
git tag -s v1.2.0 -m "릴리스 1.2.0"
git tag -v v1.2.0 # 서명 확인
GPG 키가 설정되어 있어야 합니다(gpg.format=ssh이면 SSH 키도 가능). GitHub와 GitLab은 이런 태그에 검증 표시를 보여 줍니다.
태그 보기
git tag # 모든 태그, 알파벳순
git tag -l "v1.*" # 패턴으로
git tag -n # 메시지 첫 줄과 함께
git tag --sort=-v:refname # 버전순, 최신이 위로
git show v1.2.0 # 태그 정보와 커밋
git describe --tags # 가장 가까운 태그: v1.2.0-3-g7be41d0
정렬에 주의하세요. 기본 정렬은 문자열 단위이므로 v1.10.0이 v1.9.0보다 앞에 옵니다. 버전처럼 정렬하려면 --sort=v:refname을 쓰세요.
git describe의 출력은 이렇게 읽습니다. 태그 v1.2.0 이후에 커밋이 세 개 있고, 현재 커밋은 7be41d0입니다(g는 git을 뜻합니다). 릴리스 사이의 빌드에 버전 번호를 붙일 때 편리합니다.
서버로 태그 보내기
일반 git push는 태그를 보내지 않습니다. "왜 GitHub에 태그가 안 보이죠?"라는 질문의 흔한 원인이 이것입니다.
git push origin v1.2.0 # 태그 하나
git push --tags # 로컬의 모든 태그
git push --follow-tags # 커밋과, 그 커밋을 가리키는 주석 태그
--follow-tags는 --tags보다 안전합니다. 개인적으로 찍어 둔 가벼운 태그를 실수로 보내지 않습니다. 기본 동작으로 켜려면 git config --global push.followTags true를 설정하세요.
서버의 태그를 받으려면 git fetch --tags를 씁니다. 일반 git fetch도 받아 오는 커밋을 가리키는 태그는 함께 가져옵니다.
태그 삭제와 옮기기
삭제
git tag -d v1.2.0 # 로컬에서
git push origin --delete v1.2.0 # 서버에서
# 예전 방식: git push origin :refs/tags/v1.2.0
다른 커밋으로 옮기기
git tag -a -f v1.2.0 <새-해시> -m "릴리스 1.2.0"
git push origin -f v1.2.0
기술적으로 가능하지만, 공개 프로젝트에서 이미 공개한 태그를 옮기는 것은 피하세요. 태그를 이미 받은 사람의 로컬 태그는 자동으로 바뀌지 않습니다. fetch는 기존 로컬 태그를 --force 없이 덮어쓰지 않기 때문입니다. 결과적으로 사람마다 "v1.2.0"이 다른 코드를 가리키게 됩니다. 옳은 방법은 v1.2.1을 내는 것입니다.
릴리스에 어떤 커밋이 들어갔는지 확인하기
태그는 히스토리를 읽는 모든 명령에서 범위의 경계로 쓸 수 있습니다.
git log v1.1.0..v1.2.0 --oneline # 두 릴리스 사이의 커밋
git diff v1.1.0 v1.2.0 --stat # 바뀐 파일
git tag --contains 1c02e8f # 이 커밋이 들어간 릴리스들
git merge-base --is-ancestor 1c02e8f v1.2.0 && echo "릴리스에 포함됨"
마지막 두 명령은 지원 팀이 자주 묻는 질문에 답합니다. "이 수정이 사용자에게 이미 나갔나요?" 수정이 main에만 있고 릴리스는 그보다 먼저 만들어졌다면 git tag --contains는 빈 목록을 보여 주며, 새 태그가 필요하다는 것을 알 수 있습니다.
또 다른 방법은 특정 버전의 소스를 히스토리 없이 내려받는 것입니다: git archive --format=zip -o app-1.2.0.zip v1.2.0. 고객에게 전달할 아카이브나 격리된 환경의 빌드에 씁니다.
태그로 특정 버전 체크아웃하기
git switch --detach v1.1.0
# HEAD is now at 1c02e8f fix: таймаут запросов
detached HEAD 상태가 됩니다. 보고 빌드할 수는 있지만, 새 커밋은 어떤 브랜치에도 속하지 않습니다. 옛 버전을 수정해야 한다면 태그에서 브랜치를 만드세요.
git switch -c hotfix/1.1.x v1.1.0
# 수정, 커밋
git tag -a v1.1.1 -m "보안 수정"
git push origin hotfix/1.1.x --follow-tags
릴리스 번호: SemVer
가장 널리 쓰이는 태그 이름 체계는 Semantic Versioning, 즉 MAJOR.MINOR.PATCH입니다.
- MAJOR는 호환되지 않는 API 변경 때 올립니다:
1.4.2 → 2.0.0. - MINOR는 호환성을 유지하는 새 기능:
1.4.2 → 1.5.0. - PATCH는 오류 수정:
1.4.2 → 1.4.3. - 사전 릴리스는 접미사로 표시합니다:
2.0.0-beta.1,2.0.0-rc.1.
v 접두사(v1.4.2)는 태그 이름의 흔한 관례이지만, SemVer 자체가 요구하지는 않습니다. 중요한 것은 한 가지 형식을 정하고 지키는 것입니다. 그렇지 않으면 최신 릴리스를 찾는 스크립트가 깨집니다.
릴리스 과정에서의 태그
팀에서 흔한 릴리스 절차는 다음과 같습니다.
- 변경을 main에 합치고, 테스트를 통과시킵니다.
- CHANGELOG와 프로젝트 파일의 버전 번호를 갱신합니다(프로젝트가 그 값을 저장한다면).
- 주석 태그를 답니다:
git tag -a v1.5.0 -m "릴리스 1.5.0". git push origin main --follow-tags.- CI가 새 태그를 보고 산출물을 빌드하고 배포합니다. GitHub와 GitLab은 태그에서 설명과 파일이 담긴 Release 페이지를 만듭니다.
이렇게 하면 운영 환경의 어떤 버전이든 코드와 정확히 대응됩니다. 빌드 이름의 태그로 git show를 하면 무엇이 들어갔는지 보입니다. git log v1.4.0..v1.5.0 --oneline은 릴리스 노트를 위한 변경 목록을 줍니다.
태그에서 릴리스 노트 만들기
커밋 메시지가 의미 있다면, 태그로 히스토리를 바로 변경 목록으로 바꿀 수 있습니다. 직전 릴리스와 최신 릴리스를 수작업으로 찾을 필요가 없습니다.
LAST=$(git describe --tags --abbrev=0)
PREV=$(git describe --tags --abbrev=0 "$LAST^")
git log "$PREV..$LAST" --no-merges --pretty=format:"- %s (%h)"
# - feat: экспорт отчётов (7be41d0)
# - fix: таймаут запросов (1c02e8f)
팀이 Conventional Commits 형식(feat:, fix:, feat!:)으로 커밋한다면, 다음 버전 번호도 자동으로 계산할 수 있습니다. fix는 PATCH를, feat는 MINOR를, 느낌표나 BREAKING CHANGE는 MAJOR를 올립니다. 인기 있는 자동 릴리스 도구는 이 원리로 태그를 달고 CHANGELOG를 씁니다.
태그와 CI/CD
파이프라인은 보통 두 가지 이벤트를 구분합니다. 브랜치에 push가 있을 때는 테스트와 테스트 환경 빌드를, 태그가 생길 때는 배포를 실행합니다. GitHub Actions에서는 이렇게 씁니다.
on:
push:
tags:
- "v*.*.*"
GitLab CI에서는 CI_COMMIT_TAG 변수와 rules: - if: $CI_COMMIT_TAG 규칙이 같은 역할을 합니다. 파이프라인 안에서 태그 이름은 Docker 이미지나 패키지의 버전 번호가 되므로, 태그 형식은 엄격하고 예측 가능해야 합니다.
이것이 태그를 옮기지 말아야 할 또 하나의 이유입니다. 파이프라인이 이미 패키지 1.2.0을 레지스트리에 올렸다면, 같은 번호로 다시 올리는 것은 보통 금지됩니다. 그리고 사용자에게 있는 1.2.0 이미지는 옛것으로 남습니다.
태그 이름 규칙
태그 이름도 브랜치 이름과 같은 규칙을 따릅니다. 공백이나 ~ ^ : ? * [ 문자, 연속된 점 두 개는 안 됩니다. 슬래시는 허용되므로, 모노레포에서는 api/v1.2.0, web/v3.0.1처럼 패키지별로 버전을 따로 붙이는 경우가 있습니다. 이름이 맞는지 미리 확인하려면 git check-ref-format "refs/tags/v1.2.0"을 쓰세요. 잘못되었다면 0이 아닌 종료 코드를 냅니다.
태그 보호하기
팀 저장소에서는 릴리스 태그를 보호하세요. GitHub와 GitLab은 패턴(예: v*)에 맞는 태그의 생성, 덮어쓰기, 삭제를 관리자만 할 수 있게 제한할 수 있습니다. 그러면 누군가의 컴퓨터에서 실수로 git push --tags를 해도 계획에 없던 릴리스가 나가지 않고, 잘못된 git push origin --delete가 버전 기록을 지우지 않습니다.
흔한 실수
- 태그가 서버에 없음.
git push origin <태그>나--follow-tags를 빠뜨렸습니다. - 브랜치와 같은 이름의 태그. Git이
refname is ambiguous경고를 냅니다. 태그에는v접두사를 쓰고 이름을 다르게 하세요. - 커밋되지 않은 상태에 태그. 태그는 파일이 아니라 커밋에 붙습니다. 커밋하지 않은 수정은 릴리스에 포함되지 않습니다.
- 릴리스에 가벼운 태그 사용. 작성자, 날짜, 메시지가 없고,
git describe도 기본적으로 인식하지 못합니다.
정리
git tag는 히스토리에서 변하지 않는 지점을 표시합니다. 릴리스에는 git tag -a로 주석 태그를 만들고, SemVer로 이름을 붙이고, 서버에는 git push origin <태그>나 --follow-tags로 명시적으로 보내세요. 이미 공개된 태그는 옮기지 말고 새 버전을 내세요. 태그와 rebase, stash, cherry-pick은 Git 강의에서 실습으로 다룹니다.
자주 묻는 질문
태그와 브랜치는 무엇이 다른가요?
브랜치는 새 커밋이 생길 때마다 자동으로 앞으로 움직이지만, 태그는 한 커밋에 영원히 고정됩니다.
커밋 하나에 태그를 여러 개 달 수 있나요?
네, 한 커밋에 태그를 몇 개든 달 수 있습니다. 예를 들어 v2.0.0과 latest-stable을 함께 둘 수 있습니다.
브랜치를 지우면 태그도 함께 지워지나요?
아니요. 태그는 브랜치와 독립적이므로, 브랜치를 지워도 태그가 붙은 커밋은 계속 접근할 수 있습니다.
커밋이 어느 릴리스에 들어갔는지 어떻게 알 수 있나요?
git tag --contains <해시>를 실행하세요. 이 커밋을 포함하는 모든 태그가 나옵니다.
스크립트에서 마지막 태그를 가져오려면 어떻게 하나요?
git describe --tags --abbrev=0을 쓰세요. 해시 접미사 없이 가장 가까운 태그 이름을 출력합니다.