Git Tag는 특정 commit에 붙는 이름표다.
더 정확히는 commit의 SHA 해시를 가리키는 ref(reference)[혹은, Git object를 가리키는 ref]로,
한 번 붙으면 그 commit에 고정된다.
즉, Tag는 Commit 기준으로 붙는 것이다.
브랜치는 새 commit이 쌓이면 포인터가 자연스럽게 이동하지만, tag는 같은 commit을 계속 가리키는 점이 다르다.
이 차이 때문에 tag는 주로 릴리스 시점을 표시하는 용도로 쓰인다.
v1.0.0을 찍은 시점의 코드는 시간이 지나도 그대로 보존되고, 언제든 그 시점으로 돌아갈 수 있다.
Git Tag에 대해서..
- Tag는 commit의 SHA를 가리키는 ref다. 브랜치와 같은 ref지만 자동으로 이동하지 않는다.
- Lightweight와 Annotated 두 종류가 있다. Annotated는 작성자, 시각, 메시지 등 메타데이터를 포함한다.
- git push는 기본적으로 tag를 보내지 않는다. Tag는 별도 명령으로 push 해야 한다.
- 로컬과 리모트 tag가 분리된 이유는 tag가 "공유된 불변 식별자"로 취급되기 때문이다.
- Cherry-pick, rebase, squash merge는 새 commit을 만들기 때문에 tag가 새 commit에는 따라붙지 않는다.
이 글에서는 위의 간략하게 정리된 내용을 따라
tag가 commit과 어떤 관계인지, 두 종류의 tag가 어떻게 다른지,
그리고 로컬과 리모트에서 tag가 분리되어 관리되는 이유 등을 정리해 보도록 하자.
Tag와 commit의 관계
.git 디렉터리에 저장되는 tag
$ ls .git/refs/
heads/ remotes/ tags/
$ ls .git/refs/heads/
main feature/login
$ ls .git/refs/tags/
v1.0.0 v1.1.0
$ cat .git/refs/tags/v1.0.0
a3f2c1d8e9b4f5a6c7d8e9f0a1b2c3d4e5f6a7b8
브랜치와 tag는 같은 refs/ 디렉터리 안에 위치한다.
$ ls .git/refs/
heads/ # 브랜치
tags/ # 태그
remotes/ # 리모트 추적 브랜치 (origin/main 같은)
Git 공식 문서에서는 refs 디렉터리를 "commit object를 가리키는 포인터(브랜치, 태그, 리모트 등)를 저장하는 곳"으로 설명한다.
ref (reference)
Git에서 ref는 commit을 가리키는 것으로 ref 즉, Reference 참조이다.
Commit은 본래 SHA 해시(a3f2c1d8e9b4f5a6c7d8e9f0a1b2c3d4e5f6a7b8 같은 40자리 문자열)로만 식별된다.
이를 참조하는 것이다.
브랜치와 tag 둘 다 commit SHA를 가리키는 포인터지만, 동작 방식에 차이가 있다.
| 브랜치 | Tag | |
| 새 commit 시 동작 | 자동으로 이동 | 그대로 유지 |
| 주 용도 | 작업 흐름 추적 | 특정 시점 고정 |
| 저장 위치 | refs/heads/ | refs/tags/ |
A --- B --- C --- D
↑ ↑
v1.0.0 HEAD
main
A --- B --- C --- D --- E
↑ ↑
v1.0.0 main (HEAD)
위 그래프에서 main 브랜치는 E(commit 작업)까지 흘러왔지만 v1.0.0 tag는 여전히 C를 가리킨다. 브랜치가 시간의 흐름을 따라간다면 tag는 그 흐름 위에 꽂힌 깃발에 가깝다.
즉, Tag는 특정 commit의 SHA를 가리키는 reference인 것이다.
Lightweight와 Annotated
Tag는 두 가지 형태로 만들 수 있다.
git tag v1.0.0 # Lightweight
git tag -a v1.0.0 -m "메시지" # Annotated
-a는 --annotate의 약자다. 이 옵션을 붙이면 git이 tag를 별도 객체로 저장하면서 추가 정보를 함께 기록한다
Lightweight Tag
commit SHA를 가리키는 단순한 포인터.
.git/refs/tags/v1.0.0 파일에 SHA 한 줄만 저장된다.
Annotated Tag
Git 데이터베이스에 별도의 tag 객체를 만들어 태그를 만든 사람의 이름, 이메일과 태그를 만든 날짜, 그리고 태그 메시지를 저장.
- 가리키는 commit 해시
- 태그 작성자(tagger) 이름과 이메일
- 태그를 만든 시각
- 태그 메시지
- (선택) GPG 서명
일반적으로 Annotated 태그를 만들어 이 정보를 사용할 수 있도록 하는 것이 좋다.
하지만 임시로 생성하는 태그거나 이러한 정보를 유지할 필요가 없는 경우에는 Lightweight 태그를 사용할 수도 있다.
git show를 통해 tag 내용 확인
$ git tag lightweight-tag
$ git tag -a annotated-tag -m "릴리스 노트"
$ git show lightweight-tag
commit a3f2c1d8e9b4f5a6c7d8e9f0a1b2c3d4e5f6a7b8
Author: dev <dev@example.com>
Date: Mon May 4 10:00:00 2026
feat: 로그인 기능 추가
$ git show annotated-tag
tag annotated-tag
Tagger: dev <dev@example.com>
Date: Mon May 4 10:30:00 2026
릴리스 노트
commit a3f2c1d8e9b4f5a6c7d8e9f0a1b2c3d4e5f6a7b8
...
Annotated tag는 tag 정보가 먼저 표시되고 그 아래에 commit 정보가 이어진다. Lightweight는 commit 정보만 바로 보여준다.
Local tag를 Remote에 반영하는 방법
git push가 기본적으로 tag를 함께 보내지 않는다.
로컬 브랜치에서 tag를 붙여서 commit을 push를 해도 remote 브랜치 쪽에는 해당 commit에 tag가 존재하진 않는 것이다.
remote에 tag를 반영하기 위해서는 tag를 별도로 push 해줘야 하는데, 그 명령어는 다음과 같다.
git push [브랜치] [tag 명]
git push # 브랜치만 push, tag는 그대로
git push origin v1.0.0 # 특정 tag를 명시적으로 push
git push --tags # 모든 로컬 tag를 push
git push --follow-tags # push되는 commit이 가리키는 annotated tag만 push
삭제 또한 마찬가지이다.
로컬 tag를 삭제했다고 해당 remote의 tag가 삭제되지 않고, 다음과 같은 명령어 작업이 필요하다.
-d, --delete
git tag -d v1.0.0 # 로컬 tag 삭제
git push origin --delete v1.0.0 # 리모트 tag 삭제
Tag는 "공유된 불변 식별자"로 취급된다
브랜치는 작업 중에 자유롭게 이동하고 변형되는 게 자연스러운 ref다. 반면 tag는 한 번 공유되면 모든 협업자가 동일한 의미로 신뢰하는 식별자가 된다. v1.0.0이라는 tag는 누가 보든 같은 commit을 가리켜야 의미가 있다.
만약 git push가 tag까지 자동으로 보낸다면, 로컬에서 실험용으로 만든 tag나 임시 마킹이 의도치 않게 공유 저장소에 섞일 수 있다.
한번 push된 tag는 다른 협업자들이 fetch 해서 받아가게 되고, 이를 되돌리려면 모두에게 영향을 주는 작업이 필요해진다.
그래서 git은 tag push를 명시적인 행위로 분리해, "이 tag는 정말 공유할 의도가 있는 것인가"를 한 번 더 확인하게 만든 것이다.
Tag는 SHA(commit)에 고정되기 때문에, 그 commit이 다른 commit으로 "복제"되는 작업에서는 tag가 따라가지 않는다.
Cherry-pick, rebase, squash merge가 여기에 해당한다.
이 동작을 이해하려면 commit SHA가 어떻게 계산되는지 알면 도움이 된다.
commit object의 형식을 해당 시점 프로젝트의 최상위 tree, parent commit(있다면), 작성자·커미터
정보와 타임스탬프, 빈 줄, 커밋 메시지로 정의하며, 이 모든 값이 SHA 해시 계산의 입력값이 된다고 한다.
SHA = hash(
트리(파일 스냅샷) +
부모 commit의 SHA +
작성자, 커미터 정보 +
커미터 시각 +
커밋 메시지
)
대략적인 예시가 이렇기 때문에, 같은 변경사항이라도 부모가 다르거나 시점이 다르면 SHA가 완전히 달라진다.
새 SHA를 가진 commit은 git 입장에서는 별개의 commit이고, 원본에 붙어있던 tag는 따라붙지 않는다.
다음 예시를 통해 여러가지 상황에서의 Tag 상태를 확인해 보자.
feature 브랜치에서 C 커밋에 tag 생성
A---B---C
tag: v1.0.0
Git Merge 방식에 대해서는 이전에 포스팅한 내용이 있다 (Git Merge 종류)
[Git] Git Merge 종류 (with. force push로 commit이 사라졌을 경우)
작업을 하거나 협업을 할 때 브랜치를 만들고 해당 브랜치를 Merge 한 경험은 다들 있을 것이다.회사에서 Feather 브랜치를 만들고 delvop 같은 브랜치에 merge 하여 자신이 한 작업물을 올리는 등의 각
androidhelper.tistory.com
1. Fast-forward merge
Merge 전
develop
A---B
feature
A---B---C
tag: v1.0.0
Merge 후
develop, feature
A---B---C
tag: v1.0.0
✅ tag 위치 유지
✅ develop에서 tag 보임
2. (3-way) Merge
Merge 전
develop
A---B---D
feature
A---B---C
tag: v1.0.0
Merge 후
develop
A---B---D------M
\ /
C------
tag: v1.0.0
✅ tag는 여전히 C에 있음
✅ C가 develop 히스토리에 포함되므로 develop에서 tag 보임
⚠️ tag가 merge commit M에 붙는 건 아님
릴리즈 기준이 merge commit이어야 한다면 merge 후 다시 tag를 M에 붙여야 한다.
git tag -a v1.0.0 -m "Release v1.0.0"
git push origin v1.0.0
3. Squash merge
Merge 전
develop
A---B---D
feature
A---B---C
tag: v1.0.0
Squash Merge 후
develop
A---B---D---S
↑
C의 변경사항을 새로 만든 커밋
feature
A---B---C
tag: v1.0.0
❌ tag는 develop의 S에 붙지 않음
❌ develop 히스토리에서 tag가 안 보일 수 있음
❌ tag 기준 코드와 실제 develop 코드의 커밋이 다름
이 경우 릴리즈 tag는 squash merge 후 develop에서 다시 생성해야 한다.
git tag -a v1.0.0 -m "Release v1.0.0"
git push origin v1.0.0
4. Rebase and merge
rebase 전
develop
A---B---D
feature
A---B---C
tag: v1.0.0
feature를 develop 위로 rebase 후
develop
A---B---D
feature
A---B---D---C'
↑
C와 내용은 같지만 새 커밋
원래 C
A---B---C
tag: v1.0.0
❌ tag는 새 커밋 C'로 자동 이동하지 않음
❌ rebase 된 feature를 develop에 merge해도 tag는 원래 C에 남음
Tag 자체가 ref이기 때문에, tag가 commit을 가리키고 있는 한 그 commit은 GC 대상이 되지 않는다.
5. Cherry-pick
cherry-pick도 기존 커밋을 그대로 가져오는 게 아니라 새 커밋을 만든다.
cherry-pick 전
develop
A---B---D
feature
A---B---C
tag: v1.0.0
develop에서 C를 cherry-pick
develop
A---B---D---C'
↑
C의 변경사항을 복사한 새 커밋
feature
A---B---C
tag: v1.0.0
❌ tag는 cherry-pick 된 C'로 이동하지 않음
❌ tag 기준 커밋과 develop 반영 커밋이 다름
브랜치를 지워도 tag는 살아있다
Git에서 commit이 저장소에 남아있는 조건은 "어떤 ref에서든 도달 가능해야 한다"이다.
브랜치(refs/heads/...)와 tag(refs/tags/...)는 모두 ref이므로, 브랜치를 지워도 tag가 그 commit을 가리키고 있으면 commit은 garbage collection 대상이 되지 않는다.
$ git branch -D feature
Deleted branch feature (was a3f2c1d).
$ git tag
v1.0.0 ← 그대로 존재
$ git checkout v1.0.0 ← 정상 동작
Note: switching to 'v1.0.0'.
You are in 'detached HEAD' state.
$ git log --oneline v1.0.0
a3f2c1d feat: 로그인 기능 추가 ← commit도 그대로 살아있음
저장소에 있는 tag 목록을 조회하는 명령어
git tag # 전체 목록
git tag -l "v1.8.5*" # 패턴 검색 (-l 필수)
추가적으로 알아보면 좋을 부분
- tag에 chackout 후 새로운 commit (detached HEAD 상태)
참고 자료
https://git-scm.com/book/ko/v2/Git%EC%9D%98-%EA%B8%B0%EC%B4%88-%ED%83%9C%EA%B7%B8
Git - 태그
와일드카드를 사용하여 Tag 리스트를 확인하려면 -l, --list 옵션을 지정 단순히 모든 Tag 목록을 확인하기 위해 git tag 명령을 실행했을 때 -l 또는 --list 옵션이 적용된 것과 동일한 결과가 출력된다.
git-scm.com
https://git-scm.com/book/en/v2/Git-Internals-Plumbing-and-Porcelain
Git - Plumbing and Porcelain
10.1 Git Internals - Plumbing and Porcelain You may have skipped to this chapter from a much earlier chapter, or you may have gotten here after sequentially reading the entire book up to this point — in either case, this is where we’ll go over the
git-scm.com
https://git-scm.com/book/ko/v2/Git%EC%9D%98-%EB%82%B4%EB%B6%80-Git-%EA%B0%9C%EC%B2%B4
Git - Git 개체
여러분이 사용하는 쉘이 어떤 것인가에 따라 master^{tree} 표현식이 오류를 일으킬 수도 있다. Windows 에서 CMD는 ^ 문자는 이스케이프 기호로 사용한다. ^ 문자를 제대로 사용하려면 git cat-file -p master
git-scm.com
'Android > 학습' 카테고리의 다른 글
| [Android] 가볍게 시작하는 Android Task (1) | 2026.05.30 |
|---|---|
| [Android] 실전 예제로 보는 MVP Pattern의 장단점 (0) | 2026.04.12 |
| [Android] Android 16 (target SDK 36) 대응 (0) | 2026.03.14 |
| [Android] targetSdk와 compileSdk의 차이, 그에 따른 영향. (0) | 2026.02.22 |
| [CS] "부동소수점"이란? 컴퓨터에서 소수를 표현하는 방법과 그에 따른 오차 (0) | 2026.02.07 |