본문 바로가기
Infra2026년 10월 8일18분 읽기

releases.json으로 외부 배포 프로그램 버전 자동 동기화 — 매니페스트 설계와 스크립트

YS
김영삼
조회 7
releases.json으로 외부 배포 프로그램 버전 자동 동기화 — 매니페스트 설계와 스크립트

다른 서버에서 배포하는 프로그램의 버전을 내 사이트 다운로드 페이지에 정확히 맞추는 가장 확실한 방법은, 상대 쪽에 releases.json 같은 기계용 매니페스트를 두게 하고 내 쪽 스크립트가 그걸 주기적으로 읽어 반영하는 것이다. 사람이 손으로 고치는 한, 언젠가는 반드시 빠뜨린다.

나는 CADora AI와 한칸(HanKan)이라는 두 프로그램을 youngsam.net 프로그램 페이지에 소개하고 있다. 그런데 설치 파일은 각자 자기 사이트에서 배포한다. 이 글은 그 사이의 버전 정보를 자동으로 맞추기 위해 매니페스트 형식을 어떻게 정했고, 동기화 스크립트에 어떤 안전장치를 넣었는지에 대한 기록이다.

3줄 요약
  • 받기 버튼은 상대 사이트의 고정 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 해시가 적혀 있고, 이게 한국어·영어·일본어·중국어 네 개 언어 본문에 각각 들어 있다. 버전 기록 목록도 있다. 상대가 버전을 올리면 이 모든 게 한꺼번에 낡는다.

2개
외부 사이트에서 배포하는 프로그램
5종
플랫폼별 고정 latest 링크
4개 언어
파일명·해시·용량이 들어간 본문
2시간
자동 동기화 주기

손으로 고치는 걸 상상해 보면 답이 나온다. 버전이 하나 오를 때마다 네 개 언어 본문에서 플랫폼별 파일명과 해시와 용량을 찾아 바꾸고, 버전 배지를 고치고, 버전 기록에 새 항목을 넣어야 한다. 한두 번은 할 수 있다. 하지만 언젠가 일본어 본문의 해시 하나를 빠뜨린다. 그리고 해시가 틀린 다운로드 페이지는 해시가 없는 페이지보다 나쁘다. 사용자가 검증을 해 보면 실패하니까.

영삼
내 생각
정확도를 높이는 핵심은 꼼꼼함이 아니라 사람 손을 빼는 것이라고 본다. 같은 값을 여러 곳에 반복해서 적어야 하는 작업은 사람이 아무리 조심해도 결국 틀린다. 그런 작업은 처음부터 기계가 하도록 설계하는 게 맞다.

PART 02왜 HTML 긁기가 아니라 매니페스트를 요구했나

처음 떠올린 방법은 상대 사이트의 다운로드 페이지를 긁어서 버전을 알아내는 것이었다. 하지만 금방 접었다. 페이지 디자인이 바뀌면 바로 깨지고, 해시나 정확한 용량 같은 값은 페이지에 아예 없을 수도 있다. 무엇보다 긁은 값이 맞는지 확신할 근거가 없다.

그래서 상대 쪽에 /download/latest/releases.json을 만들어 달라고 요청했다. 사람이 읽는 페이지와 별개로, 기계가 읽는 정해진 형식의 파일이다. 최신 버전이 배열 맨 앞에 오고, 각 릴리스에 날짜, 언어별 변경 내역, 플랫폼별 파일 정보가 들어간다.

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 파일은 앞으로도 영원히 이 주소에 있어야 한다. 이게 있어야 버전 기록에서 예전 버전을 받는 링크를 걸 수 있다. 변경 내역을 언어별로 받는 것도 같은 이유다. 내 페이지가 네 개 언어라서 한국어만 받으면 나머지를 내가 번역해야 한다.

