다른 서버에서 배포하는 프로그램의 버전을 내 사이트 다운로드 페이지에 정확히 맞추는 가장 확실한 방법은, 상대 쪽에 releases.json 같은 기계용 매니페스트를 두게 하고 내 쪽 스크립트가 그걸 주기적으로 읽어 반영하는 것이다. 사람이 손으로 고치는 한, 언젠가는 반드시 빠뜨린다.
나는 CADora AI와 한칸(HanKan)이라는 두 프로그램을 youngsam.net 프로그램 페이지에 소개하고 있다. 그런데 설치 파일은 각자 자기 사이트에서 배포한다. 이 글은 그 사이의 버전 정보를 자동으로 맞추기 위해 매니페스트 형식을 어떻게 정했고, 동기화 스크립트에 어떤 안전장치를 넣었는지에 대한 기록이다.
- 받기 버튼은 상대 사이트의 고정 latest 링크로 걸고, 버전·파일명·용량·SHA-256·버전 기록은 상대가 제공하는
releases.json을 2시간마다 읽어 자동 반영한다. - 가장 중요한 안전장치는 "모든 플랫폼 파일이 해시·용량·이름·주소를 다 갖췄을 때만 반영"이다. 반쯤 배포된 상태를 페이지에 올리지 않는다.
- 웹훅 대신 폴링을 고른 이유는 상대 서버 수정을 최소화하고, 한 번 실패해도 다음 주기에 저절로 회복되기 때문이다.
PART 01문제: 상대 사이트가 버전업하면 내 페이지가 낡는다
CADora AI는 오토캐드를 대체하는 무료 AI CAD이고, 한칸은 엑셀을 대체하는 AI 스프레드시트다. 둘 다 설치 파일을 각자의 사이트(cadora.kbox.uk, hankan.kbox.uk)에서 배포한다. 내 프로그램 페이지의 받기 버튼은 그쪽의 고정 링크로 연결했다. 형태는 https://<사이트>/download/latest/<플랫폼>이고, 플랫폼은 windows, windows-portable, mac, mac-intel, linux 다섯 가지다.
받기 버튼 자체는 문제가 없다. 링크가 항상 최신 파일을 가리키니까. 문제는 그 주변이었다. 페이지에는 버전 표시, 파일 이름, 용량, SHA-256 해시가 적혀 있고, 이게 한국어·영어·일본어·중국어 네 개 언어 본문에 각각 들어 있다. 버전 기록 목록도 있다. 상대가 버전을 올리면 이 모든 게 한꺼번에 낡는다.
손으로 고치는 걸 상상해 보면 답이 나온다. 버전이 하나 오를 때마다 네 개 언어 본문에서 플랫폼별 파일명과 해시와 용량을 찾아 바꾸고, 버전 배지를 고치고, 버전 기록에 새 항목을 넣어야 한다. 한두 번은 할 수 있다. 하지만 언젠가 일본어 본문의 해시 하나를 빠뜨린다. 그리고 해시가 틀린 다운로드 페이지는 해시가 없는 페이지보다 나쁘다. 사용자가 검증을 해 보면 실패하니까.
PART 02왜 HTML 긁기가 아니라 매니페스트를 요구했나
처음 떠올린 방법은 상대 사이트의 다운로드 페이지를 긁어서 버전을 알아내는 것이었다. 하지만 금방 접었다. 페이지 디자인이 바뀌면 바로 깨지고, 해시나 정확한 용량 같은 값은 페이지에 아예 없을 수도 있다. 무엇보다 긁은 값이 맞는지 확신할 근거가 없다.
그래서 상대 쪽에 /download/latest/releases.json을 만들어 달라고 요청했다. 사람이 읽는 페이지와 별개로, 기계가 읽는 정해진 형식의 파일이다. 최신 버전이 배열 맨 앞에 오고, 각 릴리스에 날짜, 언어별 변경 내역, 플랫폼별 파일 정보가 들어간다.
{
"latest": "1.0.6",
"releases": [
{
"version": "1.0.6",
"date": "2026-10-01",
"notes": {
"ko": ["변경 사항 예시"],
"en": ["Example change"],
"ja": ["変更例"],
"zh": ["更新示例"]
},
"files": {
"windows": {
"name": "App-Setup-1.0.6.exe",
"size": 123456789,
"sha256": "<64자리 16진수>",
"url": "https://example.com/download/1.0.6/App-Setup-1.0.6.exe"
}
}
}
]
}여기서 신경 쓴 건 파일마다 들어 있는 url이다. 이건 latest 링크가 아니라 버전이 고정된 주소다. 1.0.6 파일은 앞으로도 영원히 이 주소에 있어야 한다. 이게 있어야 버전 기록에서 예전 버전을 받는 링크를 걸 수 있다. 변경 내역을 언어별로 받는 것도 같은 이유다. 내 페이지가 네 개 언어라서 한국어만 받으면 나머지를 내가 번역해야 한다.
PART 03동기화 스크립트 설계: 하나의 스크립트, 프로그램마다 타이머 하나
동기화 스크립트는 Node와 Prisma로 만든 하나짜리다. 프로그램마다 따로 만들지 않고, 대상 정보를 환경변수로 넘긴다. 어느 프로그램의 slug인지, 상대 사이트 주소가 뭔지, 어떤 플랫폼 키를 확인할지, 마지막으로 반영한 상태를 어느 파일에 기억할지를 지정한다.
실행은 systemd 타이머가 2시간마다 한다. 프로그램마다 타이머가 하나씩 있다. 그러니 새 프로그램을 같은 방식으로 붙이려면, 같은 형식의 매니페스트만 있다면 환경변수를 바꾼 타이머 하나를 추가하면 끝난다.
latest와 각 릴리스의 버전이 숫자.숫자.숫자 형식인지 본다. 형식이 이상하면 아무것도 바꾸지 않고 종료한다.이 모든 쓰기 작업은 한 트랜잭션 안에서 한다. 본문은 바뀌었는데 버전 기록은 안 바뀌는 식의 어중간한 상태가 생기면 안 되기 때문이다. 중간에 하나라도 실패하면 전부 되돌아가고, 다음 주기에 다시 시도한다. 그리고 --dry 모드가 있어서, 실제로 쓰지 않고 무엇이 바뀔지만 출력해 볼 수 있다. 새 프로그램을 처음 붙일 때 확인용으로 쓰기 좋다.
첫 실행에서 상태만 초기화하는 것도 의도한 동작이다. 스크립트를 처음 붙였을 때는 페이지가 이미 사람 손으로 맞춰져 있었다. 그 상태에서 "모르는 버전이 나타났으니 새 릴리스"라고 판단해 버리면 중복 기록이 생긴다. 그래서 첫 실행은 "지금 상태가 기준"이라고만 기억하고 끝낸다.
PART 04가장 중요한 안전장치: 반쯤 배포된 상태는 반영하지 않는다
상대 쪽 배포는 한순간에 끝나지 않는다. Windows 파일을 먼저 올리고, 몇 분 뒤 Mac 파일을 올리고, 마지막에 매니페스트를 갱신하는 식일 수 있다. 순서가 바뀌어 매니페스트가 먼저 갱신되면, 그 사이에 내 스크립트가 돌 경우 일부 플랫폼 파일 정보가 비어 있는 매니페스트를 보게 된다.
이걸 반영하면 페이지의 해시 칸이 비거나, 받기 버튼이 아직 없는 파일을 가리킨다. 그래서 스크립트는 지정한 모든 플랫폼이 다 갖춰졌을 때만 움직인다. 아래가 그 검사와 버전 비교를 줄인 코드다.
const HEX64 = /^[0-9a-f]{64}$/i
const SEMVER = /^\d+\.\d+\.\d+$/
// 지정한 플랫폼 파일이 전부 갖춰졌는가
function allPlatformsReady(release, platforms) {
return platforms.every((key) => {
const f = release.files && release.files[key]
return f && HEX64.test(f.sha256 || '') &&
Number.isInteger(f.size) && f.size > 0 &&
typeof f.name === 'string' && f.name.length > 0 &&
/^https:\/\//.test(f.url || '')
})
}
// a가 b보다 새 버전인가 (숫자.숫자.숫자만 허용)
function newer(a, b) {
if (!SEMVER.test(a) || !SEMVER.test(b)) return false
const x = a.split('.').map(Number), y = b.split('.').map(Number)
for (let i = 0; i < 3; i++) {
if (x[i] !== y[i]) return x[i] > y[i]
}
return false
}newer()는 일부러 단순하게 만들었다. 문자열 비교로 하면 "1.0.10"이 "1.0.9"보다 작다고 나오는 고전적인 실수를 한다. 그래서 숫자로 쪼개 앞자리부터 비교한다. 형식이 맞지 않으면 무조건 false를 돌려준다. 이상한 버전 문자열이 들어왔을 때 "새 버전"으로 착각하는 것보다 "아무것도 안 함"이 훨씬 안전하다.
PART 05일부러 자동으로 바꾸지 않는 것들
자동화라고 다 바꾸면 안 된다. 나는 두 가지를 일부러 제외했다. 첫째는 스크린샷 설명 속의 버전 숫자다. "x.y.z 버전의 편집 화면" 같은 설명은 그 스크린샷이 실제로 그 버전 화면이기 때문에 맞는 말이다. 이걸 최신 버전 숫자로 바꾸면 오히려 거짓말이 된다.
둘째는 썸네일이다. 처음부터 썸네일에는 버전 숫자를 넣지 않았다. 자동으로 갱신되는 프로그램인데 썸네일에 버전을 박아 두면, 다음 버전업 순간부터 썸네일만 낡는다. 이미지 속 글자는 치환할 수도 없다.
PART 06웹훅 대신 폴링을 고른 이유
"상대가 배포할 때 웹훅으로 알려주면 더 빠르지 않냐"는 생각이 당연히 든다. 맞다, 더 빠르다. 하지만 나는 폴링을 골랐다. 이유는 두 가지다.
- 상대 배포 과정에 호출 코드 추가 필요
- 내 쪽에 외부에서 들어오는 엔드포인트와 인증 필요
- 호출이 한 번 실패하면 재시도 설계 필요
- 배포 순서와 호출 시점이 꼬일 수 있음
- 상대는 정적 JSON 하나만 두면 됨
- 내 쪽에 열어 둘 입구가 없음
- 실패해도 다음 주기에 저절로 회복
- 반쯤 배포 상태는 검사로 걸러냄
첫째, 상대 서버 수정이 최소화된다. 상대는 정적 파일 하나만 제대로 만들어 두면 된다. 배포 스크립트에 내 서버를 부르는 코드를 넣어 달라고 할 필요가 없다. 내 쪽에도 외부에서 호출받는 입구를 새로 열 필요가 없다. 입구가 하나 늘면 그걸 지키는 일도 하나 는다.
둘째, 회복이 공짜다. 웹훅은 그 한 번의 호출이 실패하면 누가 다시 불러 줄지 따로 설계해야 한다. 폴링은 이번에 네트워크가 끊겼든, 매니페스트가 반쯤 올라가 있었든, 2시간 뒤에 다시 보면 된다. 버전 정보가 몇 시간 늦게 반영되는 건 다운로드 페이지에서 큰 문제가 아니다. 받기 버튼은 어차피 고정 latest 링크라 항상 최신 파일을 준다.
로그는 journalctl로 본다. 타이머가 돌 때마다 무엇을 확인했고 무엇을 바꿨는지, 혹은 왜 건너뛰었는지 한 줄씩 남는다. 지금 두 프로그램 모두 "up to date"다.
PART 07다음 프로그램을 위한 요청 템플릿
이 구조의 진짜 이득은 두 번째 프로그램부터 나온다. 같은 형식의 매니페스트만 있으면 스크립트는 그대로 쓰고 타이머 하나만 추가하면 된다. 그래서 상대 개발 쪽에 보낼 요청 템플릿을 아예 만들어 두었다.
/download/latest/<windows|windows-portable|mac|mac-intel|linux>/download/latest/releases.json을 위 형식대로 제공(최신 릴리스가 배열 맨 앞)"매니페스트는 마지막에 갱신"이라는 항목을 템플릿에 넣었지만, 내 스크립트는 그 약속이 지켜지지 않아도 안전하게 동작하도록 만들었다. 상대가 순서를 지켜 주면 좋고, 안 지켜도 내 쪽 검사가 걸러낸다. 약속과 방어를 둘 다 두는 게 남의 서버와 엮이는 자동화의 기본이라고 생각한다.
- 여러 언어·여러 자리에 반복되는 값은 사람이 아니라 스크립트가 고친다.
- HTML을 긁지 말고 상대에게 기계용 매니페스트를 요구한다. 파일마다 버전 고정 주소를 꼭 받는다.
- 모든 플랫폼이 해시·용량·이름·주소를 다 갖췄을 때만 반영하고, 쓰기는 한 트랜잭션으로 묶는다.
- 스크린샷 설명처럼 "그 버전이라서 맞는" 숫자는 일부러 건드리지 않는다.
- 웹훅보다 폴링을 택해 상대 서버 수정을 줄이고, 실패는 다음 주기가 회복하게 둔다.
자주 묻는 질문
다른 사이트에서 배포하는 프로그램의 최신 버전을 내 사이트에 자동으로 표시하려면 어떻게 하나요?
상대 사이트에 버전과 파일 정보를 담은 JSON 매니페스트를 두게 하고, 내 서버에서 그 파일을 주기적으로 읽어 페이지를 갱신하는 방식이 가장 안정적이다. 다운로드 버튼은 상대 쪽 고정 latest 링크로 걸어 두면 버전 반영이 조금 늦어도 사용자는 항상 최신 파일을 받는다.
releases.json에는 어떤 정보가 들어가야 하나요?
최신 버전 번호와 릴리스 목록이 기본이고, 각 릴리스에는 버전, 날짜, 변경 내역, 플랫폼별 파일 정보가 필요하다. 파일 정보에는 이름, 바이트 단위 용량, SHA-256 해시, 그리고 버전이 고정된 다운로드 주소를 넣는 것이 좋다. 고정 주소가 있어야 예전 버전 링크를 유지할 수 있다.
배포 도중에 동기화가 돌면 잘못된 정보가 반영되지 않나요?
그래서 모든 플랫폼 파일이 해시·용량·이름·주소를 다 갖췄을 때만 반영하도록 검사해야 한다. 하나라도 비어 있으면 그 주기는 건너뛰고 다음 주기에 다시 확인한다. 쓰기 작업을 한 트랜잭션으로 묶으면 중간 실패로 인한 어중간한 상태도 막을 수 있다.
버전 문자열 비교는 왜 문자열 비교로 하면 안 되나요?
문자열로 비교하면 "1.0.10"이 "1.0.9"보다 작다고 판단되는 문제가 생긴다. 점으로 나눠 각 자리를 숫자로 바꾼 뒤 앞자리부터 비교해야 한다. 형식이 예상과 다르면 새 버전으로 보지 않고 아무것도 하지 않는 편이 안전하다.
웹훅과 폴링 중 무엇이 버전 동기화에 더 적합한가요?
실시간성이 중요하면 웹훅이 낫지만, 상대 배포 과정에 호출 코드를 넣고 내 쪽에 인증된 입구를 열어야 한다. 다운로드 페이지처럼 몇 시간 늦어도 괜찮은 경우에는 폴링이 단순하고, 한 번 실패해도 다음 주기에 자연스럽게 회복된다는 장점이 크다.

댓글 0