버전을 올렸는데, 아무것도 변하지 않았다
GitStyle 프로젝트에 머리카락 테마를 추가하면서 API 라우트를 처음부터 다시 썼습니다. 코드 구조 문제는 분명했습니다. 그런데 그 작업을 마치고 버전을
0.0.3으로 올리면서 문득 멈칫했습니다.한 편으로는 또 다른 프로젝트 — 크롬 익스텐션 hanspoon — 에서 심사에 세 번 실패했고, 그때마다
0.0.1,0.0.2,0.0.3으로 버전을 올렸습니다. 두 프로젝트 모두0.0.3인데, 이 숫자들은 정확히 같은 의미였을까요?
1. 문제 상황
GitStyle은 GitHub 유저명을 받아 커밋 활동을 시각적 애니메이션으로 변환해 반환하는 서비스다. 꽃 테마로 시작해 머리카락 테마를 추가했고, 그 과정에서 API 라우트 전체를 리팩터링했다. 테마 추가가 완료됐을 때 package.json을 열고 버전 칸을 채우려는데, 고민이 들었다.
// 머리카락 테마 추가 전
{ "version": "0.0.2" }
// 머리카락 테마 추가 후 — 그런데 0.0.3이 맞나?
{ "version": "0.0.3" }
hanspoon의 경우는 더 노골적이었다. 크롬 웹스토어 심사에 실패할 때마다 버전을 올렸다. 코드가 달라지지 않아도, 사용자 입장에서 아무것도 바뀌지 않아도, 심사를 재제출하려면 버전이 달라야 했기 때문이다.
v0.0.1 — 첫 번째 심사 제출
v0.0.2 — 심사 실패 후 재제출 (코드 변경 없음)
v0.0.3 — 다시 실패 후 재제출 (코드 변경 없음)
두 프로젝트 모두 버전이 있었고, 버전이 올라갔지만, 그 숫자들이 무엇을 의미하는지는 스스로도 설명하기 어려웠다. 버전은 무언가를 ‘말해야’ 한다고 느끼고 있었지만, 무엇을 말해야 하는지를 알고 있지 않았다.
2. 내가 처음 한 생각
처음에는 버전이 일종의 ‘타임스탬프’ 같은 거라고 막연히 생각했다. 뭔가 업데이트가 있을 때 올리면 되는 것. 심사를 재제출할 때 버전이 올라야 한다고 하니까 올렸고, 기능을 추가했으니 올렸다. 버전 숫자가 커지면 뭔가 더 발전한 것 같다는 느낌도 있었다.
그런데 이 방식의 문제는 한 가지다. 버전을 보는 사람 입장에서는 아무 정보도 얻을 수 없다는 것이다.
0.0.3을 보고 “아, 버그가 세 번 고쳐졌구나” 또는 “기능이 세 개 추가됐구나”라고 해석하면 hanspoon에서는 완전히 틀린 해석이 된다. 버전 숫자가 ‘심사 실패 횟수’를 뜻하는 건 나만 알고 있는 비밀이다.
버저닝이 의사소통의 도구라면, 나는 혼자만 아는 언어로 소통하고 있었던 셈이다.
3. 실제로 해 본 선택 — Semantic Versioning 이해하기
버저닝을 제대로 알고 싶어서 Semantic Versioning 명세(semver.org)를 읽었다. 핵심은 생각보다 단순했다.
버전은 MAJOR.MINOR.PATCH로 구성되고, 각각이 무엇을 바꿨는지를 나타낸다.
1.4.2
│ │ └── PATCH: 하위 호환 버그 수정
│ └──── MINOR: 하위 호환 기능 추가
└────── MAJOR: 하위 호환이 안 되는 변경 (Breaking Change)
이 규칙이 중요한 이유는 버전 숫자가 변경의 의도를 담기 때문이다. PATCH가 올라가면 “안심하고 업데이트해도 된다”는 신호다. MAJOR가 올라가면 “뭔가 크게 달라졌으니 꼭 확인하고 업그레이드해라”는 경고다.
MAJOR — Breaking Change란 무엇인가
Breaking Change는 기존에 동작하던 코드가 새 버전에서 깨지는 변경을 말한다.
GitStyle의 맥락에서 보면 명확하다. 초기 API는 이런 URL 구조를 가졌다:
https://git-style.vercel.app/api/your-username?flower=rose&color=ff0000
머리카락 테마를 추가하면서 theme 파라미터를 도입했다.
https://git-style.vercel.app/api/your-username?theme=flower&flower=rose&color=ff0000
https://git-style.vercel.app/api/your-username?theme=hair&curliness=curly
이전 URL을 그대로 쓰던 사용자가 있다면 어떻게 될까? theme 파라미터 없이 flower=rose만 보내면, 새 API가 어떻게 처리하느냐에 따라 동작이 달라진다. 만약 theme 없이는 에러를 반환하도록 바꿨다면, 그건 Breaking Change다. 반면 theme이 없을 때 꽃 테마를 기본값으로 처리한다면, 하위 호환이 유지된 것이다.
이런 변경을 PATCH나 MINOR로 올리면 “기존 방식 그대로 써도 된다”는 신호를 보내면서 실제로는 기존 사용자를 깨뜨리는 셈이다.
MINOR — 기능 추가는 어디에 속할까
GitStyle에 머리카락 테마를 추가한 건 무엇이었을까.
기존 꽃 테마 URL이 그대로 동작한다면 — 즉, 하위 호환이 유지된다면 — 이건 MINOR 변경이다. 0.1.0으로 올라야 했다. 새로운 기능이 추가됐지만 기존 동작은 유지되니까.
git-style 프로젝트에서 기능을 추가할 때마다 0.0.x를 올린 건, PATCH 자리를 MINOR처럼 쓴 것이다. 버전만 보면 “버그를 고쳤나?” 라는 오해를 부른다.
PATCH — 진짜 이 자리에 들어오는 건
PATCH는 기능의 추가나 변경 없이, 잘못 동작하던 것을 올바르게 고친 경우다. API 응답이 간헐적으로 깨지던 버그를 수정했다면 0.0.1 → 0.0.2가 된다. 로직이나 인터페이스는 그대로고, 틀린 동작만 바로잡은 것이 해당한다.
hanspoon에서 심사를 재제출하기 위해 버전을 올린 건 이 세 카테고리 중 어디에도 해당하지 않는다. 코드가 바뀌지 않았으니까. 이 경우 버전 숫자가 올라가는 건 스토어 요구사항을 맞추기 위한 절차일 뿐이며, 버저닝의 의미 체계와는 별개다.
잠깐 — Backporting이란
버저닝을 공부하다 보면 Backporting이라는 개념을 만난다.
main: 1.0.0 → 1.1.0 (기능 A 추가) → 2.0.0 (Breaking Change)
↓
legacy: 1.1.1 (1.x 사용자를 위한 보안 패치 적용)
Backporting은 최신 버전에서 발견한 버그나 보안 이슈를 이전 MAJOR 버전에도 적용하는 것이다. 이미 2.0.0으로 업그레이드하기 어려운 사용자들이 있을 때, 그들도 중요한 수정을 받을 수 있게 해주는 방식이다.
개인 프로젝트 규모에선 당장 필요하지 않을 수 있다. 그러나 GitStyle처럼 README에 URL을 박아두고 쓰는 사용자가 있는 서비스라면, MAJOR 버전을 올릴 때 이전 버전 지원을 얼마나 유지할지를 의식적으로 결정해야 한다. 그 결정이 곧 사용자와의 약속이기 때문이다.
4. 결과 — 무엇이 달라졌나
이 과정을 거치고 나서 두 프로젝트를 다시 봤을 때, 버전 히스토리가 다르게 읽혔다.
hanspoon의 경우: 심사 실패마다 올린 0.0.1 → 0.0.2 → 0.0.3은 버저닝이 아니었다. 스토어 제출을 위한 일련번호였다. 버전 숫자는 코드나 기능에 대해 아무것도 말하지 않는다. 이를 인식하면, 앞으로는 심사 실패로 인한 재제출은 pre-release 표기나 별도 메모로 관리하고, 버전은 실제 코드 변경 기준으로만 올리는 게 더 정직하다.
GitStyle의 경우: 머리카락 테마 추가는 0.0.3이 아니라 0.1.0이었어야 했다. API 라우트의 전면 리팩터링과 새 테마 도입은 하위 호환을 유지하는 기능 확장이었으니까. 그리고 만약 URL 구조를 변경했다면 — theme= 파라미터 도입으로 기존 URL이 깨졌다면 — 그건 1.0.0이었어야 했다.
5. 지금 다시 해본다면 — 나만의 버저닝 기준
규칙을 이해하고 나서, 개인 프로젝트와 소규모 오픈소스에 현실적으로 쓸 수 있는 기준을 정리했다.
MAJOR: 올릴 때 두 번 생각한다
MAJOR는 사용자에게 “지금 쓰는 방식이 바뀔 수 있다”고 공개적으로 선언하는 것이다. 기준은 단순하다.
- URL 파라미터 이름이나 구조가 바뀌면 MAJOR
- 응답 JSON의 필드가 제거되거나 타입이 바뀌면 MAJOR
- 함수 시그니처가 바뀌어 기존 코드가 컴파일되지 않으면 MAJOR
이 판단이 어렵다면, “기존 사용자가 버전을 올렸을 때 코드를 수정해야 하는가?”라는 질문 하나로 충분하다. 수정해야 한다면 MAJOR다.
MINOR: 기능 추가의 기준
새 테마 추가 (기존 테마 URL 그대로 동작) → 0.1.0
새 파라미터 추가 (선택적, 기존 없이도 동작) → 0.1.0
MINOR는 “더 풍부해졌지만 기존 방식은 그대로”라는 신호다. 새 기능을 쓰지 않아도 아무 문제가 없어야 한다.
PATCH: 진짜 버그 수정만
API 응답이 특정 유저명에서만 깨지던 버그 수정 → 0.0.1
의도한 대로 동작하던 걸 더 빠르게 만든 최적화 → 0.0.1
PATCH는 “잘못됐던 게 올바르게 됐다”는 신호다. 새로운 동작이 추가되지 않는다.
0.x.x 시절 — 특수 규칙
0.x.x 구간은 Semver 명세상 “초기 개발” 단계로, 아직 Public API가 안정적이지 않다는 의미다. 이 구간에서는 엄격한 규칙보다는 다음 원칙만 지켜도 충분하다.
0.0.x: 실험적. API가 언제든 바뀔 수 있다.0.1.x이상: 어느 정도 방향이 잡혔다. Breaking Change에 한해 마이너를 올린다.1.0.0: 사용자와 약속을 시작하는 시점. 이 순간부터 Semver를 엄격히 지킨다.
크롬 익스텐션처럼 제출 요구사항 때문에 버전을 올려야 할 때
스토어 재제출처럼 코드 변경 없이 버전이 필요한 경우라면, 그 의미를 명시적으로 기록한다.
v0.0.4 — 크롬 웹스토어 심사 재제출 (코드 변경 없음, 정책 검토 대응)
버전 숫자만 보면 알 수 없지만, Release Note에 이유를 남기는 것만으로도 나중에 히스토리를 볼 때 혼란이 없다.
버전은 ‘내가 몇 번 배포했는지’가 아니라 ‘사용자 입장에서 무엇이 달라졌는지’를 말하는 것이다. 그 관점의 전환 하나가 버저닝 전체를 다르게 만든다.