영삼
내 생각
남의 사이트에서 데이터를 가져와야 할 때, 나는 HTML을 긁기보다 상대에게 매니페스트 하나를 요구하는 쪽이 훨씬 낫다고 본다. 상대 입장에서도 부담이 작다. 이미 빌드 과정에서 알고 있는 값을 JSON으로 한 번 더 써 주기만 하면 된다. 그 대가로 양쪽 모두 "형식이 깨졌다"는 사고에서 벗어난다.

PART 03동기화 스크립트 설계: 하나의 스크립트, 프로그램마다 타이머 하나

동기화 스크립트는 Node와 Prisma로 만든 하나짜리다. 프로그램마다 따로 만들지 않고, 대상 정보를 환경변수로 넘긴다. 어느 프로그램의 slug인지, 상대 사이트 주소가 뭔지, 어떤 플랫폼 키를 확인할지, 마지막으로 반영한 상태를 어느 파일에 기억할지를 지정한다.

실행은 systemd 타이머가 2시간마다 한다. 프로그램마다 타이머가 하나씩 있다. 그러니 새 프로그램을 같은 방식으로 붙이려면, 같은 형식의 매니페스트만 있다면 환경변수를 바꾼 타이머 하나를 추가하면 끝난다.

1
버전 형식 검사
latest와 각 릴리스의 버전이 숫자.숫자.숫자 형식인지 본다. 형식이 이상하면 아무것도 바꾸지 않고 종료한다.
2
모든 플랫폼 준비 여부 확인
지정한 플랫폼 키 전부에 대해 sha256(64자 16진수), size, name, url이 다 있는지 확인한다. 하나라도 빠지면 이번 주기는 건너뛴다.
3
상태 파일과 비교
마지막으로 반영한 버전과 파일 정보를 상태 파일에서 읽어 비교한다. 첫 실행이면 상태만 기록하고 페이지는 건드리지 않는다.
4
새 버전이면 반영
지난 릴리스의 받기 주소를 버전 고정 url로 바꾸고, 새 Release를 만들고(변경 내역은 notes.ko), 프로그램 버전과 네 개 언어 본문의 파일명·해시·용량·머리말 버전 배지를 치환한다.
5
재빌드면 파일 정보만
버전은 같은데 해시나 용량만 바뀌었다면 재빌드로 보고 파일 정보만 갱신한다.

이 모든 쓰기 작업은 한 트랜잭션 안에서 한다. 본문은 바뀌었는데 버전 기록은 안 바뀌는 식의 어중간한 상태가 생기면 안 되기 때문이다. 중간에 하나라도 실패하면 전부 되돌아가고, 다음 주기에 다시 시도한다. 그리고 --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를 돌려준다. 이상한 버전 문자열이 들어왔을 때 "새 버전"으로 착각하는 것보다 "아무것도 안 함"이 훨씬 안전하다.

영삼
내 생각
이 스크립트에서 단 하나만 남기라면 나는 "반쯤 배포된 상태를 반영하지 않는다"를 남기겠다. 동기화가 2시간 늦는 건 아무 문제가 아니다. 반대로 틀린 해시나 존재하지 않는 파일 링크가 2시간 동안 페이지에 떠 있는 건 신뢰 문제다. 동기화 스크립트는 빠른 것보다 틀리지 않는 것이 먼저다.

PART 05일부러 자동으로 바꾸지 않는 것들

자동화라고 다 바꾸면 안 된다. 나는 두 가지를 일부러 제외했다. 첫째는 스크린샷 설명 속의 버전 숫자다. "x.y.z 버전의 편집 화면" 같은 설명은 그 스크린샷이 실제로 그 버전 화면이기 때문에 맞는 말이다. 이걸 최신 버전 숫자로 바꾸면 오히려 거짓말이 된다.

둘째는 썸네일이다. 처음부터 썸네일에는 버전 숫자를 넣지 않았다. 자동으로 갱신되는 프로그램인데 썸네일에 버전을 박아 두면, 다음 버전업 순간부터 썸네일만 낡는다. 이미지 속 글자는 치환할 수도 없다.

