빌드 로그 • Koal & KoalStudio

WineDuck의 세 개 릴리스 레인

WineDuck의 화면 코드는 웹과 앱에서 함께 쓰지만 라이브 웹, 셀프호스트 OTA, App Store 바이너리는 서로 다른 산출물입니다. 변경 분류와 콜드 스타트 적용, 채널별 버전 규칙을 정리합니다.

WineDuck 웹에서 문구 하나를 고쳤어요. 공개 주소를 열면 새 문구가 보이는데 아이폰 앱에서는 이전 문구가 남을 수 있습니다. 웹과 앱이 같은 화면 코드를 쓴다는 사실만 보면 이상하지만, 사용자가 받는 산출물을 나눠 보면 자연스러운 결과예요.

WineDuck 라이브 웹, 설치 앱이 내려받는 OTA 번들, App Store에서 배포하는 iOS 바이너리는 서로 다른 릴리스 레인이에요. 공개 주소에 역사적인 AsterDuck 식별자가 남아 있어도 현재 사용자-facing 제품명은 WineDuck입니다. 소스 일부를 공유해도 만들어지는 파일, 전달 채널, 적용 시점이 다릅니다.

이 글의 질문은 “어떤 배포 명령을 실행할까”가 아니에요. 무엇이 바뀌었고, 그 변경을 담을 수 있는 산출물이 무엇이며, 사용자가 언제 그 산출물을 받는지를 먼저 분류합니다.

하나의 소스가 세 산출물로 갈라져요

WineDuck의 화면은 웹 기술로 만들어요. 같은 TypeScript, CSS, 문구가 브라우저에서도 보이고 Capacitor로 감싼 iOS 앱 안에서도 보여요. 그러나 빌드와 배포가 시작되면 세 갈래로 나뉩니다.

라이브 웹 산출물은 서버에 배포한 웹 파일이에요. 사용자가 공개 주소를 새로 열면 브라우저가 현재 배포된 자산을 받아요. 화면별 지연 로딩 파일과 캐시까지 포함해 라이브 서버의 상태로 존재합니다.

OTA 산출물은 설치 앱용으로 빌드하고 압축한 웹 번들이에요. 앱이 업데이트 서버에서 내려받아 현재 번들 대신 실행합니다. 이 경로는 웹 파일을 바꿀 수 있지만 앱에 이미 들어 있지 않은 네이티브 기능을 새로 만들 수는 없어요.

App Store 산출물은 서명된 iOS 바이너리예요. 네이티브 플러그인, 권한, 앱 설정, 아이콘과 함께 그 시점의 웹 번들을 내장합니다. 심사와 출시, 사용자의 앱 업데이트를 거쳐 기기에 도착해요.

최신 소스라는 상태만으로는 어느 사용자도 업데이트되지 않아요. 어떤 산출물을 만들었는지, 어느 채널에 보냈는지, 그 채널의 적용 시점이 언제인지까지 이어져야 릴리스가 됩니다.

라이브 웹: 브라우저가 받는 현재 자산

웹 화면과 웹 로직만 바뀌었다면 라이브 웹 레인을 먼저 생각할 수 있어요. 검증한 소스에서 웹 산출물을 만들고 서버에 반영한 뒤, 바뀐 경로를 공개 주소에서 직접 확인합니다.

대표 JavaScript 파일 하나의 이름만 비교해서는 부족해요. 수정한 화면이 지연 로딩 파일로 분리돼 있을 수 있고, 캐시 때문에 이전 파일이 남을 수도 있습니다. 따라서 이 레인의 마지막 확인 대상은 배포 작업의 성공 표시가 아니라 바뀐 화면과 실제 사용자 동작이에요.

여기까지 끝나도 설치된 앱은 그대로일 수 있어요. WineDuck 앱은 매번 라이브 웹의 화면 파일을 직접 실행하는 것이 아니라, 바이너리에 내장했거나 OTA로 받은 웹 번들을 사용하기 때문입니다.

OTA: 설치 앱의 웹 번들을 바꿔요

WineDuck의 OTA는 앱과 업데이트 서버라는 두 부분으로 구성돼요. 앱 쪽에는 업데이트 확인, 다운로드, 번들 전환을 맡는 플러그인이 있고, 서버 쪽에는 번들과 활성 대상을 관리하는 API가 있습니다. (@capgo/capacitor-updater 오픈소스 플러그인과 KoalStudio의 Flask 서버를 함께 쓰며 Capgo Cloud는 사용하지 않습니다.)

