금요일 오후, 프로덕션 수정 사항이 준비됐지만 Capacitor 앱의 live update 메커니즘 때문에 배포가 추측 게임으로 변해 버렸습니다. 한 기기는 새 번들을 보고하지만, 다른 기기는 이전 에셋 세트를 그대로 유지하고, 롤백 경로는 어떤 버전이 먼저 도착했는지에 따라 다르게 동작합니다. 설치할 때 업데이터는 작은 통합처럼 보였습니다. 하지만 이를 교체하는 일은 differential을 설정하는 것에 더 가깝습니다. 기존 어셈블리는 깔끔하게 제거해야 하고, 새 어셈블리는 베이스와 일치해야 하며, 모든 측정값은 재현 가능해야 하기 때문입니다.
그 비교가 중요한 이유는 업데이트 시스템이 단순한 다운로드 버튼이 아니기 때문입니다. 업데이트 시스템은 버전을 추적하고, 번들을 재구성하며, 네이티브와 JavaScript 경계를 적용하고, 패키지가 불완전하거나 호환되지 않을 때 어떻게 처리할지 결정합니다. 실질적인 질문은 differential 전송을 교체하는 방법만이 아닙니다. 마이그레이션 중 두 번째 실패 모드를 만들지 않고 업데이터를 교체하는 방법입니다.
목차
새로운 Updater가 필요한 이유
첫 번째 경고는 보통 운영상의 미스터리로 다가옵니다. 릴리스는 로컬 테스트를 통과했고 서버에는 업로드 성공으로 표시되는데, 고객 지원팀에는 사용자가 여전히 이전 화면을 본다는 보고가 계속 들어옵니다. 엔지니어링 팀이 bundle identifier, native build 버전, channel 할당, 디바이스 로그를 대조해 보면, updater가 로컬에 저장된 것과 다른 베이스로 빌드된 패키지를 수락했다는 사실을 발견합니다.
이는 기어 설정이 잘못된 차동 장치(differential)와 비슷합니다. 부품은 호환되는 것처럼 보이지만, 실제 구동 시 소음이 발생하죠. Capacitor 앱에서는 이에 해당하는 '소음'이 에셋 상태 불일치, 추출 실패, 무한 재시도, 또는 기대한 bundle의 일부만 복구하는 롤백으로 나타납니다.
유용한 진단은 가정이 아닌 증상에서 시작됩니다:
- 버전 불일치: 설치된 native shell, 다운로드된 bundle, updater 메타데이터가 의도한 호환성 범위에 대해 서로 일치하는지 확인하세요.
- 불명확한 롤백: 업데이트 실패 시 이전 bundle 전체로 되돌리는지, 아니면 잔여 파일이 남는지 테스트하세요.
- 관측성 부족: 영향받은 디바이스, 패키지 버전, channel, 실패 단계, 마지막 성공 실행 시점을 식별할 수 있는지 확인하세요.
- 제어되지 않는 타겟팅: 프로덕션 사용자가 베타, 스테이징 또는 특정 고객용 스트림을 대상으로 한 아티팩트를 받고 있는지 확인하세요.
- 비효율적인 전송: 웹 에셋 몇 개만 변경됐는데도 시스템이 전체 아카이브를 전송하는지 측정하세요. 이 설계 트레이드오프의 배경은 다음에서 다룹니다. delta 업데이트가 페이로드 크기를 줄이는 방법.