항목자동 갱신 여부이유
프로그램 버전·머리말 버전 배지갱신항상 최신을 보여 줘야 함
4개 언어 본문의 파일명·해시·용량갱신사용자가 검증에 쓰는 값
버전 기록(Release)새 항목 추가변경 내역은 notes.ko 사용
지난 릴리스 받기 주소버전 고정 url로 교체예전 버전을 계속 받을 수 있게
스크린샷 설명 속 버전바꾸지 않음그 화면은 그 버전의 화면
썸네일애초에 버전 없음이미지 속 숫자는 반드시 낡음
치환 대상을 좁게 본문에서 버전 숫자를 통째로 찾아 바꾸면 스크린샷 설명이나 다른 문맥의 숫자까지 바뀐다. 나는 파일명·해시·용량·버전 배지처럼 "바꿔야 할 자리"를 정해 두고 그 자리만 치환한다. 넓게 치환하는 자동화는 언젠가 엉뚱한 문장을 고친다.

PART 06웹훅 대신 폴링을 고른 이유

"상대가 배포할 때 웹훅으로 알려주면 더 빠르지 않냐"는 생각이 당연히 든다. 맞다, 더 빠르다. 하지만 나는 폴링을 골랐다. 이유는 두 가지다.

웹훅빠르지만 손이 많이 감
  • 상대 배포 과정에 호출 코드 추가 필요
  • 내 쪽에 외부에서 들어오는 엔드포인트와 인증 필요
  • 호출이 한 번 실패하면 재시도 설계 필요
  • 배포 순서와 호출 시점이 꼬일 수 있음
폴링(2시간)느리지만 단순
  • 상대는 정적 JSON 하나만 두면 됨
  • 내 쪽에 열어 둘 입구가 없음
  • 실패해도 다음 주기에 저절로 회복
  • 반쯤 배포 상태는 검사로 걸러냄

첫째, 상대 서버 수정이 최소화된다. 상대는 정적 파일 하나만 제대로 만들어 두면 된다. 배포 스크립트에 내 서버를 부르는 코드를 넣어 달라고 할 필요가 없다. 내 쪽에도 외부에서 호출받는 입구를 새로 열 필요가 없다. 입구가 하나 늘면 그걸 지키는 일도 하나 는다.

둘째, 회복이 공짜다. 웹훅은 그 한 번의 호출이 실패하면 누가 다시 불러 줄지 따로 설계해야 한다. 폴링은 이번에 네트워크가 끊겼든, 매니페스트가 반쯤 올라가 있었든, 2시간 뒤에 다시 보면 된다. 버전 정보가 몇 시간 늦게 반영되는 건 다운로드 페이지에서 큰 문제가 아니다. 받기 버튼은 어차피 고정 latest 링크라 항상 최신 파일을 준다.

로그는 journalctl로 본다. 타이머가 돌 때마다 무엇을 확인했고 무엇을 바꿨는지, 혹은 왜 건너뛰었는지 한 줄씩 남는다. 지금 두 프로그램 모두 "up to date"다.

PART 07다음 프로그램을 위한 요청 템플릿

이 구조의 진짜 이득은 두 번째 프로그램부터 나온다. 같은 형식의 매니페스트만 있으면 스크립트는 그대로 쓰고 타이머 하나만 추가하면 된다. 그래서 상대 개발 쪽에 보낼 요청 템플릿을 아예 만들어 두었다.

상대 개발 쪽에 보내는 요청 템플릿 요지
✓플랫폼별 고정 latest 링크: /download/latest/<windows|windows-portable|mac|mac-intel|linux>
✓/download/latest/releases.json을 위 형식대로 제공(최신 릴리스가 배열 맨 앞)
✓각 파일에 name, size(바이트), sha256(64자 16진수), 버전 고정 url 포함
✓변경 내역은 ko·en·ja·zh 네 개 언어 배열로
✓매니페스트는 모든 플랫폼 파일 업로드가 끝난 뒤 마지막에 갱신
✓프로그램 소개 본문, 스크린샷, 아이콘 등 페이지 구성용 자료 목록

