flutter install은 설치 전에 앱을 지운다

실기기에 새 build를 덮어쓰고 싶었다.
명령 이름은 정확히 그렇게 들렸다.
flutter install
그런데 installer의 첫 행동은 install이 아니었다.
Uninstalling old version...
기존 앱을 지운 뒤 새 앱을 설치했다.
일상용 iPhone에서 이 차이는 단어 문제가 아니었다.
실제 한 device pass에서는 로컬 score와 streak가 사라졌다.
install이라는 이름을 install-over-existing으로 읽은 대가였다.
현재 구현은 먼저 uninstall한다
이 글을 쓴 환경의 Flutter는 3.47.1 stable, framework revision 6655482ec0이다.
현재 flutter_tools의 install.dart에는 install helper가 있다.
핵심 default는 다음과 같다.
bool uninstall = true,
기기에 같은 package가 설치돼 있고 이 값이 true라면 helper는 상태를 출력한다.
Uninstalling old version...
그다음 device.uninstallApp(...)을 호출한다.
마지막에야 device.installApp(...)을 호출한다.
순서는 명확하다.
isAppInstalled
→ uninstallApp
→ installApp
flutter install command는 이 helper를 기본값 그대로 호출한다.
따라서 기존 앱이 감지된 일반 경로에서 uninstall은 부수 효과가 아니다.
현재 command가 의도한 단계다.
command 이름은 보존 계약이 아니다
설치는 새 binary를 기기에 넣는다는 뜻만 전달한다.
기존 app container를 보존하는지, 먼저 지우는지까지 이름이 말해 주지는 않는다.
같은 이름의 작업도 tool마다 구현이 다를 수 있다.
version이 바뀌면 구현도 바뀔 수 있다.
그래서 실기기 write를 결정할 때에는 command의 일상어 의미가 아니라 현재 installer의 경계를 읽어야 한다.
이 경우 가장 작은 evidence는 source와 실제 출력의 Uninstalling 한 줄이다.
flutter run은 다른 경로를 탄다
현재 Flutter source에서 물리 iOS CoreDevice의 한 run 경로도 확인했다.
debugging이 비활성화된 해당 경로는 _coreDeviceControl.installApp(...)을 호출한 뒤 launchApp(...)을 호출한다.
Flutter의 명시적 uninstallApp(...) 단계는 그 경로에 없다.
내부 install은 다음 형태의 devicectl command를 구성한다.
devicectl device install app
현재 local devicectl help도 이 command를 “Installs an app”이라고 설명한다.
실제 과거 device pass에서도 flutter run을 통한 여러 install은 uninstall 문구를 출력하지 않았고, flutter install만 출력했다.
이 차이 때문에 install-over-existing이 필요할 때에는 flutter run 경로를 우선한다.
범위를 좁혀 읽어야 한다
여기서 “flutter run은 언제나 안전하다”라는 새 일반화를 만들면 안 된다.
확인한 source는 현재 Flutter 3.47.1의 물리 iOS CoreDevice 경로다.
platform, Xcode, device type, build mode, debugging path가 달라지면 호출 경로도 달라질 수 있다.
flutter run도 read-only command가 아니다.
앱을 설치하고 launch한다.
option에 따라 실행 중인 앱을 다루는 방식도 달라질 수 있다.
그러므로 “run이니까 안전”이 아니라 “이번 version과 target에서 explicit uninstall 없이 install하는 경로를 확인했다”가 정확한 결론이다.
데이터 보존도 별도 검증 대상이다
explicit uninstall이 없다는 사실은 중요한 경계다.
하지만 이것만으로 모든 app state 보존을 보증할 수는 없다.
bundle identifier, signing, entitlement, app migration, OS 동작은 별도 변수다.
installer가 uninstall을 호출하지 않았다는 evidence와, 사용자가 실제로 score나 session을 유지했다는 evidence는 다르다.
후자는 install 뒤 앱을 열어 읽어야 한다.
보존이 목적이라면 build 설치 성공만 보지 않는다.
보존하려던 값을 직접 확인한다.
일상용 폰에서는 write를 한 단계씩 다룬다
실기기가 전용 test rig가 아니라면 install, launch, termination은 서로 다른 영향이다.
한 번의 승인이 세 단계를 모두 승인한 것으로 취급하면 안 된다.
실행 전에는 한 줄로 다음 write를 알린다.
대상 기기에 현재 profile build를 install-over-existing 방식으로 설치합니다.
install이 끝난 뒤 launch가 필요하면 그것도 별도 단계로 다룬다.
session이 끝날 때에는 폰에 어떤 build가 남았는지 말한다.
작업 로그의 마지막 성공보다 사용자가 다음에 열 앱이 더 중요하다.
실행 전에 확인할 것
첫째, target device가 맞는지 확인한다.
실제 identifier는 글이나 공유 로그에 노출하지 않는다.
둘째, 현재 Flutter version과 command 구현을 확인한다.
셋째, 기존 앱 보존이 필요한지 virgin install이 필요한지 먼저 결정한다.
넷째, installer output에서 Uninstalling을 찾는다.
다섯째, 설치 뒤 보존 대상 state를 앱에서 직접 읽는다.
여섯째, session 종료 시 남은 build mode와 version을 기록한다.
이 글에서는 기기에 쓰지 않았다
이 글의 확인은 Flutter source, flutter --version, devicectl help만 읽었다.
실제 iPhone에 install, uninstall, launch, terminate command를 실행하지 않았다.
source inspection으로 command path를 확인하는 일과 device state를 바꾸는 일은 분리할 수 있다.
먼저 읽고, 필요한 write만 승인받아 실행하면 된다.
실전 규칙
현재 flutter install은 기존 앱이 있으면 explicit uninstall을 먼저 수행한다.
출력의 Uninstalling old version...은 장식이 아니라 실제 호출 경계다.
현재 물리 iOS CoreDevice의 확인된 flutter run 경로는 explicit Flutter uninstall 없이 install과 launch를 수행한다.
이 결과를 다른 platform, mode, version에 자동으로 일반화하지 않는다.
실기기 write는 install, launch, termination을 각각 알리고 실행한다.
install-over-existing 여부와 실제 app state 보존은 따로 확인한다.
마지막으로
명령 이름은 요약이다.
구현은 계약이다.
install 여덟 글자보다 Uninstalling old version... 한 줄이 더 많은 것을 말할 때가 있다.
특히 그 기기가 내일 아침에도 들고나갈 폰이라면.
다음 글도 받아보세요.
글을 끝까지 읽으셨다면, 다음 글은 받은편지함이나 RSS 리더에서 만나보세요.