그동안 저의 오픈 소스 프로젝트를 지켜보셨다면, 제가 v0.x.x와 같은 제로 메이저 버전을 고수하는 경향이 있다는 것을 눈치채셨을 것입니다. 예를 들어 이 글을 쓰는 현재 UnoCSS의 최신 버전은 v0.65.3, Slidev는 v0.50.0, unplugin-vue-components
는 v0.28.0입니다. React Native는 v0.76.5, sharp는 v0.33.5 등 다른 프로젝트도 이 방식을 따릅니다.
사람들은 종종 메이저 버전이 0이라고 하면 소프트웨어가 프로덕션에 사용할 준비가 되지 않았다고 생각합니다. 그러나 여기에 언급된 모든 프로젝트는 이미 수백만 개의 프로덕션 환경에서 안정적으로 사용되고 있습니다.
왜 그럴까요? 이 글을 읽고 계신 분들도 궁금하셨을 겁니다.
버전 번호는 코드베이스의 스냅샷 역할을 하여 변경 사항을 효과적으로 전달할 수 있도록 도와줍니다. 예를 들어 “v1.3.2에서는 작동했지만 v1.3.3에서는 문제가 발생했다.”라고 말할 수 있습니다. 이렇게 하면 메인테이너가 버전 간의 차이점을 비교하여 버그를 더 쉽게 찾을 수 있습니다. 버전은 본질적으로 특정 시점의 코드베이스에 대한 마커, 즉 일종의 표시(seal)입니다.
그러나 코드는 복잡하고 모든 변경에는 장단점이 수반됩니다. 변경 사항이 코드에 어떤 영향을 미치는지 설명하는 것은 자연어를 사용하더라도 까다로울 수 있습니다. 버전 번호만으로 릴리스의 모든 뉘앙스를 담아낼 순 없습니다. 그렇기 때문에 변경 로그, 릴리스 노트, 커밋 메시지를 통해 더 많은 컨텍스트를 제공합니다.
저는 버전을 사용자에게 변경 사항을 알리는 방법, 즉 업그레이드 시 호환성과 안정성을 보장하기 위한 라이브러리 관리자와 사용자 간의 계약이라고 생각합니다. 사용자는 변경 로그를 확인하지 않고는 v2.3.4
와 v2.3.5
사이에 무엇이 변경되었는지 항상 알 수 없습니다. 하지만 숫자를 보면 버그를 수정하기 위한 패치 릴리스이므로 업그레이드해도 안전하다는 것을 유추할 수 있습니다. 이처럼 버전 번호만 보고도 변경 사항을 파악할 수 있는 것은 라이브러리 관리자와 사용자 모두가 버전 체계에 합의했기 때문에 가능한 일입니다.
버전은 계약일 뿐이며 특정 프로젝트마다 다르게 해석될 수 있으므로 맹목적으로 신뢰해서는 안 됩니다. 버전은 변경 로그를 자세히 살펴보고 업그레이드에 신중을 기해야 함을 알려주는 지표 역할을 할 뿐입니다. 그러나 의도했든 의도하지 않았든 간에 어떤 변경으로 인해 동작이 변경될 수 있으므로 모든 것이 예상대로 작동한다는 보장은 없습니다.
자바스크립트 생태계, 특히 npm에 게시된 패키지의 경우 시맨틱 버전 또는 줄여서 SemVer라고 알려진 규칙을 따릅니다. SemVer 버전 번호는 세 부분으로 구성됩니다: MAJOR.MINOR.PATCH
. 규칙은 간단합니다:
npm
, pnpm
, yarn
등 우리가 사용하는 패키지 관리자는 모두 npm의 모든 패키지가 SemVer를 준수한다는 가정 하에 동작합니다. 사용자 또는 패키지가 ^1.2.3
과 같이 버전 범위가 있는 종속성을 지정하면 동일한 메이저 버전(1.x.x
)을 공유하는 모든 버전으로 업그레이드해도 좋다는 의미입니다. 이러한 시나리오에서는 패키지 관리자가 특정 프로젝트에 가장 적합한 버전을 기준으로 설치할 최적의 버전을 자동으로 결정합니다.
이 규칙은 기술적으로 잘 작동합니다. 패키지가 새 메이저 버전 v2.0.0
을 릴리스했을 때 지정된 범위가 ^1.2.3
인 경우 패키지 관리자는 이를 설치하지 않습니다. 이렇게 하면 버전 범위를 수동으로 업데이트할 때까지 예기치 않은 변경 사항이 프로젝트에 영향을 미치지 않습니다.
그러나 인간은 숫자간의 절대적인 차이보다 몇배나 차이나는지를 더 중요하게 여기는 경향이 있습니다. v2.0
에서 v3.0
으로의 변경은 매우 크고 획기적인 변화로 인식하는 반면, v125.0
에서 v126.0
으로의 변경은 SemVer에서 호환되지 않는 API 변경임에도 불구하고 훨씬 더 사소한 것으로 인식하는 경향이 있습니다. 이러한 경향은 메인테이너가 사소한 비호환성 변경 사항(breaking changes)으로 인해 메이저 버전을 변경하는 것을 주저하게 만들고, 하나의 메이저 릴리스에 많은 비호환성 변경 사항이 누적되어 사용자가 업그레이드를 더 어렵게 만들 수 있습니다. 반대로 v125.0
과 같은 경우에는 v126.0
으로의 전환이 사소해 보이기 때문에 주요 변경 사항의 중요성을 전달하기가 어려워집니다.
Dominik Dorfmeister는 API 설계에 대한 훌륭한 강연을 통해 이를 설명하는 흥미로운 부등식을 소개했습니다. "Breaking Changes !== Marketing Event”
저는 점진성의 원칙을 굳게 신봉하고 있습니다. 점진성은 한 번에 훨씬 더 높은 단계로 도약하는 대신 사용자가 자신의 속도에 맞춰 점진적으로 변화를 받아들일 수 있게 해줍니다. 점진성은 잠시 멈추고 평가할 수 있는 기회를 제공하므로 각 변화의 영향을 더 쉽게 이해할 수 있습니다.
계단처럼 점진적으로 - 내 강연 점진적으로 나아가기의 스크린샷
저는 버전에도 동일한 원칙을 적용해야 한다고 생각합니다. 메이저 버전을 대규모 개편으로 취급하는 대신 관리하기 쉬운 소규모 업데이트로 세분화할 수 있습니다. 예를 들어, v1.x
에서 10가지 비호환성 변경 사항이 포함된 v2.0.0
을 릴리스하는 대신 이러한 변경 사항을 여러 개의 소규모 메이저 릴리스에 분산시킬 수 있습니다. 이렇게 하면 2가지 비호환성 변경 사항이 포함된 v2.0
을 릴리스한 다음, 1가지 비호환성 변경 사항이 포함된 v3.0
을 릴리스하는 등의 방식으로 릴리스할 수 있습니다. 이 접근 방식을 사용하면 사용자가 점진적으로 변경 사항을 쉽게 적용할 수 있으며, 지나치게 많은 변경 사항이 한꺼번에 적용되어 사용자에게 부담을 주는 상황을 방지할 수 있습니다.
비호환성 변경에 대한 점진적 추진 - 내 강연 점진적으로 나아가기의 스크린샷
제가 v0.x.x
를 고집하는 이유는 버전 관리에 대한 저만의 색다른 접근 방식 때문입니다. 저는 필요하거나 사소한 비호환성 변경사항은 조기에 도입하여 일반적으로 v2
에서 v3
으로 메이저 버전이 전환될 때 발생하는 불안감을 주지 않고 업그레이드를 쉽게 하는 것을 선호합니다. 어떤 변경사항은 "기술적으로" 획기적일 수 있지만 실제로 99.9%의 사용자에게 영향을 미치지는 않습니다. (비호환성 변경은 상대적인 개념입니다. 이전 동작에 의존하는 사용자에게는 버그 수정조차도 비호환적일 수 있지만 이는 또 다른 논제입니다 :P).
SemVer에는 맨 앞에 오는 메이저 버전이 0
일 때 모든 마이너 버전 업데이트를 비호환성으로 간주하는 특별한 규칙이 있습니다. 저는 SemVer의 한계를 극복하기 위해 이 규칙을 조금 악용했습니다. 메이저 버전을 0(zero-major)으로 해 실질적으로 첫 번째 숫자를 버리고, MINOR
와 PATCH
를 하나의 숫자로 합치는 것이죠(이 방법을 알려주신 David Blass에게 감사드립니다).
~ZERO~.MAJOR.{MINOR + PATCH}
물론 제로-메이저 버전이 점진적으로 발전할 수 있는 유일한 방법인 것은 아닙니다. Node.js, Vite, Vitest와 같은 도구는 일관된 간격으로 메이저 버전을 출시하고 있으며, 각 릴리스에 적용하기 쉬운 최소한의 비호환성 변경 사항만 적용하고 있습니다. 많은 노력과 추가적인 관심이 필요한 일입니다. 그들에게 찬사를 보냅니다!
저는 제로 메이저 버전을 고수하는 것이 최선의 방법은 아니라는 것을 인정해야 했습니다. 커뮤니케이션을 개선하기 위해 더 세분화된 버전 관리를 목표로 했지만, 제로 메이저 버전 관리를 사용하면 실제로 변경 사항을 효과적으로 전달하는 데 한계가 있었습니다. 실제로 저는 제 고집 때문에 버전 관리 체계의 귀중한 부분을 낭비하고 있었습니다.
그래서 저는 새로운 대안을 제시하고자 합니다.
이상적으로는 SemVer가 epoch.major.minor.patch.
4개의 숫자를 갖기를 바랍니다. EPOCH
버전은 중요한 변경을 위한 것이고, MAJOR
버전은 기술적으로 호환되지 않는 API 변경 중 영향이 적어보이는 것을 위한 것입니다. 이렇게 하면 변경 사항을 보다 세분화하여 전달할 수 있습니다. 이와 유사하게, HUMAN.MAJOR.MINOR
를 제안하는 로맨틱 버전도 있습니다. SemVer의 창시자 Tom Preston-Werner도 블로그를 통해 비슷한 고민과 해결책을 언급했습니다. (이 사실을 알려주신 Sébastien Lorber에게 감사드립니다).
물론 전체 생태계가 새로운 버전 관리 체계를 채택하기에는 너무 늦었습니다.
SemVer를 바꿀 수 없다면 최소한 확장할 수는 있을 것입니다.
제가 제안하는 새로운 버전 관리 체계는 🗿 Epoch Semantic Versioning, 줄여서 Epoch SemVer입니다. 이 방식은 MAJOR.MINOR.PATCH
형식을 기반으로, 첫 번째 숫자를 확장하여 EPOCH
와 MAJOR
조합으로 만듭니다. 이 둘 사이를 구분하기 위해 넷째 자리를 사용하여 EPOCH
를 나타내며, MAJOR
의 범위는 0에서 999까지입니다. 이렇게 하면 기존 도구를 그대로 사용하면서 SemVer와 똑같은 규칙을 따르지만 사용자에게 더 세분화된 정보를 제공할 수 있습니다.
“에포크(Epoch)"라는 이름은 Debian의 버전 관리 체계에서 영감을 얻었습니다.
형식은 다음과 같습니다.
{EPOCH * 1000 + MAJOR}.MINOR.PATCH
이전에는 EPOCH 배수를
100
으로 제안했지만, 커뮤니티 피드백에 따르면 메이저 버전에 더 많은 공간을 주고 숫자 간 구분을 좀 더 명확하게 하기 위해1000
이 더 선호되는 것 같습니다. EPOCH의 배수는 엄격한 규칙이 아니므로 필요에 따라 자유롭게 조정할 수 있습니다.
예를 들어, UnoCSS는 v0.65.3
에서 v65.3.0
으로 전환됩니다(EPOCH
가 0인 경우). SemVer에 따라 패치 릴리스는 v65.3.1
이 되고 기능(feature) 릴리스는 v65.4.0
이 됩니다. 에지 케이스에 영향을 미치는 사소한 비호환성 변경 사항이 반영되면 사용자에게 잠재적인 영향을 알리기 위해 v66.0.0
으로 업데이트할 수 있습니다. 코어에 대한 대대적인 개편이 필요한 경우에는 v1000.0.0
으로 바로 건너 뛰어 새로운 시대를 알리고 대대적인 업데이트를 발표 할 수 있습니다. 저는 기억하기 쉽고 참조하기 쉽도록 0이 아닌 각 EPOCH
에 코드명을 할당하는 것을 제안하고 싶습니다. 이 접근 방식은 메인테이너가 변경 사항을 사용자에게 효과적으로 전달할 수 있는 유연성을 제공합니다.
팁
에포크를 자주 사용할 필요는 없습니다. 주로 최종 사용자를 대상으로 하는 높은 수준의 라이브러리나 프레임워크에 유용합니다. 낮은 수준의 라이브러리의 경우 에포크값을 전혀 올릴 필요가 없을 수도 있습니다(
ZERO-EPOCH
는 본질적으로 SemVer와 동일합니다).
물론 모든 사람이 이 접근 방식을 채택해야 한다고 말씀드리는 것은 아닙니다. 이는 기존 시스템을 보완하기 위한 아이디어일 뿐이며, 이러한 요구가 있는 패키지에 한해서만 사용할 수 있습니다. 실제로 어떻게 운영되는지 지켜보는 것도 흥미로울 것입니다.
저는 UnoCSS, Slidev, 그리고 제가 유지 관리하는 모든 플러그인을 포함한 프로젝트에 에포크 시맨틱 버전 관리를 도입하고 궁극적으로 안정적인 패키지 배포를 위해 제로 메이저 버전 관리를 포기할 계획입니다. 이 새로운 버전 관리 방식이 변경 사항을 더 효과적으로 전달하고 업그레이드할 때 사용자에게 더 나은 컨텍스트를 제공하는 데 도움이 되길 바랍니다.
이 아이디어에 대한 여러분의 생각과 피드백을 듣고 싶습니다. 아래 링크를 통해 의견을 자유롭게 공유해 주세요!
🚀 한국어로 된 프런트엔드 아티클을 빠르게 받아보고 싶다면 Korean FE Article(https://kofearticle.substack.com/)을 구독해주세요!
잘읽었습니다~