Skip to content
Published on

업데이트 가능한 솔루션 만드는 법: 설치 마법사부터 안전한 자동 업데이트까지

Authors

들어가며 — 설치는 시작일 뿐이다

소프트웨어를 사용자에게 전달하는 순간부터 진짜 문제가 시작됩니다. 처음 설치는 한 번뿐이지만, 업데이트는 제품이 살아 있는 내내 계속됩니다. 버그를 고치고, 기능을 더하고, 보안 구멍을 막으려면 이미 배포된 수천, 수만 대의 설치본을 안전하게 새 버전으로 갈아 끼워야 합니다.

이 글은 두 가지를 함께 다룹니다. 하나는 사용자가 처음 만나는 **설치 마법사(wizard)**의 UX 설계이고, 다른 하나는 그 뒤에서 조용히 돌아가는 자동 업데이트 메커니즘입니다. 데스크톱 앱이든 모바일 앱이든 웹 PWA든, "업데이트 가능한 솔루션"을 만드는 원리는 놀랄 만큼 비슷합니다. 다운로드하고, 검증하고, 원자적으로 교체하고, 실패하면 되돌린다. 이 네 단계가 전부입니다.

1부. 설치 마법사의 UX

멀티스텝 흐름과 단계별 검증

마법사는 복잡한 설정을 여러 단계로 쪼갠 UI입니다. 한 화면에 모든 옵션을 쏟아붓는 대신, 사용자를 한 걸음씩 안내합니다. 좋은 마법사에는 공통된 뼈대가 있습니다.

  • 단계별 검증(per-step validation): 다음으로 넘어가기 전에 그 단계의 입력을 즉시 검증합니다. 마지막에 한꺼번에 실패를 터뜨리지 않습니다. 예를 들어 설치 경로를 고르는 단계라면, 그 경로에 쓰기 권한이 있는지·용량이 충분한지를 그 자리에서 확인합니다.
  • 진행 표시와 뒤로 가기(progress and back): 지금 몇 단계 중 몇 번째인지 보여주고, 이전 단계로 자유롭게 돌아갈 수 있어야 합니다. 되돌아가도 이미 입력한 값이 사라지지 않아야 합니다.
  • 합리적 기본값(sensible defaults): 대부분의 사용자는 기본값 그대로 "다음"만 누릅니다. 그러니 기본값이 곧 대다수의 실제 설정이 됩니다. 기본값은 가장 안전하고 흔한 선택이어야 합니다.

미리보기, 드라이런, 그리고 실패 시 롤백

설치나 업데이트는 시스템을 바꾸는 작업입니다. 사용자가 "적용"을 누르기 전에 무엇이 바뀌는지 보여주면 신뢰가 올라갑니다.

  • 드라이런/미리보기(dry-run/preview): 실제로 파일을 건드리기 전에, 어떤 파일이 추가·수정·삭제될지 요약해 보여줍니다. 서버 설정 마법사라면 "이 설정으로 연결을 시도해 본" 결과를 먼저 보여줄 수 있습니다.
  • 실패 시 롤백(rollback on failure): 설치 도중 중간에 실패하면, 어중간하게 반쯤 설치된 상태로 남겨선 안 됩니다. 트랜잭션처럼, 전부 성공하거나 전부 원래대로 되돌아가야 합니다.

재개 가능성

큰 파일을 내려받는 도중 네트워크가 끊기거나, 사용자가 실수로 창을 닫을 수 있습니다. 그때마다 처음부터 다시 하게 만들면 사용자는 지칩니다. **재개 가능성(resumability)**은 중단된 지점의 상태를 저장해 두었다가, 다시 열었을 때 그 지점부터 이어가는 능력입니다. 다운로드는 HTTP Range 요청으로 이어받고, 마법사의 입력값은 로컬에 임시 저장해 둡니다.

2부. 업데이트 메커니즘

시맨틱 버저닝과 업데이트 채널