교체 전 진단하기
모든 업데이트 실패를 업데이터 자체에 결함이 있다는 증거로 보지 마세요. 잘못된 빌드 아티팩트, 부정확한 채널 규칙, 깨진 서명 시크릿, 네이티브 호환성 변경도 동일한 증상을 만들 수 있습니다. 패키지 ID, 베이스 버전, 디바이스 상태, 업데이트 적용 후 앱이 정상적으로 실행됐는지 여부를 포함해 실패 경로를 먼저 기록하세요.
메커니즘이 신뢰할 수 있는 답변이나 안전한 복구를 제공하지 못하면 교체가 정당화됩니다. 팀이 어떤 디바이스가 번들을 받았는지 입증할 수 없거나, 잘못된 릴리스를 빠르게 무효화할 수 없거나, 베이스가 없을 때 전체 패키지로 폴백된다는 것을 보장할 수 없다면, 설정 패치는 다음 인시던트를 미룰 뿐입니다.
이 엔지니어링 원칙은 기어 어셈블리와 같습니다. 하우징을 제거하기 전에 결함이 있는 컴포넌트를 먼저 확인하세요. 전체 updater 마이그레이션은 초기 비용이 더 많이 들지만, 불확실한 설치 위에 또 다른 조정을 덧씌우는 대신 깔끔한 프로토콜, 명확한 버전 규칙, 그리고 테스트 가능한 롤백 경계를 제공합니다.
기존 메커니즘 안전하게 제거하기
제거 작업을 통제된 격리 절차로 취급하세요. 목표는 단순히 패키지를 삭제하고 다음 빌드에서 무엇이 남는지 확인하는 것이 아닙니다. 두 개의 업데이트 메커니즘이 동일한 웹 디렉토리, startup hook, 설정 키 또는 delivery endpoint를 두고 충돌하는 것을 방지하는 것이 목표입니다.
먼저 마이그레이션 브랜치를 고정하고 현재 동작을 기록하는 것부터 시작하세요. 활성화된 플러그인 버전, native 설정, 환경 변수, CI 명령, channel 이름, 그리고 다운로드된 bundle이 저장되는 위치를 저장해 두어야 합니다. 첫 번째 클린 빌드를 실행할 때 의도적인 변경 사항과 실수로 누락된 사항을 구분하려면 이 인벤토리가 반드시 필요합니다.
깨끗한 베이스라인 구축하기
임시 브랜치나 작업 복사본을 만든 다음, 의존성 트리와 Capacitor 설정을 검토하세요. “update”를 일반적으로 검색하는 대신 실제 패키지 이름으로 레거시 updater 패키지를 식별하세요. 다음을 검토하세요: package.jsonlockfiles, capacitor.config.tsiOS 프로젝트 설정, Android 리소스, 그리고 download, sync, install 또는 rollback 메서드를 호출하는 startup code.
해당 리포지토리에서 사용하는 패키지 매니저로 의존성을 제거하세요. 예를 들어 npm의 경우:
npm uninstall <legacy-updater-package>
npx cap sync
pnpm이나 Yarn을 프로젝트 표준으로 쓴다면 그에 해당하는 명령어를 사용하세요. 플레이스홀더는 의도된 것입니다. 마이그레이션 가이드가 패키지 이름을 임의로 만들어 잘못된 의존성에 파괴적인 명령을 권장해서는 안 됩니다.
그다음 저장소에서 이전 플러그인 식별자, API 호출, 엔드포인트 이름, 설정 키, 이벤트 리스너를 검색하세요. 애플리케이션 레이어에서 더 이상 필요 없는 import와 초기화 code를 삭제하세요. 사용되지 않는 래퍼도 제거하세요. 제거된 업데이터를 호출하는 로컬 추상화를 남겨두면 빌드는 성공해도 시작 시 동작이 실패할 수 있어 나중에 디버깅하기가 훨씬 어려워집니다.
Native 잔여물 정리하기
의존성 제거 후 iOS와 Android 프로젝트를 열어 패키지 매니저가 정리하지 못한 네이티브 참조를 제거하세요. 여기에는 수동으로 추가한 pods, Gradle 의존성, 매니페스트 항목, 리소스 값, URL 스킴, 리시버 선언, 초기화 훅이 포함됩니다. 이름이 관련 있어 보인다고 해서 파일을 삭제하지 마세요. 각 파일이 레거시 업데이터에만 속하는지, 공유 Capacitor 설정이 해당 파일에 의존하지 않는지 확인하세요.
대체 패키지를 설치하기 전에 네이티브 의존성 새로고침을 실행하고 두 플랫폼을 모두 컴파일하세요. 이렇게 하면 정리 실패와 통합 실패를 구분할 수 있습니다. 클린 네이티브 빌드는 마이그레이션이 제대로 동작함을 증명하지 않지만, 클린 빌드가 실패하면 이전 메커니즘이 완전히 무력화되지 않았음을 증명합니다.
스크립트에도 같은 원칙이 적용됩니다. CI에서 오래된 upload 작업, 엔드포인트 변수, 서명 단계, artifact 복사 명령, 릴리스 알림을 제거하세요. 프로덕션 워크플로우뿐 아니라 pull-request 워크플로우도 확인하세요. 방치된 preview job은 테스터가 받기 전까지는 문제없어 보이는 채널에 호환되지 않는 패키지를 게시할 수 있습니다.
실무 원칙: 마이그레이션할 때 기존 updater를 변경하고 새 updater를 같은 커밋에 추가하지 마세요. 제거 작업을 관찰 가능하게 만들고 빌드한 뒤, 그다음에야 교체본을 도입하세요.
다음 단계로 넘어가기 전에, 정리된 앱을 테스트 기기에 설치해 더 이상 기존 서비스에 연결을 시도하지 않는지 확인하세요. 필요한 경우 앱 데이터를 삭제하고, startup logs를 점검하며, 앱이 bundled web assets를 정상적으로 로드하는지 확인하세요. 이 기준선이 다음 단계의 대조 샘플입니다. 다양한 접근 방식을 비교하려면 다음을 참고하세요. Capacitor, Ionic, Cordova를 위한 CodePush 대안.
새 플랫폼 설치 및 설정하기
교체할 updater는 불투명한 라이브러리가 아니라 명시적인 프로토콜로 프로젝트에 도입되어야 합니다. bundle에 무엇이 포함되는지, 어떤 native 버전에 적용할 수 있는지, 채널이 대상 그룹에 어떻게 매핑되는지, 저장된 base를 사용할 수 없을 때 클라이언트가 어떻게 동작하는지 정의하세요. 설치 전에 이 규칙들이 명확하지 않으면, 마이그레이션이 happy-path 테스트는 통과하더라도 프로덕션에서는 안전하지 않은 채로 남습니다.
클라이언트 신중하게 추가하기
저장소의 package manager로 새 updater 패키지를 설치한 다음, native 프로젝트를 동기화하세요:
npm install <new-updater-package>
npx cap sync
실제 프로젝트에서는 벤더가 문서에 명시한 패키지 이름과 설정 키를 사용하세요. 의존성 변경은 관련 없는 Capacitor 업그레이드와 분리해, 네이티브 빌드가 실패할 때 원인 범위를 좁힐 수 있게 하세요.
앱 시작 시 예측 가능한 시점에 updater를 초기화하세요. updater는 현재 네이티브 빌드, 활성 채널, 로컬 web bundle 버전, 업데이트 확인을 위한 서버 정책을 알고 있어야 합니다. 여러 화면에 업데이트 호출을 흩어 놓지 마세요. 단일 서비스가 확인, 다운로드, 검증, 설치 결정, 오류 보고를 모두 담당해야 합니다.
클라이언트는 JavaScript bundle 업데이트와 네이티브 앱 업데이트를 구분해야 합니다. web bundle은 네이티브 플러그인을 추가하거나 플랫폼 권한을 변경할 수 없고, 새로운 네이티브 브리지가 있다고 안전하게 가정할 수도 없습니다. 릴리스 메타데이터에 호환성을 명시하고, updater가 지원되는 네이티브 범위 밖의 패키지를 거부하게 하세요.
배포 전 채널을 먼저 생성하세요
내부 테스트, staging, production을 위한 별도 스트림을 설정하세요. 채널은 단순한 라벨이 아닙니다. 어떤 대상이 패키지를 받을 수 있는지 결정하는 가드레일이며, 앱 시작, 네비게이션, 인증, 오프라인 동작, 네이티브 브리지 호출을 검증할 통제된 환경을 제공합니다.
승격 경로를 문서화하세요:
- 개발 배포 없이 로컬에서 빌드합니다.
- 내부 엔지니어링 및 QA 기기용 서명된 아티팩트를 받습니다.
- 무대 구성 프로덕션과 유사한 서비스와 릴리스 구성을 검증합니다.
- 프로덕션 이전 검사를 통과한 아티팩트만 허용합니다.
가능하면 채널 할당은 애플리케이션 번들 외부에 두고, 하드코딩된 테스트 값 대신 관리형 환경 설정을 사용하세요. 프로덕션 빌드가 실수로 베타 스트림을 가리키는 것은 사용자 오류가 아니라 릴리스 프로세스의 실패입니다.
Bundle 경계 보호하기
서비스는 나중에 전송 최적화로 변경된 파일만 보내더라도 완전하고 결정론적인 웹 번들을 게시해야 합니다. 아티팩트는 깨끗한 워크스페이스에서 생성하고, 의도적으로 배포할 이유가 없는 한 소스 맵이나 개발 파일은 제외하고, 결과 패키지의 콘텐츠 식별자를 기록하세요.
설치 전에 무결성 검증을 요구하세요. 클라이언트는 다운로드한 데이터를 검증하고, 예상 디렉터리를 재구성한 뒤 원자적으로 전환해야 합니다. 검증이 실패하면 현재 작동 중인 번들을 유지하고, 진단할 수 있을 만큼 충분한 컨텍스트와 함께 실패를 보고하세요.
안전한 업데이트 타이밍과 네이티브 제한 사항을 포함한 더 넓은 OTA 모델은 다음에서 설명합니다 OTA 업데이트 수행 방법.

