본문 바로가기
Front

SemVer가 모든것을 해결해줄까?

by Kool Jay 2025. 1. 19.
  • 주의 1. 이 글은 SemVer(유의적 버전)를 다루는 글이 아닙니다.
  • 주의 2. 이 글에서 다루는것은 SemVer의 장/단점이 아닙니다.
  • 주의 3. SemVer의 자세한 내용은 링크에서 확인 가능합니다. (유의적 버전 명세)
  • 주의 4. 늘 그렇듯이 이 글에서 다루는 내용은 객관성을 향하지만 개인, 또 조직의 상황을 완전히 배제할 수는 없었습니다.
  • 주의 5. 이 글이 옳은 내용만을 담지 않았을 수 있습니다. 수정이 필요한 내용은 댓글로 피드백 부탁드립니다.

롤백을 해야하는데 버저닝이 제대로 안되어 있다면 어떻게 해야될까요?

이벤트와 프로모션을 담당하는 어플리케이션이 있습니다. 이벤트와 프로모션은 조직의 방향성, 사용자들의 피드백, 또 제한된 예산으로 인하여 배포 후에도 많은 작업들이 더해지고 수정이 빈번하게 발생합니다. 브랜치 전략과 여러 안전장치가 있을 수 있지만, 동시에 여러 작업과 QA를 진행하며 사람의 실수, 또 예측하기 어려운 일이 생길 수 있습니다.


이런 상황을 줄이기 위해 기존에는 없었던 약속들을 만들게 되었습니다.

  1. 브랜치 전략
  2. 버저닝
  3. 커밋 메시지 템플릿
  4. PR 템플릿

여러가지 상황을 고려하고 내부의 구성원과, 또 외부의 협업자들과 많은 이야기를 나눈 후, 브랜치 전략을 마련하고 이전과는 다르게 다음 버전의 배포와 이전 배포를 하면서 발생했던 이슈들을 분석해 봤습니다. 이 과정에서 해당 어플리케이션의 버저닝 관리가 안되고 있다는것을 발견하여 이를 보완하기로 했습니다.

버저닝은 왜 하는가?

버전 관리는 해당 프로젝트의 성격에 따라 조금의 차이가 있지만 크게 두 가지의 경우로 나눌 수 있습니다. 1. 모듈 개발에서는 호환성 유지의 큰 목적이 있으며 2. 서비스 어플리케이션 개발에서는 서비스의 안정성을 위하여 버전을 명시하고 그 버전을 바탕으로 기능 추가와 개발 및 배포를 추적하며 관리할 수 있게 됩니다.


실제 개발업무를 진행하게 되면 프로젝트에 필요한, 생각보다 많은, 모듈의 패키지를 설치하고 사용합니다. 이 의존성들을 관리하고 있는 파일을 보게되면(ex. 프론트엔드 프로젝트 - package.json) 설치된 패키지들의 버전을 확인할 수 있습니다. 이 버전들이 설치된 패키지의 기능과 호환성을 알려주는 역할을 합니다.


대부분의 경우에는 해당 프로젝트의 구성에 맞게 호환성이 유지되도록 패키지 매니저를 통해 자동으로 설치되어 설치된 그대로 사용하지만 특별한 이유가 발생할 경우, 예를 들어 특정 브라우저의 하위 버전 호환성을 위하여, 혹은 취약성 점검 후 조치를 위하여, 설치된 버전이 아닌 특정 버전으로 이를 해결해야 하는 경우가 있습니다.


만약, 우리가 사용하는 어플리케이션에서 사용하는 모듈의 버전 명시가 없다면, 계속해서 수정/발행되어 서로 다른 기능과 호환성의 차이를 보이는 패키지들을 어떻게 관리할 수 있을까요?


우리가 사용하는 모듈들은 정체된 상태로 관리되지 않습니다. 늘 변화가 있거나 변화를 준비하고 있으며 언제라도 변경사항이 발생할 수 있습니다.

SemVer는 왜 많이 사용하지?

여러 버저닝 관리 방법이 있지만 그중 왜 SemVer를 많이 사용하게 되었을까요? SemVer는 단순하고 명확하며, 그 자체로 자기 설명적이면서 가장 널리 알려져 있습니다. (많이 사용하였기에 알려졌지만 반대로 많이 알려졌기에 사용량이 높은 것이죠) 단순하면서도 명확한 이 체계 때문에 다양한 프로젝트에 SemVer를 채택하는 경우가 많습니다.


Semantic Versioning은 소프트웨어 버전 번호를 세 가지 주요 숫자(주 버전, 부 버전, 수정 버전)로 나누어 소프트웨어 변경 사항을 의미 있게 전달하는 방법입니다. 이 규약을 따르면 버전 번호는 다음과 같은 형식을 가집니다.

Major.Minor.Patch


Major (주 버전): 이전 버전과 비교하여 매우 크거나 많은 변경사항이 생겨 호환성에 문제가 있거나 아직 발견되지 않은 잠재적 이슈가 존재할 경우 주버전을 올립니다.