웹 화면, TypeScript나 JavaScript 로직, CSS, 문구, 로케일처럼 웹 번들 안에 들어가는 변경은 OTA로 전달할 수 있어요. 반대로 네이티브 플러그인, iOS 권한, 앱 아이콘처럼 바이너리 경계에 있는 변경은 OTA만으로 전달할 수 없습니다.

적용 시점도 라이브 웹과 달라요. 앱은 시작 과정에서 업데이트를 확인하고, 서버가 현재 활성 대상으로 제공하는 번들의 버전과 다운로드 주소, 체크섬을 받습니다. 새 번들을 내려받더라도 사용 중인 화면을 갑자기 교체하지 않도록 즉시 전환하지 않아요.

첫 번째 콜드 스타트에서는 업데이트 확인과 다운로드가 일어날 수 있어요. 앱을 완전히 종료한 뒤 다시 시작하는 다음 콜드 스타트에서 내려받은 번들이 적용됩니다. 따라서 한 번 실행해 화면이 그대로라고 실패라고 단정할 수도 없고, 다운로드 로그만 보고 적용 완료라고 말할 수도 없습니다.

새 번들이 정상적으로 열렸다는 준비 상태도 중요해요. 다운로드는 성공했지만 JavaScript 오류나 시작 순서 문제로 번들이 준비되지 않으면 앱은 기대한 화면을 보여주지 못할 수 있습니다. OTA의 동작 확인은 서버의 업데이트·다운로드 기록과 두 콜드 스타트 뒤의 실기기 화면을 함께 봐야 해요.

App Store: 네이티브 경계를 바꾸는 레인

다음 변경은 새 iOS 바이너리가 필요해요.

  • 네이티브 플러그인의 추가, 삭제, 업그레이드
  • Capacitor 앱 설정이나 OTA 서버 주소 변경
  • iOS 권한과 프로젝트 설정 변경
  • 앱 아이콘, 스플래시, 최소 지원 버전 변경

OTA 서버 주소도 네이티브 빌드에 들어갑니다. 이미 설치된 바이너리가 모르는 새 주소를 OTA로 알려줄 수는 없어요. 새 주소를 담은 바이너리가 먼저 설치돼야 그 뒤의 OTA를 받을 수 있습니다.

App Store 바이너리에는 웹 번들도 내장돼요. 네이티브 코드만 맞는다고 끝나지 않고, 어떤 웹 자산이 함께 들어갔는지 확인해야 합니다. 심사와 출시 뒤에는 새 네이티브 동작뿐 아니라 앱 시작 후 OTA 확인이 계속 정상인지도 실기기에서 봐야 해요.

변경 분류가 필요한 레인을 정해요

도구 이름이나 수정한 저장소보다 변경의 경계를 먼저 봅니다.

변경라이브 웹셀프호스트 OTAApp Store 바이너리
화면·웹 로직·CSS·문구·로케일필요설치 앱에도 반영하려면 별도 필요보통 불필요
서버 API 구현별도 서버 배포번들이 새 API를 쓸 때 함께 고려보통 불필요
네이티브 플러그인·앱 설정·iOS 권한웹 영향에 따라단독으로 해결 불가필요
아이콘·스플래시·최소 iOS 버전불필요불가필요

웹 변경을 브라우저와 설치 앱에 모두 전달하려면 라이브 웹과 OTA 산출물을 각각 만들어야 해요. 네이티브 변경이라면 App Store 레인을 타면서 바이너리에 내장할 웹 자산과 현재 활성 OTA 번들의 호환성도 함께 봅니다. 하나의 변경이 여러 행에 걸치면 하나의 레인을 고르는 것이 아니라 필요한 레인을 모두 통과해야 해요.

파일 변경 범위를 보고 네이티브 경계가 포함됐는지 경고하는 일은 자동화할 수 있어요. 웹과 OTA 산출물이 같은 기준 소스에서 만들어졌는지 비교하는 것도 가능합니다. 실제 화면이 의도와 맞는지, 이번 변경을 공개해도 되는지, App Store 제출을 시작할지는 사람의 제품 판단으로 남아요.

OTA 업로드와 롤백은 다른 연산이에요