설정은 저장소가 아니라 플랫폼의 시크릿 매니저나 보호된 CI 변수에 저장하세요. 빌드 작업의 읽기 접근 권한과 릴리스 삭제나 프로덕션 타겟팅 변경이 가능한 관리 권한을 분리하세요. 업데이터는 전체 패키지 폴백도 제공해야 합니다. 디바이스에 필요한 베이스 버전이 없으면 시스템은 앵커되지 않은 패치를 적용하려 시도해서는 안 됩니다.
Build 및 CI Pipeline 조정하기
수동 배포는 실험에는 괜찮지만 릴리스 시스템으로는 위험합니다. 신뢰할 수 있는 파이프라인은 소스 리비전을 알려진 아티팩트로 만들고, 그 아티팩트에 검사를 실행하고, 서명해 의도한 채널에 배포한 뒤 결과를 기록합니다. 개발자 워크스테이션에서 실행하든 호스팅 러너에서 실행하든 같은 입력이면 같은 릴리스 결정이 나와야 합니다.
Clean 환경에서 artifact 빌드하기
재현 가능한 순서로 시작하세요. 고정된 의존성을 설치하고, 린팅과 테스트를 실행하고, 웹 레이어를 빌드한 뒤 결과 파일로 Capacitor 출력을 생성합니다. 개발자의 작업 디렉터리를 기준으로 차이를 계산하지 마세요. CI가 생성한 버전이 지정된 불변 번들 두 개를 비교하세요.
실용적인 파이프라인은 관심사를 분리합니다:
- 검증: 업로드 전에 단위 테스트, 타입 체크, 린팅, 프로덕션 웹 빌드를 실행합니다.
- 호환성: 번들에 필요한 네이티브 기능이 대상 앱 빌드와 일치하는지 확인합니다.
- 패키징: 결정적 아카이브 또는 파일 매니페스트를 생성하고 임시 파일은 제외합니다.
- 서명: CI 환경이 보유한 보호된 자격 증명으로 릴리스에 서명합니다.
- 게시: 아티팩트를 브랜치, 태그 또는 릴리스 승인에 따라 선택된 채널로 전송합니다.
- 본인 확인: 전달 서비스를 조회하여 게시된 버전, 체크섬, 채널이 일치하는지 확인합니다.

