Sik.limited Logo

웹 앱의 업데이트 전략: App Store·Play Store·Tauri·Electron 배포를 어떻게 나눌까

웹 코드로 만든 앱의 업데이트를 App Store·Google Play·Tauri·Electron 배포 경로별로 나누고, 서명·버전·릴리스 채널·롤백까지 설계하는 방법을 정리했습니다.

Sik ·

업데이트는 배포팀의 마지막 일이 아니다

웹 서비스에서는 서버에 올린 순간이 배포의 끝처럼 보입니다. 앱은 다릅니다. 사용자의 기기 안에는 이미 서명된 실행 파일, 이전 데이터, 운영체제의 권한, 스토어가 관리하는 버전이 함께 있습니다. 그래서 업데이트는 “새 기능을 올리는 일”이 아니라 서로 다른 버전의 사용자를 안전하게 같은 제품으로 데려오는 일에 가깝습니다.

특히 웹 코드로 만든 앱은 혼동하기 쉽습니다. 화면이 HTML과 JavaScript로 만들어졌다는 사실이, 언제든 화면 코드를 바꿔도 된다는 뜻은 아닙니다. iOS·Android 스토어, 데스크톱 설치 프로그램, Tauri·Electron의 자동 업데이트는 각각 다른 신뢰 체계와 사용자 기대 위에서 작동합니다.

이 글은 Capacitor·Tauri·Electron 중 무엇이 더 빠른지 비교하지 않습니다. 웹 코드로 앱 만들기: Tauri·Electron·Capacitor 선택 가이드에서 고른 런타임을 실제 릴리스 운영으로 옮기는 방법을 정리합니다. 구현 비교가 필요하다면 Tauri vs Electron: SvelteKit 데스크톱 앱 선택 기준, 모바일과 데스크톱 확장의 경계가 궁금하다면 Capacitor vs Tauri: 하나의 웹 코드베이스를 모바일·데스크톱 앱으로 확장하는 선택 기준도 함께 읽으면 좋습니다.

먼저 업데이트의 대상을 세 종류로 분리한다

모든 변경을 같은 긴급도로 보내면 운영이 꼬입니다. 첫 릴리스 전에 바뀌는 것을 세 종류로 분리해두는 편이 좋습니다.

변경 대상예시기본 배포 경로반드시 남길 기록
서버 데이터·콘텐츠공지 문구, 추천 목록, 원격 설정 값서버 배포변경자, 시각, 이전 값
웹 자산과 제품 로직화면 동작, 상태 관리, API 호출 방식앱 바이너리/설치 프로그램의 새 버전앱 버전, 빌드 SHA, 호환 범위
네이티브 기능·권한카메라, 푸시, 파일 시스템, 결제 SDK스토어 또는 서명된 데스크톱 릴리스서명 키, entitlement/권한, 검증 결과

이 구분은 “어떤 업데이트가 심사를 받는가”만 가르는 표가 아닙니다. 장애가 났을 때 되돌리는 방식도 달라집니다. 원격 설정이 잘못됐다면 서버에서 끌 수 있지만, 로컬 데이터 포맷을 바꾼 앱 버전은 이미 기기 안에 들어갔을 수 있습니다. 그래서 데이터 마이그레이션은 기능 플래그보다 먼저 롤백 계획을 가져야 합니다.

제품팀은 티켓마다 아래 네 줄을 적어두면 좋습니다.

change:
  user_value: "오프라인에서도 최근 작성물을 다시 연다"
  touches_native_boundary: true
  minimum_app_version: "1.4.0"
  rollback: "읽기 전용 모드로 전환하고 1.3 데이터 포맷을 유지"

“배포 가능”이라는 상태는 코드가 합쳐졌다는 뜻이 아니라, 이 네 줄에 답이 있다는 뜻입니다.


App Store는 앱의 내용과 업데이트 경로를 함께 본다

iOS와 macOS App Store를 통해 배포한다면, 앱은 스토어가 관리하는 업데이트 경로를 따릅니다. 특히 macOS App Store 앱은 다른 업데이트 메커니즘을 사용해서는 안 된다는 규정이 있습니다. 사용자에게는 단순한 앱 업데이트처럼 보여도, 팀 입장에서는 스토어 제출물·심사 가능한 기능·버전 메타데이터가 하나의 단위예요.