"매니페스트는 마지막에 갱신"이라는 항목을 템플릿에 넣었지만, 내 스크립트는 그 약속이 지켜지지 않아도 안전하게 동작하도록 만들었다. 상대가 순서를 지켜 주면 좋고, 안 지켜도 내 쪽 검사가 걸러낸다. 약속과 방어를 둘 다 두는 게 남의 서버와 엮이는 자동화의 기본이라고 생각한다.

외부 배포 버전 동기화에서 지킨 원칙
  1. 여러 언어·여러 자리에 반복되는 값은 사람이 아니라 스크립트가 고친다.
  2. HTML을 긁지 말고 상대에게 기계용 매니페스트를 요구한다. 파일마다 버전 고정 주소를 꼭 받는다.
  3. 모든 플랫폼이 해시·용량·이름·주소를 다 갖췄을 때만 반영하고, 쓰기는 한 트랜잭션으로 묶는다.
  4. 스크린샷 설명처럼 "그 버전이라서 맞는" 숫자는 일부러 건드리지 않는다.
  5. 웹훅보다 폴링을 택해 상대 서버 수정을 줄이고, 실패는 다음 주기가 회복하게 둔다.

자주 묻는 질문

다른 사이트에서 배포하는 프로그램의 최신 버전을 내 사이트에 자동으로 표시하려면 어떻게 하나요?

상대 사이트에 버전과 파일 정보를 담은 JSON 매니페스트를 두게 하고, 내 서버에서 그 파일을 주기적으로 읽어 페이지를 갱신하는 방식이 가장 안정적이다. 다운로드 버튼은 상대 쪽 고정 latest 링크로 걸어 두면 버전 반영이 조금 늦어도 사용자는 항상 최신 파일을 받는다.

releases.json에는 어떤 정보가 들어가야 하나요?

최신 버전 번호와 릴리스 목록이 기본이고, 각 릴리스에는 버전, 날짜, 변경 내역, 플랫폼별 파일 정보가 필요하다. 파일 정보에는 이름, 바이트 단위 용량, SHA-256 해시, 그리고 버전이 고정된 다운로드 주소를 넣는 것이 좋다. 고정 주소가 있어야 예전 버전 링크를 유지할 수 있다.

배포 도중에 동기화가 돌면 잘못된 정보가 반영되지 않나요?

그래서 모든 플랫폼 파일이 해시·용량·이름·주소를 다 갖췄을 때만 반영하도록 검사해야 한다. 하나라도 비어 있으면 그 주기는 건너뛰고 다음 주기에 다시 확인한다. 쓰기 작업을 한 트랜잭션으로 묶으면 중간 실패로 인한 어중간한 상태도 막을 수 있다.

버전 문자열 비교는 왜 문자열 비교로 하면 안 되나요?

문자열로 비교하면 "1.0.10"이 "1.0.9"보다 작다고 판단되는 문제가 생긴다. 점으로 나눠 각 자리를 숫자로 바꾼 뒤 앞자리부터 비교해야 한다. 형식이 예상과 다르면 새 버전으로 보지 않고 아무것도 하지 않는 편이 안전하다.

웹훅과 폴링 중 무엇이 버전 동기화에 더 적합한가요?

실시간성이 중요하면 웹훅이 낫지만, 상대 배포 과정에 호출 코드를 넣고 내 쪽에 인증된 입구를 열어야 한다. 다운로드 페이지처럼 몇 시간 늦어도 괜찮은 경우에는 폴링이 단순하고, 한 번 실패해도 다음 주기에 자연스럽게 회복된다는 장점이 크다.

참고 자료

댓글 0

아직 댓글이 없습니다.
Ctrl+Enter로 등록