버전 식별을 명확히 하기
소스 리비전, 네이티브 호환성, 게시된 번들을 연결하는 릴리스 식별자를 사용하세요. 문제 해결 시 'latest'처럼 변경될 수 있는 라벨에 의존하지 마세요. 업데이터는 설치된 버전과 후보 버전을 정확히 보고해야 하며, 릴리스 시스템은 의도적으로 롤백하거나 재게시할 수 있도록 충분한 이력을 유지해야 합니다.
유용한 매니페스트는 다음을 기록합니다:
| 필드 | 왜 중요한가 |
|---|---|
| 번들 버전 | 설치된 웹 자산과 후보 웹 자산 식별 |
| Native 호환성 | 웹 번들이 사용할 수 없는 브릿지 code를 전제로 동작하는 것을 방지 |
| 채널 | 대상 사용자 정의 |
| Base 버전 | Differential 재구성의 기준점 |
| 무결성 값 | 데이터 손상 또는 변조 감지 |
| 릴리스 상태 | 승격, 일시 중지, 롤백 결정 지원 |
Base 버전은 Differential 배포 시 특히 중요합니다. RFC 3229는 일반 모델을 설명합니다. 클라이언트가 저장된 표현을 식별하고 차분 알고리즘을 요청하면, 전체 리소스 대신 변경 사항을 나타내는 응답을 받습니다. 애플리케이션 레벨에서도 기기가 알고 있는 Base 버전이 유효할 때만 패치를 계산하고, 그렇지 않으면 전체 번들을 전송해야 합니다.
배포 경로 보안 강화
CI runner를 권한 있는 릴리스 머신으로 취급하세요. 프로덕션 배포를 트리거할 수 있는 사람을 제한하고, 서명 자료가 로그에 남지 않게 하며, 플랫폼의 secret 제어 기능으로 자격 증명을 교체하세요. 또한 신뢰할 수 없는 pull request가 릴리스 secret에 접근하지 못하게 해야 합니다. DevArmor CI/CD 보안 관행은 권한, secret, 파이프라인 경계를 강화하는 데 유용한 컨텍스트를 제공합니다.
프로덕션 배포 전에 dry-run 또는 staging 배포 단계를 추가하세요. 다운로드한 artifact를 깨끗한 시뮬레이터나 기기에서 테스트하고, 첫 실행, 재실행, 다운로드 중단, 서명 거부, 롤백 동작을 확인하세요. 가능한 경우 릴리스 작업을 멱등하게 유지하세요. 실패한 작업을 다시 실행해도 식별자가 겹치는 모호한 패키지 묶음이 생겨서는 안 됩니다.
구체적인 구현 패턴은 다음을 참고하세요: Capacitor CI/CD 파이프라인 설정 가이드. 목표는 단순히 upload를 자동화하는 것이 아닙니다. 소스 커밋부터 기기 상태까지 모든 릴리스를 감사 가능하게 만드는 것입니다.
배포 및 성공 모니터링
롤아웃은 업데이터 설계가 실제 부하에서 검증되는 단계입니다. 첫날, 스테이징에서는 깔끔해 보이던 패키지가 구버전 베이스, 중단된 다운로드, 부족한 저장 공간, QA 매트릭스가 커버하지 못한 앱 버전을 가진 디바이스를 만납니다. 퍼센트 기반 타겟팅은 다이얼로 보고, 실제 안전장치는 승격을 중단하고 사용자를 검증된 번들로 되돌리는 능력으로 두세요. 새 디퍼렌셜 기어셋을 세팅하듯 롤아웃하세요. 통제된 피팅 체크로 시작해 문제를 듣고, 맞물림이 안정적임이 확인된 뒤에만 범위를 넓히세요.
전체 업데이트 경로 모니터링하기
전송만 보지 말고 전체 경로를 보세요. 디바이스가 모든 바이트를 다운로드하고도 서명 검증, 추출, 활성화 또는 교체 후 첫 실행에서 실패할 수 있습니다. 각 단계를 번들 버전, 네이티브 앱 버전, 채널, 플랫폼, 개인정보를 고려한 디바이스 식별자, 팀이 대응할 수 있는 오류 카테고리와 함께 로그로 남기세요.
최소 시그널은 몇 가지 운영 질문에 답할 수 있어야 합니다:
- 디바이스가 업데이트 후보를 발견했는가?
- 전체 번들을 받았는가, 아니면 differential 패치를 받았는가?
- 무결성 검증을 통과했는가?
- 활성화가 완료되었는가?
- 그 후 앱이 성공적으로 실행되었는가?
- 다음 정상 세션에서도 클라이언트가 정상 상태였는가?
이 이벤트 트레일은 팀마다 필요한 정보를 제공합니다. Support는 한 기기에서 무슨 일이 있었는지 설명하려면 기기별 타임라인이 필요하고, Release 담당자는 전체 플릿의 채택률, 실패율, 롤백 추세를 파악해야 합니다. 실용적인 모델은 Capacitor 앱에서 OTA 업데이트 성공 추적하기 이러한 단계에 명확하게 매핑되어 대시보드 설계를 훨씬 쉽게 만들어 줍니다.