새 OTA 번들을 업로드할 때 버전은 고유해야 하고 이전 신규 업로드보다 단조 증가해야 해요. 새 버전 번호로 저장하는 동작과 서버가 어느 저장된 번들을 활성 대상으로 제공할지 정하는 동작은 구분됩니다.

롤백이 필요하면 서버에 이미 보관된 이전 정상 번들을 다시 활성화할 수 있어요. 이때 과거 번들을 새 업로드로 위장하거나 반드시 더 높은 버전으로 다시 발행할 필요는 없습니다. 이전 정상 내용을 고쳐 더 높은 새 버전으로 발행하는 방식은 롤백의 유일한 형태가 아니라 롤포워드예요.

이 구분은 콜드 스타트 동작과 함께 봐야 합니다. 서버에서 활성 대상을 바꿔도 설치 앱이 확인하고 내려받고 다음 시작에서 전환하는 시간이 필요해요. 운영 화면에서 활성화가 성공했다는 사실만으로 기기 롤백까지 끝난 것은 아닙니다.

App Store의 두 버전 값을 섞지 않아요

App Store에는 사용자가 보는 마케팅 버전과 각 업로드를 구분하는 빌드 번호가 있어요. 두 값의 재사용 규칙은 다릅니다.

한 번 출시된 마케팅 버전은 새 출시에서 다시 사용할 수 없어요. 반면 아직 출시되지 않은 같은 마케팅 버전 아래에는 여러 빌드를 업로드할 수 있습니다. 이 경우 업로드마다 고유하고 더 높은 빌드 번호를 써야 해요. 예를 들어 출시 전 수정 빌드는 마케팅 버전을 그대로 두고 빌드 번호만 올릴 수 있지만, 이미 출시된 버전의 다음 공개 릴리스라면 마케팅 버전도 올려야 합니다.

OTA 버전, App Store 마케팅 버전, 빌드 번호는 서로 다른 목적을 가져요. 값을 억지로 하나의 증가 규칙으로 묶기보다 어떤 채널의 어떤 산출물을 식별하는지 분리해서 기록하는 편이 안전합니다.

새 바이너리와 활성 OTA의 관계

App Store에서 받은 새 바이너리에는 출시 시점의 웹 번들이 들어 있어요. 그런데 업데이트 서버가 그 바이너리와 호환되지 않거나 의도보다 오래된 웹 번들을 활성 대상으로 제공하면, 앱은 시작 과정에서 내장 화면과 다른 OTA 화면으로 이동할 수 있습니다.

이것은 과거 사고 연대기를 반복할 문제가 아니라 세 레인이 만나는 구조적 경계예요. 네이티브 릴리스 전에 바이너리에 내장한 웹 자산과 서버의 활성 OTA 대상을 함께 확인해야 합니다. 이전 번들을 롤백 대상으로 의도적으로 활성화한 상태라면 그 선택이 새 바이너리에도 어떤 화면과 동작을 제공할지 명시적으로 판단해야 해요.

따라서 App Store 출시 확인은 새 바이너리가 설치됐다는 데서 끝나지 않습니다. 첫 시작의 내장 동작, 업데이트 확인과 다운로드, 다음 콜드 스타트 뒤 활성 OTA 적용까지 이어서 봐야 세 레인의 조합을 확인할 수 있어요.

같은 코드가 아니라 세 도착 경로를 운영해요

WineDuck에서 웹을 배포했다는 말은 브라우저 경로 하나가 바뀌었다는 뜻이에요. 설치 앱까지 바꾸려면 OTA 번들을 별도로 만들고 콜드 스타트 적용을 확인해야 합니다. 네이티브 경계가 바뀌었다면 App Store 바이너리와 그 안에 들어갈 웹 자산까지 새로 다뤄야 해요.

세 레인을 하나로 뭉개면 빠지는 단계가 생겨요. 반대로 변경을 먼저 분류하면 필요하지 않은 심사를 기다리지 않으면서도 필요한 산출물은 빠뜨리지 않을 수 있습니다. 같은 소스를 여러 번 배포하는 일이 아니라, 라이브 웹과 설치 앱과 네이티브 바이너리라는 서로 다른 사용자 도착 경로를 운영하는 일이에요.

댓글

댓글 남기기

댓글 삭제 시 사용됩니다
공개되지 않습니다
0/2000
← 목록으로