Capacitor 앱에서 흔히 놓치는 지점은 “웹 화면을 감싼 앱”이 아니라는 점입니다. App Review는 앱이 웹사이트를 그대로 보여주는 수준을 넘어 지속적인 효용과 앱다운 경험을 제공하는지 봅니다. 카메라, 알림, 오프라인 작업, 공유, 기기 상태와 결합한 실제 사용 흐름이 있을수록 제품의 이유가 명확해집니다. 반대로 임시 로그인, 빈 화면, 심사자가 재현할 수 없는 서버 상태는 업데이트가 좋아도 통과 가능성을 낮춥니다.

iOS 릴리스에 필요한 질문

  • 이 버전에서 바뀌는 기능을 심사자가 로그인 없이, 또는 제공한 테스트 계정으로 확인할 수 있는가.
  • 권한을 처음 요청하는 순간에 기능의 맥락이 충분히 드러나는가.
  • 웹에서 바꾼 화면이 스토어 제출 바이너리에도 실제로 포함됐는가.
  • 긴급 수정이라도 실행 코드나 네이티브 동작을 바꾼다면, 다음 스토어 버전으로 다뤄야 하는가.
  • 사용자가 업데이트하지 않아도 서버 API가 이전 앱 버전을 일정 기간 지원하는가.

이 마지막 질문이 자주 빠집니다. 앱을 새 버전으로 제출했다고 모든 사용자가 같은 날 업데이트하지 않습니다. API의 필수 필드를 바꾸거나 인증 흐름을 끊을 때는 최신 앱이 아니라 가장 오래 지원할 앱을 기준으로 배포해야 합니다.

// 서버가 앱 버전을 읽고 점진적으로 응답을 바꾸는 단순한 예시
type ClientInfo = { platform: 'ios' | 'android' | 'desktop' | 'web'; version: string };

function supportsSavedSearchV2(client: ClientInfo) {
  return client.platform !== 'ios' || compareSemver(client.version, '1.4.0') >= 0;
}

function serializeSavedSearch(client: ClientInfo, search: SavedSearch) {
  return supportsSavedSearchV2(client)
    ? { id: search.id, query: search.query, filters: search.filters }
    : { id: search.id, query: search.query };
}

이 코드는 완벽한 버전 관리 도구가 아닙니다. 다만 서버 변경이 “앱 업데이트 후에만 가능한가”를 코드 수준에서 드러냅니다. 클라이언트 버전을 신뢰 보안 판단에 쓰면 안 되지만, 호환 가능한 응답 형태를 고르는 신호로는 유용합니다.

Google Play는 버전 코드와 서명이 이어지는 릴리스다

Android에서 업데이트가 기존 앱 위에 설치되려면 애플리케이션 ID, 서명 인증서, 버전 코드 같은 조건이 맞아야 합니다. 특히 Google Play는 더 높은 버전 코드를 기준으로 사용자의 앱을 최신으로 판단합니다. “APK 파일을 새로 만들었다”는 사실만으로 업데이트가 성립하지 않습니다.

Capacitor의 Android 프로젝트를 관리할 때는 웹 빌드 버전과 Android 버전 코드를 분리해서 이해해야 합니다. 화면 자산의 변경은 웹 빌드에서 시작하지만, 스토어에 가는 것은 Android 앱 번들입니다. 그 중간에서 cap sync와 native build가 어떤 입력을 사용했는지 CI 로그로 남겨야 합니다.

npm ci
npm run build
npx cap sync android
cd android
./gradlew bundleRelease

릴리스마다 다음 정보를 같은 곳에 기록해두세요.

product version: 1.4.0
android versionCode: 10400
web commit: 8f3c1a2
capacitor sync: completed
artifact: app-release.aab
api compatibility: 1.2.0+

Android의 인앱 업데이트는 사용자가 앱 안에 있을 때 업데이트를 안내하는 방법입니다. 유연한 흐름은 백그라운드 다운로드와 상태 관리를 허용하고, 즉시 흐름은 더 강한 전환을 요청할 수 있습니다. 어느 흐름이든 사용자 작업을 잃지 않게 설계해야 합니다. 작성 중인 긴 글이나 업로드 중인 파일이 있다면 업데이트를 지금 강제하는 게 제품적으로 옳은지 먼저 판단해야 해요.