업데이트를 관리하려면 먼저 버전을 체계적으로 매겨야 합니다. **시맨틱 버저닝(SemVer)**은 버전을 MAJOR.MINOR.PATCH 세 자리로 나눕니다. 호환성이 깨지면 MAJOR, 기능이 추가되면 MINOR, 버그만 고치면 PATCH를 올립니다. 클라이언트는 이 규칙을 보고 "이 업데이트가 안전한 자잘한 수정인지, 조심해야 할 큰 변경인지"를 판단할 수 있습니다.

여기에 **업데이트 채널(update channel)**을 더합니다. 같은 제품이라도 stable(안정), beta(베타), nightly(매일 빌드)처럼 여러 흐름을 두어, 위험을 감수할 수 있는 사용자에게만 먼저 새 버전을 흘려보냅니다. 대부분의 사용자는 stable에 두고, 얼리어답터는 beta로 옮깁니다.

전체 업데이트 vs 델타/차분 업데이트

가장 단순한 방법은 새 버전 전체를 통째로 내려받아 갈아 끼우는 **전체 업데이트(full update)**입니다. 구현이 쉽지만, 앱이 수백 MB라면 한 줄만 고쳐도 매번 수백 MB를 내려받아야 합니다.

**델타/차분 업데이트(delta/differential update)**는 이 낭비를 없앱니다. 이전 버전과 새 버전의 **차이(diff)**만 내려받아 로컬에서 합칩니다. 대표적인 도구가 있습니다.

  • bsdiff: 바이너리 두 개의 차이를 작은 패치로 만들어 냅니다. 널리 쓰이는 고전적 방식입니다.
  • Courgette: 크롬이 만든 방식으로, 실행 파일을 어셈블리 수준에서 분석해 bsdiff보다 훨씬 작은 패치를 만듭니다. 재컴파일로 주소가 통째로 밀려도 실제 변경은 작다는 점을 이용합니다.

델타 업데이트는 대역폭을 크게 아끼지만, "이전 버전이 정확히 그 버전이어야" 패치가 맞습니다. 그래서 보통 여러 버전에서 최신으로 가는 여러 개의 패치를 준비하거나, 델타가 실패하면 전체 업데이트로 자동 폴백하도록 만듭니다.

원자적 교체와 롤백

업데이트에서 가장 위험한 순간은 파일을 실제로 갈아 끼우는 때입니다. 옛 파일을 지우고 새 파일을 쓰는 중간에 정전이 나면? **원자적 교체(atomic swap)**가 이 문제를 막습니다.

핵심은 "완전히 준비된 뒤 마지막에 딱 한 번 전환한다"는 것입니다. 새 버전을 옆에 통째로 설치해 두고(예: v2 폴더), 검증이 끝나면 "현재 버전"을 가리키는 포인터(심볼릭 링크나 설정)를 v1에서 v2로 한 번에 바꿉니다. 파일 시스템의 원자적 rename을 쓰면, 어느 순간에도 사용자는 온전한 v1 아니면 온전한 v2를 봅니다. 반쯤 섞인 상태는 존재하지 않습니다. 문제가 생기면 포인터를 v1로 되돌리기만 하면 **롤백(rollback)**이 끝납니다.

  /app/current  ->  /app/versions/1.4.0   (원자적 rename 으로 전환)
                    /app/versions/1.5.0   <- 미리 설치 + 검증 완료

  전환 후:
  /app/current  ->  /app/versions/1.5.0
  실패 시 current 포인터를 1.4.0 으로 되돌리면 롤백 완료

스테이지드/카나리 롤아웃

새 버전을 모든 사용자에게 동시에 밀면, 숨어 있던 버그가 전체를 덮칩니다. **스테이지드/카나리 롤아웃(staged/canary rollout)**은 이를 막습니다. 먼저 1%에게만 배포하고, 오류율과 지표를 관찰합니다. 문제가 없으면 5%, 25%, 100%로 서서히 넓힙니다. 광산의 카나리아처럼, 소수의 사용자가 먼저 위험을 감지해 주는 셈입니다. 이상이 보이면 즉시 롤아웃을 멈추거나 되돌립니다.

백그라운드 다운로드, 재시작 시 적용

