소프트웨어 개발에서 버전 관리는 항상 골치 아픈 문제였습니다. 특히 지속적 배포 (CD) 환경에서는 더욱 그렇죠. 기존의 Semantic Versioning(SemVer) 은 많은 개발자들이 사용하고 있지만, 제품의 실제 개발 주기와 맞지 않는 경우가 많았습니다.
예를 들어, 다음과 같은 상황을 생각해봅시다:
이러한 문제를 해결하기 위해 LINE 에서 개발한 것이 바로 HeadVer 입니다.
프로덕트에 적용된 실제 모습
HeadVer 는 다음과 같은 구조를 가집니다:
<head>.<yearweek>.<build>
각 부분의 의미는 다음과 같습니다:
head
: 주요 변경사항을 나타내는 숫자yearweek
: 릴리스 년도와 주차 (예: 2324 는 2023 년 24 주차)build
: 빌드 번호이를 시각화하면 다음과 같습니다:
3 . 2324 . 59
│ │ │
│ │ └─ 빌드 번호
│ │
│ └─ 2023년 24주차
│
└─ 주요 변경 3회
HeadVer 와 SemVer 를 비교해보면 다음과 같은 차이점이 있습니다:
특징 | HeadVer | SemVer |
---|---|---|
버전 구조 | <head>.<yearweek>.<build> | <major>.<minor>.<patch> |
시간 정보 | 포함 (yearweek) | 미포함 |
주요 변경 표시 | head 숫자로 명확히 표시 | major 버전 증가로 표시 |
빌드 정보 | 포함 | 선택적 포함 |
HeadVer 의 장점은 버전 번호만으로도 제품의 발전 과정과 릴리스 시기를 직관적으로 파악할 수 있다는 것입니다.
HeadVer 를 JavaScript 프로젝트에 적용하는 과정을 더 자세히 살펴보겠습니다. 각 함수의 동작 원리와 코드의 세부 사항을 설명하겠습니다.
먼저, 필요한 라이브러리를 설치합니다:
npm i date-fns semver
date-fns
: 날짜 처리를 위한 라이브러리semver
: 시맨틱 버저닝 파싱 및 비교를 위한 라이브러리scripts/update-version.js
파일을 생성하고 다음과 같이 작성합니다:
const { format } = require('date-fns');
const fs = require('fs');
const path = require('path');
const semver = require('semver');
// 연도와 주차 계산 함수
function getYearWeek(date) {
return format(date, 'yyww');
}
getYearWeek
함수는 주어진 날짜를 'yyww' 형식 (년도의 마지막 두 자리와 주차) 으로 변환합니다. 예를 들어, 2023 년 9 월 10 일은 '2336' 으로 변환됩니다.
// HeadVer 문자열 생성 함수
function getHeadVer(args) {
const headVer = `${args.head}.${getYearWeek(args.date)}.${args.build}`;
return args.suffix ? `${headVer}+${args.suffix}` : headVer;
}
getHeadVer
함수는 HeadVer 형식의 버전 문자열을 생성합니다. head
, date
, build
, 그리고 선택적으로 suffix
를 인자로 받아 "head.yearweek.build+suffix" 형식의 문자열을 반환합니다.
// 다음 버전 계산 함수
function getNextVersion(args) {
const current = semver.parse(args.currentVersion);
const newSuffix = args.suffix !== undefined ? args.suffix : current.build.join('');
return getHeadVer({
head: current.major || 0,
date: args.date,
build: args.build,
suffix: newSuffix,
});
}
getNextVersion
함수는 현재 버전을 파싱하고, 새로운 HeadVer 버전을 생성합니다. 현재 버전의 major 숫자를 head 로 사용하고, 새로운 날짜와 빌드 번호를 적용합니다.
// 메인 로직
const packageJsonPath = path.resolve(__dirname, '../package.json');
const buildNumberPath = path.resolve(__dirname, '../build_number.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const currentVersion = packageJson.version;
const date = new Date();
const yearWeek = getYearWeek(date);
메인 로직에서는 먼저 필요한 파일 경로를 설정하고, package.json
에서 현재 버전을 읽어옵니다. 그리고 현재 날짜와 주차를 계산합니다.
let buildNumberData = {};
if (fs.existsSync(buildNumberPath)) {
buildNumberData = JSON.parse(fs.readFileSync(buildNumberPath, 'utf8'));
}
const build = buildNumberData[yearWeek] ? buildNumberData[yearWeek] + 1 : 1;
buildNumberData[yearWeek] = build;
fs.writeFileSync(buildNumberPath, JSON.stringify(buildNumberData, null, 2));
이 부분에서는 build_number.json
파일을 읽어 현재 주차의 빌드 번호를 관리합니다. 파일이 없으면 새로 생성하고, 있으면 해당 주차의 빌드 번호를 1 증가시킵니다.
const nextVersion = getNextVersion({
currentVersion,
date: date,
build: build,
});
packageJson.version = nextVersion;
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
console.log(`::set-output name=new_version::${nextVersion}`);
console.log(`Updated version to ${nextVersion}`);
마지막으로, 새 버전을 계산하고 package.json
파일을 업데이트합니다. 그리고 새 버전 정보를 콘솔에 출력합니다.
package.json
파일에 다음 스크립트를 추가합니다:
"scripts": {
"update-version": "node ./scripts/update-version.js",
"build:versioned": "npm run update-version && npm run build"
}
update-version
: 버전 업데이트 스크립트를 실행합니다.build:versioned
: 버전을 업데이트한 후 빌드를 실행합니다.버전 업데이트 프로세스는 다음과 같이 진행됩니다:
build_number.json
파일에서 현재 주차의 빌드 번호를 읽어옵니다.package.json
에서 현재 버전을 읽어옵니다.head
: 현재 버전의 major 숫자를 유지합니다.yearweek
: 현재 날짜를 기반으로 계산합니다.build
: 증가된 빌드 번호를 사용합니다.package.json
에 저장합니다.이 프로세스를 통해, 매 빌드마다 자동으로 버전이 업데이트되며, 주차가 변경될 때마다 빌드 번호가 리셋됩니다.
예를 들어, 현재 버전이 1.2336.5
이고 2023 년 37 주차에 새로운 빌드를 한다면:
yearweek
가 2337 로 변경됩니다.build
번호가 1 로 리셋됩니다.1.2337.1
이 됩니다.만약 같은 주 내에 또 다른 빌드를 한다면:
yearweek
는 그대로 2337 입니다.build
번호가 2 로 증가합니다.1.2337.2
가 됩니다.이러한 방식으로, HeadVer 는 제품의 주요 변경사항 (head
), 릴리스 시기 (yearweek
), 그리고 빌드 횟수 (build
) 를 직관적으로 표현할 수 있습니다.
버전 관리를 완전히 자동화하기 위해 GitHub Actions 를 활용해보겠습니다.
.github/workflows/versioning.yml
파일을 생성하고 다음 내용을 추가합니다:
name: Versioning CI
on:
push:
branches-ignore:
- main
- develop
permissions:
contents: write
jobs:
versioning:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
- name: Install dependencies
run: npm install
- name: Update version
id: update_version
run: npm run update-version
- name: Commit updated version
run: |
git config --global user.name 'github-actions[bot]'
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
git add package.json build_number.json
git commit -m "ci: update version to ${{ steps.update_version.outputs.new_version }}"
git push
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
이 워크플로우는 main
과 develop
브랜치를 제외한 모든 브랜치에 push 가 발생할 때마다 자동으로 버전을 업데이트합니다.
HeadVer 를 적용한 후, 버전 변화의 실제 예시를 살펴보겠습니다:
0.2336.1
(2023 년 36 주차 첫 빌드)0.2336.2
0.2337.1
1.2337.2
1.2352.5
1.2401.1
이러한 버전 변화를 통해 제품의 발전 과정과 릴리스 주기를 쉽게 파악할 수 있습니다.
HeadVer 의 유연성은 풀스택 개발 환경에서 더욱 빛을 발합니다. 특히 백엔드와 프론트엔드의 버전을 동기화하여 호환성을 관리하는 데 매우 유용합니다.
우리 팀에서는 다음과 같은 상황에서 백엔드와 프론트엔드의 head
버전을 동시에 올립니다:
이렇게 함으로써 얻는 이점은 다음과 같습니다:
head
버전을 가진 백엔드와 프론트엔드는 완전한 호환성을 보장합니다.head
버전 간에는 호환성 문제가 있을 수 있음을 명확히 표시할 수 있습니다.예를 들어, 현재 백엔드와 프론트엔드의 버전이 각각 2.2345.10
과 2.2345.15
라고 가정해봅시다.
API 구조를 대폭 변경하는 작업을 수행합니다.
변경 작업이 완료되면 백엔드와 프론트엔드의 head
버전을 모두 3 으로 올립니다.
결과적으로 새 버전은 다음과 같이 됩니다:
3.2345.11
3.2345.16
이렇게 하면 버전 번호만 보고도 두 컴포넌트가 호환되는지 즉시 알 수 있습니다.
head
버전이 같은 경우: 완전한 호환성을 기대할 수 있습니다.head
버전이 다른 경우: 호환성 문제가 있을 수 있으므로 주의가 필요합니다.이 방식을 통해 마이크로서비스 아키텍처나 분산 시스템에서도 각 컴포넌트 간의 호환성을 효과적으로 관리할 수 있습니다.
HeadVer 버저닝 시스템을 적용함으로써 얻은 이점은 다음과 같습니다:
HeadVer 도입 후, 우리 팀은 제품의 변경사항과 릴리스 주기를 더욱 명확하게 이해하게 되었고, 이는 제품 관리와 고객 지원 측면에서도 큰 도움이 되었습니다. 특히 풀스택 개발 환경에서 프론트엔드와 백엔드 간의 일관성을 유지하는 데 큰 도움이 되었습니다.여러분도 프로젝트에 HeadVer 를 적용해보는 것은 어떨까요? 버전 관리의 새로운 패러다임을 경험해보실 수 있을 것입니다. 특히 복잡한 시스템 구조를 가진 프로젝트에서 HeadVer 의 장점이 더욱 빛을 발할 것입니다.