// 네이티브 Android 레이어에서 업데이트 가능 여부를 확인하는 형태의 예시
val appUpdateManager = AppUpdateManagerFactory.create(context)

appUpdateManager.appUpdateInfo.addOnSuccessListener { info ->
    if (info.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE &&
        info.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)) {
        // UI는 "다운로드 중에도 계속 작업할 수 있음"을 설명해야 합니다.
        appUpdateManager.startUpdateFlowForResult(
            info,
            AppUpdateType.FLEXIBLE,
            activity,
            REQUEST_CODE
        )
    }
}

여기서 중요한 건 API 호출보다 메시지입니다. “업데이트가 있습니다”만 띄우면 사용자는 자기 작업이 사라질지 모릅니다. 유연한 업데이트라면 다운로드 중 계속 작업 가능한지, 설치 전에 재시작이 필요한지, 나중에 해도 되는지를 UI에 적어야 합니다. 즉시 업데이트를 쓰는 경우에는 왜 지금 필수인지 사용자가 이해할 수 있어야 합니다.

Capacitor에서 웹 자산 업데이트를 다룰 때의 기준

웹 자산은 빠르게 바꿀 수 있다는 유혹이 큽니다. 하지만 앱 안의 웹 자산 변경이 실행 코드·앱 동작·결제·권한과 얽히면, “콘텐츠 변경”처럼 취급할수록 위험해집니다. 플랫폼 정책과 심사 관점은 시점에 따라 바뀔 수 있으므로, 원격으로 바꾸는 범위는 출시 직전에 최신 스토어 정책과 법무·보안 검토를 다시 확인해야 합니다.

실무적으로는 다음 선이 안전합니다.

변경운영 원칙
문구·이미지·서버에서 내려오는 목록서버 변경과 캐시 무효화로 관리
실험의 노출 조건원격 설정으로 관리하되 기본값과 킬 스위치 보유
화면 구조·네비게이션·인증·결제 흐름앱 릴리스로 관리
네이티브 플러그인·권한·SDK앱 릴리스로 관리
DB 포맷·오프라인 동기화 규칙마이그레이션·복구 계획과 함께 앱 릴리스로 관리

이 표는 기술적 한계가 아니라 책임의 경계입니다. 사용자가 스토어에서 설치한 앱과 다른 행위를 하게 되는 변경은, 테스트·심사·고객 지원의 맥락까지 포함해 더 보수적으로 다뤄야 합니다. 빠른 수정이 필요하다면 “원격 변경을 할 수 있는가”보다 “이 변경이 실패했을 때 누구에게 어떻게 되돌릴 것인가”를 먼저 적습니다.

원격 설정도 앱과 같은 수준으로 버전 관리해야 합니다.

{
  "schemaVersion": 3,
  "minimumAppVersion": "1.3.0",
  "featureFlags": {
    "newEditor": false,
    "offlineSearch": true
  },
  "fallback": {
    "newEditor": false,
    "offlineSearch": false
  }
}

앱은 네트워크 실패, 잘못된 JSON, 알 수 없는 스키마에서 안전한 기본값으로 돌아가야 합니다. 스위치를 켜는 순간보다 끄는 순간이 더 중요합니다. 킬 스위치는 콘솔에만 있으면 안 되고, 누가 어떤 상황에서 끌지 운영 문서에 있어야 합니다.

Tauri 업데이트는 서명 키가 제품의 연속성을 만든다

Tauri v2 updater는 업데이트 파일이 신뢰할 수 있는 출처에서 왔는지 검증하기 위해 서명을 사용합니다. 공개 키는 앱 설정에 들어가고, 개인 키는 릴리스 산출물에 서명하는 데 쓰입니다. 개인 키를 잃으면 이미 설치된 앱에 새 업데이트를 발행할 수 없다는 점이 특히 중요합니다. 이건 단순한 CI 비밀값이 아니라, 설치된 제품과 다음 릴리스를 연결하는 열쇠입니다.