사용자를 멈춰 세우고 진행 막대를 지켜보게 하는 건 나쁜 경험입니다. 성숙한 업데이터는 백그라운드에서 조용히 내려받고(background download), 준비가 끝나면 "다음에 재시작할 때 적용됩니다"라고만 알립니다. 그리고 사용자가 앱을 다시 켤 때 이미 받아 둔 새 버전으로 매끄럽게 전환합니다. 크롬, VS Code, macOS의 여러 앱이 이 방식을 씁니다.

3부. 무결성과 보안

업데이트 채널은 공격자에게 매력적인 표적입니다. 만약 가짜 업데이트를 밀어 넣을 수 있다면, 공격자는 사용자의 모든 기기에서 코드를 실행할 수 있습니다. 그래서 무결성과 보안은 선택이 아니라 필수입니다.

코드 서명과 서명·해시 검증

**코드 서명(code signing)**은 개발자의 비밀 키로 배포물에 디지털 서명을 붙이는 것입니다. 클라이언트는 공개 키로 그 서명을 검증해, 이 파일이 진짜 그 개발자에게서 왔고 도중에 변조되지 않았음을 확인합니다.

철칙은 **적용하기 전에 검증(verify before applying)**입니다. 내려받은 패치의 해시와 서명을 먼저 확인하고, 통과한 경우에만 설치를 진행합니다. 순서가 뒤바뀌면(먼저 적용하고 나중에 검증) 이미 늦습니다.

TUF의 아이디어

**TUF(The Update Framework)**는 업데이트 시스템을 겨냥한 특수한 공격까지 방어하도록 설계된 프레임워크입니다. 단일 서명 키 하나가 아니라 역할을 나눈 여러 키(루트, 타깃, 스냅샷, 타임스탬프)를 두고, 키 하나가 털려도 전체가 무너지지 않게 합니다. 핵심 아이디어 몇 가지만 챙겨도 큰 도움이 됩니다.

  • 역할 분리와 키 회전: 서명 권한을 나누고, 키가 노출되면 갈아 끼울 수 있게 합니다.
  • 신선도 보장(freshness): 타임스탬프 메타데이터로 "지금 받은 이 메타데이터가 최신인지"를 확인해, 오래된 정보를 새 것처럼 속이는 공격을 막습니다.

HTTPS, 핀닝, 다운그레이드 방지

  • HTTPS와 인증서 핀닝(pinning): 업데이트는 반드시 HTTPS로 받고, 중요한 경우 서버 인증서를 앱에 고정(pinning)해 중간자 공격을 어렵게 만듭니다.
  • 다운그레이드 공격(downgrade attack) 방지: 공격자는 서명이 유효한 옛 취약 버전을 다시 밀어 알려진 구멍을 되살리려 합니다. 이를 막으려면 클라이언트가 현재 버전보다 낮은 버전으로는 절대 내려가지 않게 하고, 서버가 제시한 버전 번호가 롤백된 것은 아닌지 확인해야 합니다.

4부. 생태계 도구들

바퀴를 새로 발명할 필요는 없습니다. 플랫폼마다 성숙한 업데이트 도구가 있습니다.

  • Squirrel: 윈도우와 macOS용 업데이트 프레임워크입니다. 델타 업데이트와 조용한 백그라운드 설치에 강합니다.
  • Sparkle: macOS 앱의 사실상 표준 업데이터입니다. appcast라는 RSS/XML 피드로 새 버전 정보를 알리고, 서명 검증을 내장합니다.
  • electron-updater: 일렉트론 앱의 자동 업데이트를 담당하며, 내부적으로 Squirrel과 appcast 스타일 피드를 함께 씁니다.
  • Tauri updater: 러스트 기반 Tauri 앱의 공식 업데이터로, 서명 검증과 업데이트 매니페스트를 기본 제공합니다.
  • OS 패키지 매니저: 리눅스라면 apt, dnf 같은 시스템 패키지 매니저가 업데이트·의존성·서명 검증을 통째로 맡아 줍니다. 앱이 직접 업데이터를 만들 필요가 줄어듭니다.
  • 모바일 인앱 업데이트(in-app updates): 안드로이드의 In-App Updates API처럼, 스토어를 거치되 앱 안에서 유연/즉시 업데이트를 유도할 수 있습니다.
  • 웹/PWA 서비스 워커: 웹은 새로고침이 곧 업데이트지만, PWA는 서비스 워커의 업데이트 수명주기를 통해 "새 버전 준비됨 → 다음 방문에 활성화"를 세밀하게 제어합니다. 아래에서 예제로 다룹니다.
  • 서버 주도 기능 플래그/원격 설정(remote config): 코드를 새로 배포하지 않고도, 서버가 내려주는 플래그로 기능을 켜고 끄거나 점진 노출할 수 있습니다. 롤아웃과 즉시 롤백의 또 다른 축입니다.