Minor (부 버전): 이전 버전과 호환되며 이전 버전에 단순 추가되는 새로운 기능이 있을 경우 업데이트합니다.

Patch (수정 버전): 버그 수정, 매우 작은(개발 문서 업데이트 등) 영향을 끼치는 변경사항에 증가시킵니다.

 

아래는 유의적 버전 2.0.0-ko2의 요약 내용입니다. (출처: https://semver.org/lang/ko/)

버전을 주.부.수 숫자로 하고:
기존 버전과 호환되지 않게 API가 바뀌면 “주(主) 버전”을 올리고,
기존 버전과 호환되면서 새로운 기능을 추가할 때는 “부(部) 버전”을 올리고,
기존 버전과 호환되면서 버그를 수정한 것이라면 “수(修) 버전”을 올린다.
주.부.수 형식에 정식배포 전 버전이나 빌드 메타데이터를 위한 라벨을 덧붙이는 방법도 있다.

SemVer는 The Correct One일까?

물론 SemVer가 명확하고 간단한 버저닝 방법이지만 모든것에 적합하거나 효율적이지는 않습니다. 블로그 혹은 이벤트 페이지를 보여주는 어플리케이션 같이 새로운 기능의 추가 및 크고 작은 변화 보다는 콘텐츠의 업데이트가 잦은 프로젝트에는 SemVer가 '과연 적정한 관리 방법일지' 한번은 고민하게 됩니다.


단순 페이지의 추가가 주를 이루는 경우, 배포되는 내용이 이전 배포의 호환성에 영향을 주지 않으며, 주/부/수로 나눠진 숫자가 날짜와 시간 기반으로 게시되는 콘텐츠를 해당 숫자로 즉각적으로 알아차리게 할수있을지, 또 그게 효율적인지는 의문입니다.


이벤트나 프로모션은 아주 예외적인 상황을 제외하고 날짜와 시간을 기반으로 진행됩니다. 그렇기에 특정된 날짜와 시간이 해당 프로젝트를 설명해주며, 배포 시간이 다른 어플리케이션과는 다른 의미를 갖게 됩니다.

날짜를 기반으로 한 버저닝

출시를 위한 모든 준비가 끝난 시점(이벤트 진행 전)을 명확하게 표현하고 그것으로 버전 관리를 할수있겠다는 생각을 가지고 포맷을 정리하게 됐습니다.

YY.MM.DD.totalSeconds


YY: 해당 일자의 연도 뒤 2자리 (leading-zero)

MM: 해당 일자의 월 (leading-zero)

DD: 해당 일자의 일 (leading-zero)

totalSeonds: 해당 일자의 현재까지의 누적 초


시간(누적 초)을 초 단위로 설정하게 된 까닭은 실수로 인한 즉각적인 수정을 진행하여 버전을 업데이트할 때, 부수적인 문자열을 추가하는 무의미한 행위를 없애기 위함이였습니다.
구성원들에게 해당 내용을 공유하고 동의를 얻은 후, 기존에 없던 프로세스를 추가하기 위하여 \`출시 준비 단계\`를 생성하였습니다. 수동으로 이 단계를 수행할 수 없었기에 npm script를 짜고 이를 공표하였습니다. 아래는 출시 준비(prepublishOnly)를 위해 작성한 스크립트 구문입니다.

import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import fs from 'fs';
import { execSync } from 'child_process';

const now = new Date();
const year = String(now.getFullYear() % 100).padStart(2, '0');
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const totalSeconds = now.getHours() * 3600 + now.getMinutes() * 60 + now.getSeconds();
const newVersion = `${year}.${month}.${day}.${totalSeconds}`;

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const packageJsonPath = join(__dirname, 'package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
packageJson.version = newVersion;
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
console.log(`새로운 버전으로 업데이트 되었습니다. ${newVersion}`);

try {
  execSync('git add package.json', { stdio: 'inherit' });
  execSync(`git commit -m "Release version ${newVersion}"`, { stdio: 'inherit' });
  execSync(`git tag -a ${newVersion} -m "Release ${newVersion}"`, { stdio: 'inherit' });
  execSync('git push origin main --tags', { stdio: 'inherit' });
  console.log(`Git tag ${newVersion} 버전의 생성과 원격 저장소 푸시가 완료되었습니다.`);
} catch (error) {
  console.error('버전 생성과 원격 저장소 푸시 중 에러가 발생했습니다.', error.message);
  process.exit(1);
}

실험적 접근

날짜 기반 버저닝은 빠르게 변동하는 프로젝트나 이벤트 중심 애플리케이션에서 보다 직관적이고 실용적인 관리 방식이 될 수 있습니다. 연관된 다른 프로세스 정리 때문에, 프로젝트 적용 전이라, 예상되는 문제와 개선할 점이 반드시 있겠지만 이와 같은 버저닝 방식을 통해 기존 SemVer의 한계를 극복할 수 있는 가능성도 존재한다는 것을 공유하고 싶었습니다.