초기 설정에서는 키 생성, CI 보관, 접근 권한, 사고 대응을 하나의 체크리스트로 만듭니다.

# 개발자가 개인 키를 생성하는 예시. 실제 경로·보관 정책은 팀의 비밀관리 체계를 따릅니다.
npm run tauri signer generate -- -w ./secrets/my-app.key

# CI에서는 저장소 파일이 아니라 보호된 secret을 환경 변수로 주입합니다.
export TAURI_SIGNING_PRIVATE_KEY="$TAURI_SIGNING_PRIVATE_KEY"
npm run tauri build
{
  "bundle": {
    "createUpdaterArtifacts": true
  },
  "plugins": {
    "updater": {
      "pubkey": "앱에 포함할 공개 키",
      "endpoints": [
        "https://updates.example.com/{{target}}/{{arch}}/{{current_version}}"
      ]
    }
  }
}

예시 URL과 키 문자열은 그대로 복사해 쓰는 설정이 아닙니다. 하지만 구조는 분명합니다. 앱은 HTTPS 엔드포인트에서 새 버전 정보를 받고, 서명으로 산출물을 검증합니다. 업데이트 서버를 정적 JSON으로 운영하든 동적 응답으로 운영하든, 서버 접근 제어와 배포 권한을 앱 배포 권한과 같은 수준으로 관리해야 합니다.

업데이트 응답에는 버전, 다운로드 위치, 서명, 변경 사항이 들어갑니다. 팀이 반드시 테스트해야 할 것은 “새 버전이 있다”는 정상 경로만이 아닙니다.

  • 잘못된 서명은 설치되지 않는가.
  • 이전 버전으로 되돌려야 할 때 어떤 정책을 쓰는가.
  • 업데이트 메타데이터가 잘못 배포됐을 때 안전한 응답으로 막을 수 있는가.
  • 사용자가 저장하지 않은 작업이 있을 때, 다운로드와 재시작의 순서는 무엇인가.
  • 플랫폼별 산출물이 모두 같은 제품 호환 범위를 갖는가.

Electron은 업데이트 서버와 보안 경계를 함께 운영한다

Electron의 autoUpdater는 업데이트 피드를 확인하고 다운로드·설치를 지원합니다. 다만 플랫폼과 패키징 방식에 따라 동작이 다르며, 기본 내장 지원 범위와 Linux 배포 방식도 구분해서 봐야 합니다. “패키징 도구가 알아서 할 것”이라고 두기에는 업데이트는 사용자 신뢰와 직결되는 영역입니다.

가장 작은 형태의 업데이트 확인 코드는 아래처럼 보일 수 있습니다.

// electron/main.ts
import { app, autoUpdater, BrowserWindow } from 'electron';

autoUpdater.setFeedURL({
  url: 'https://updates.example.com/feed'
});

autoUpdater.on('update-available', () => {
  BrowserWindow.getAllWindows()[0]?.webContents.send(
    'update:status',
    { state: 'downloading' }
  );
});

autoUpdater.on('update-downloaded', (_event, notes, name) => {
  BrowserWindow.getAllWindows()[0]?.webContents.send(
    'update:status',
    { state: 'ready', notes, name }
  );
});

// 사용자가 "재시작하고 설치"를 명시적으로 선택했을 때만 호출합니다.
function installDownloadedUpdate() {
  autoUpdater.quitAndInstall();
}

여기서 quitAndInstall()은 기술적으로 한 줄이지만, 제품적으로는 한 화면이 필요합니다. 사용자가 무엇을 잃는지, 언제 다시 열리는지, 업데이트를 미뤄도 되는지 설명해야 해요. 자동 업데이트는 조용할수록 좋다는 생각도 위험합니다. 보안 패치처럼 긴급한 경우와, 기능 변경처럼 사용자의 맥락을 기다려야 하는 경우를 구분해야 합니다.

Electron 앱에서는 업데이트 자체도 renderer에 임의 권한을 주는 핑계가 되어서는 안 됩니다. 업데이트 상태를 전달하는 IPC는 읽기 전용 이벤트로 제한하고, 다운로드 URL이나 파일 시스템을 화면이 마음대로 바꾸지 못하게 해야 합니다. preload와 main 프로세스의 좁은 경계는 기능 개발뿐 아니라 업데이트 운영에도 그대로 적용됩니다.