5부. 업데이트 시 데이터 마이그레이션

코드만 새 버전으로 바뀌는 게 아닙니다. 로컬 데이터베이스나 설정 파일의 **형식(schema)**도 함께 진화합니다. 여기서 자주 사고가 납니다.

  • 스키마/버전 마이그레이션: 데이터에 버전 번호를 붙이고, 앱이 켜질 때 "현재 데이터 버전 → 목표 버전"으로 순서대로 마이그레이션 스크립트를 적용합니다. 마이그레이션은 반드시 순차적이고 누적적이어야 합니다.
  • 멱등한 마이그레이션(idempotent migration): 같은 마이그레이션을 두 번 돌려도 결과가 같아야 합니다. 중간에 실패하고 재시작하는 상황이 흔하기 때문입니다. "컬럼이 없으면 추가"처럼 조건을 확인하고 실행합니다.
  • 전방/후방 호환성(forward/backward compatibility): 롤백을 대비해, 새 버전이 쓴 데이터를 옛 버전이 (완벽하진 않아도) 깨지지 않고 읽을 수 있으면 좋습니다. 새 필드는 옵셔널로 추가하고, 필드를 곧바로 지우기보다 한동안 무시하는 편이 안전합니다.

예제: 작은 업데이트 매니페스트

서버가 클라이언트에게 "최신 버전이 무엇이고, 어디서 받고, 해시는 무엇인지"를 알려주는 매니페스트는 대개 이런 모습입니다.

{
  "channel": "stable",
  "latest": "1.5.0",
  "minimumSupported": "1.2.0",
  "releasedAt": "2026-07-03T09:00:00Z",
  "artifacts": [
    {
      "platform": "darwin-arm64",
      "url": "https://updates.example.com/app/1.5.0/app-arm64.zip",
      "sha256": "9f2c1b7e0a4d6f8b3c5e7a9d1f2b4c6e8a0d2f4b6c8e0a2d4f6b8c0e2a4d6f8b",
      "size": 41231884,
      "signature": "MEUCIQD...base64-signature..."
    },
    {
      "platform": "win32-x64",
      "url": "https://updates.example.com/app/1.5.0/app-x64.nupkg",
      "sha256": "1a3c5e7b9d0f2a4c6e8b0d2f4a6c8e0b2d4f6a8c0e2b4d6f8a0c2e4b6d8f0a2c",
      "size": 39882140,
      "signature": "MEQCIF...base64-signature..."
    }
  ],
  "delta": {
    "from": "1.4.0",
    "url": "https://updates.example.com/app/1.4.0-to-1.5.0.patch",
    "sha256": "7b9d1f3a5c7e9b0d2f4a6c8e0b2d4f6a8c0e2b4d6f8a0c2e4b6d8f0a2c4e6b8d"
  }
}

클라이언트는 이 매니페스트를 HTTPS로 받아, minimumSupported로 다운그레이드를 막고, 아티팩트를 내려받은 뒤 sha256signature를 검증하고, 통과하면 원자적으로 교체합니다.

예제: 서비스 워커 업데이트

PWA에서 서비스 워커는 "설치된 앱"처럼 캐시되므로, 새 버전을 사용자에게 알리고 매끄럽게 전환하는 흐름이 필요합니다. 아래는 새 서비스 워커가 대기 상태가 되면 사용자에게 알리고, 동의하면 즉시 활성화하는 최소 예제입니다.