롤백을 릴리스 기능으로 활용하기
게시 전에 롤백 조건을 설정하세요. 좋은 트리거에는 런칭 실패 증가, 반복되는 무결성 오류, 후보 버전에 갇힌 디바이스 증가, 특정 플랫폼에 국한된 크래시 패턴 등이 포함됩니다. 디바이스에 마지막 알려진 정상 번들을 유지하고, 앱 스토어 릴리스를 기다리지 않고 되돌릴 수 있도록 전환을 가역적으로 만드세요.
롤백에도 증거 보존이 필요합니다. 팀이 문제를 재현하고, 영향을 받는 호환성 범위를 식별하며, 이전 할당을 복원할지 수정된 아티팩트를 배포할지 결정할 수 있도록 실패한 패키지 메타데이터와 로그를 유지하세요. 사용자에게는 증상을 해소하지만 흔적을 지우는 롤백은 다음 릴리스에서 동일한 실패가 다시 발생하는 경우가 많습니다.
차등 전송의 기반이 되는 전송 모델은 오랫동안 존재해 왔습니다. HTTP 델타 인코딩은 2002년 1월 인터넷 엔지니어링 태스크 포스(IETF)가 발표한 RFC 3229를 통해 공식 웹 표준이 되었습니다., 그리고 RFC 3229 사양 실용적인 이유 하나 때문에 여전히 유용합니다. 클라이언트가 유효한 기본 버전을 가지고 있고, 결과를 재구성하는 비용이 전체 리소스를 전송하는 것보다 적을 때만 델타가 도움이 됩니다.
그래서 애플리케이션을 인식하는 업데이터가 일반적인 HTTP 델타 동작에 의존하는 것보다 더 잘 작동하는 경향이 있습니다. 서비스는 베이스 번들을 알고, 클라이언트는 재구성된 출력을 검증할 수 있으며, 업데이터는 맞지 않을 때 전체 패키지로 폴백할 수 있습니다. 엔지니어링 선택은 전달 프로토콜이 호환성, 무결성, 롤백, 관측성을 명시적으로 다루는지에 달려 있고, 그 답이 델타 전달이 실제로 도움이 되는지를 결정합니다.