채널을 나누면 긴급 배포와 무모한 배포를 구분할 수 있다

릴리스 채널은 대기업만 필요한 장치가 아닙니다. 두 명 팀이어도 stablepreview만 나누면 배포 사고의 반경을 줄일 수 있습니다.

채널대상허용할 위험관찰할 것
internal팀·테스터데이터 삭제가 아닌 대부분의 기능 오류설치·로그인·마이그레이션
preview자발적 얼리어답터UI·성능·호환 문제크래시, 이탈, 지원 문의
stable전체 사용자낮은 위험만 허용오류율, 업데이트 완료율, 핵심 행동
hotfix영향을 받은 사용자명확한 회귀 수정복구 성공 여부, 재발 방지

여기서 채널은 단순한 버전 문자열이 아닙니다. 데이터베이스 마이그레이션을 preview에서 먼저 통과시키고, stable 서버가 이전 앱도 읽을 수 있는 기간을 정하고, 롤백할 때 서버 플래그·업데이트 피드·스토어 배포 중 무엇을 끌지 미리 결정하는 운영 장치입니다.

버전 번호도 사용자용 이름과 기기용 순서를 분리해둡니다.

사용자에게 보이는 버전: 1.4.0
Android versionCode:      10400
데스크톱 업데이트 채널:    stable
서버 API 최소 호환:        1.2.0
데이터 스키마:             7

이 다섯 값이 릴리스 노트, CI 산출물, 오류 보고에 함께 찍히면 “어느 버전에서만 발생하는가”를 훨씬 빨리 좁힐 수 있습니다. 특히 웹과 네이티브가 섞인 앱은 화면 버전만 보고 원인을 판단하기 어렵습니다.

자동화는 배포 버튼이 아니라 증거를 남기는 흐름이다

CI가 build 명령을 실행하고 산출물을 업로드한다고 해서 릴리스 파이프라인이 완성되는 것은 아닙니다. 나중에 장애가 났을 때 팀은 “어떤 소스가 어느 키로 서명되어 누구에게 배포됐는가”를 알아야 합니다. 즉 자동화의 산출물은 .ipa, .aab, .dmg, .exe만이 아니라 배포 증거입니다.

최소한 다음 정보를 릴리스마다 하나의 JSON 또는 사람이 읽을 수 있는 노트로 남깁니다.

{
  "release": "1.4.0",
  "channel": "preview",
  "commit": "8f3c1a2",
  "builtAt": "2026-08-22T12:00:00Z",
  "artifacts": [
    { "platform": "android", "versionCode": 10400, "file": "app-release.aab" },
    { "platform": "macos", "file": "my-app.app.tar.gz" },
    { "platform": "windows", "file": "my-app-setup.exe" }
  ],
  "minimumApiCompatibility": "1.2.0",
  "migration": "schema-7"
}

값 자체가 중요한 이유는 서로를 연결하기 때문입니다. 고객 지원에서 사용자가 1.4.0이라고 말하면, 어떤 커밋과 데이터 스키마인지 찾을 수 있어야 합니다. 업데이트 피드의 잘못된 메타데이터가 문제라면, 어느 채널에 어느 산출물이 노출됐는지 즉시 멈출 수 있어야 합니다. 스토어가 처리 중인 배포물과 직접 다운로드하는 데스크톱 설치 프로그램이 같은 API 변경을 전제하는지도 확인할 수 있습니다.

자동화에는 “발행”과 “승인”을 분리하는 장치도 필요합니다. 모든 PR이 stable 업데이트 피드를 덮어쓰게 하지 말고, 테스트 산출물은 internal 채널에만 올리고, stable은 변경 사항·마이그레이션·롤백 계획을 검토한 태그에서만 생성하는 식입니다. 개인 키와 스토어 인증서에 접근하는 작업은 더 좁혀야 합니다. 빌드 로그에 비밀값이 찍히지 않는지, 키가 로컬 디렉터리에 남지 않는지도 정기적으로 확인합니다.