// 페이지 쪽 코드
navigator.serviceWorker.register("/sw.js").then((reg) => {
  reg.addEventListener("updatefound", () => {
    const newWorker = reg.installing;
    newWorker.addEventListener("statechange", () => {
      // 새 워커가 설치를 마치고, 기존 워커가 아직 페이지를 제어 중이면
      if (newWorker.state === "installed" && navigator.serviceWorker.controller) {
        // 사용자에게 "새 버전이 준비되었습니다" 배너를 보여줄 시점
        showUpdateBanner(() => {
          // 사용자가 "지금 갱신"을 누르면 대기 중 워커를 즉시 활성화
          newWorker.postMessage({ type: "SKIP_WAITING" });
        });
      }
    });
  });
});

// 컨트롤러가 바뀌면(=새 워커가 활성화되면) 한 번만 새로고침
let refreshing = false;
navigator.serviceWorker.addEventListener("controllerchange", () => {
  if (refreshing) return;
  refreshing = true;
  window.location.reload();
});
// sw.js — 서비스 워커 쪽 코드
self.addEventListener("message", (event) => {
  if (event.data && event.data.type === "SKIP_WAITING") {
    // 대기 상태를 건너뛰고 이 워커를 즉시 활성 워커로 승격
    self.skipWaiting();
  }
});

self.addEventListener("activate", (event) => {
  // 활성화되는 즉시 열려 있는 모든 탭의 제어권을 가져온다
  event.waitUntil(self.clients.claim());
});

결정 체크리스트

새 프로젝트에서 업데이트 전략을 정할 때 스스로에게 던지면 좋은 질문들입니다.

  • 전달 경로: 스토어/패키지 매니저를 통하는가, 자체 업데이터를 두는가? 가능하면 플랫폼 표준(Sparkle, Squirrel, apt/dnf, 스토어 인앱 업데이트)을 먼저 검토합니다.
  • 업데이트 크기: 델타 업데이트가 필요할 만큼 앱이 큰가? 델타를 쓴다면 전체 업데이트 폴백을 반드시 함께 둡니다.
  • 적용 안전성: 원자적 교체와 롤백이 준비되어 있는가? 반쯤 설치된 상태가 절대 남지 않는가?
  • 무결성: 적용 전에 서명과 해시를 검증하는가? 다운그레이드를 막는가? HTTPS인가?
  • 롤아웃: 카나리로 소수에게 먼저 내보내고 지표를 보는가? 한 번에 롤백할 스위치가 있는가?
  • 채널: stable/beta 채널을 나눌 것인가? 기능 플래그로 코드 배포 없이 껐다 켤 수 있는가?
  • 데이터: 스키마 마이그레이션이 멱등하고 순차적인가? 롤백 시 옛 버전이 새 데이터를 견디는가?
  • 경험: 백그라운드 다운로드 후 재시작 시 적용하는가? 마법사에 단계별 검증·뒤로 가기·재개가 있는가?

마치며

업데이트 가능한 솔루션의 본질은 사실 단순합니다. 다운로드하고, 검증하고, 원자적으로 교체하고, 실패하면 되돌린다. 여기에 사용자를 배려하는 마법사 UX(단계별 검증, 미리보기, 재개), 안전하게 넓히는 롤아웃(카나리, 채널), 공격을 막는 무결성(코드 서명, TUF, 다운그레이드 방지), 그리고 데이터의 진화를 다루는 마이그레이션(멱등·순차·호환성)을 얹으면 됩니다.

플랫폼마다 좋은 도구가 이미 있습니다. Sparkle, Squirrel, electron-updater, Tauri, 서비스 워커, OS 패키지 매니저. 중요한 것은 이 원리들을 이해하고, 내 제품의 위험과 규모에 맞는 조합을 고르는 일입니다. 설치는 시작일 뿐이고, 좋은 업데이트 경험이야말로 제품을 오래 살아 있게 합니다.

참고 자료