# CI 의사 코드: 실제 공급자 문법에 맞게 구현한다.
release:
  needs: [test, build]
  if: startsWith(git.ref, 'refs/tags/v')
  steps:
    - verify: version and migration plan
    - sign: inject protected credentials only for this job
    - publish: upload to preview or stable by approved channel
    - record: attach release manifest and checksums

이 흐름은 느려 보이지만, 긴급 수정에서 가장 빠릅니다. 누가 피드를 바꿀 수 있는지, 어떤 버전이 나갔는지, 안전하게 되돌릴 후보가 무엇인지 이미 정리되어 있기 때문입니다. 속도는 승인 단계를 없애서 생기지 않습니다. 불확실성을 줄여서 생깁니다.

롤백은 이전 파일을 다시 올리는 일보다 넓다

새 버전을 끄는 것만으로는 복구가 끝나지 않습니다. 이미 새 앱이 데이터를 새 형식으로 저장했거나, 서버가 이전 필드를 제거했거나, 기능 플래그가 캐시됐을 수 있기 때문입니다. 배포 전에 아래 순서를 실제로 한 번 연습해야 합니다.

1. 영향 범위를 확인한다: 어떤 플랫폼·버전·계정이 문제인가
2. 확산을 멈춘다: 스토어 단계 배포 중지, 업데이트 피드 보류, 플래그 OFF
3. 안전 모드로 전환한다: 쓰기 기능을 막고 읽기·내보내기를 우선한다
4. 데이터 호환을 확보한다: 이전 앱이 읽을 응답과 포맷을 되살린다
5. 복구 버전을 낸다: 새 버전의 목적과 복구 조건을 릴리스 노트에 적는다
6. 사후 검토한다: 어떤 신호가 더 일찍 이 문제를 알려줬어야 하는가

가장 좋은 롤백은 할 필요가 없는 롤백이 아닙니다. 문제가 나도 사용자의 작업을 잃지 않고, 팀이 다음 행동을 알고, 같은 실패를 채널에서 더 일찍 잡는 구조입니다. 이 관점에서는 앱 업데이트와 데이터 백업, 오류 관찰, 고객 지원 문구가 하나의 시스템입니다.

배포 체크리스트는 기획·디자인·개발이 함께 닫아야 한다

마지막으로, 아래 체크리스트를 릴리스 PR이나 노션 문서에 복사해 사용해도 좋습니다.

제품과 디자인

  • 이번 변경이 사용자의 흐름에서 무엇을 바꾸는지 한 문장으로 썼는가
  • 업데이트 다운로드·재시작·실패·권한 거절 상태에 화면과 문구가 있는가
  • 변경 사항이 오래된 앱 사용자에게도 이해 가능한가
  • 스토어 심사자가 핵심 기능을 재현할 수 있는가

개발과 보안

  • 웹 빌드 SHA, 네이티브 버전, 산출물 해시를 기록했는가
  • 앱 ID·서명 키·버전 코드가 릴리스 정책과 맞는가
  • Tauri 업데이트 개인 키 또는 Electron 업데이트 피드의 권한을 최소 인원으로 제한했는가
  • IPC·command·플러그인 권한이 기능에 필요한 범위만 열려 있는가
  • 이전 앱과 서버 API·데이터 포맷의 호환 기간을 정했는가

운영과 지원

  • internal/preview/stable 중 어디에 내보내는가
  • 실패하면 어떤 스위치, 피드, 스토어 배포를 먼저 멈출지 적었는가
  • 저장되지 않은 작업과 데이터 마이그레이션의 복구 경로를 테스트했는가
  • 사용자 문의에 답할 현재 버전·재시작·복구 안내가 준비됐는가

업데이트가 매끄러운 앱은 새 기능을 가장 자주 내는 앱이 아니라, 이전 버전을 쓰는 사람도 잊지 않는 앱입니다. Capacitor에서는 웹과 스토어 바이너리의 경계를, Tauri에서는 서명 키와 업데이트 산출물의 연속성을, Electron에서는 패키징·업데이트 피드·IPC 보안을 함께 설계해야 합니다. 이 세 축을 출시 전에 분리해두면, 다음 릴리스는 더 빠르면서도 덜 위험해집니다.

공식 문서

최신 글