계기는 단순했다. artoon 안드로이드 빌드를 직접 켜 보는데, 스플래시가 끝나고 로딩 화면으로 넘어가는 순간이 어색했다. 흰 화면이 한 번 번쩍이고 아이콘이 잘려 보였다. iOS에서 가져온 앱 16개가 같은 틀로 만들어졌으니, 하나가 이러면 나머지도 그럴 가능성이 높았다.
정적 검사는 아무 말도 안 했다
flutter analyze는 16개 앱 전부 깨끗했다. 테스트도 전부 초록이었다. 그래서 이번 감사의 전제는 처음부터 "이 계열은 정적 검사로 안 잡힌다"였다. 앱별로 리뷰 조 4개를 붙여 화면을 실제로 넘겨 보게 하고, 따로 플릿 전체를 훑는 스캔을 하나 더 돌렸다.
artoon의 정체: Android 12가 스플래시를 그린다
원인은 앱 코드가 아니라 OS였다. API 31부터 시스템은 앱이 지정한 windowBackground를 무시하고 자기 스플래시를 그린다. 배경은 흰색, 런처 아이콘은 안쪽 2/3만. values-v31에 windowSplashScreenBackground를 지정하지 않으면 다크 앱이 흰 번쩍임과 잘린 아이콘으로 열린다.
16개 중 13개가 그대로였다. 대응해 둔 건 artoon·constella·typotypo 셋뿐이었는데, 정작 artoon이 이상해 보인 건 다른 함정 때문이었다. 리소스 한정자 우선순위에서 night가 version을 이긴다. values-night/가 있으면 values-night-v31/이 따로 없는 한 values-v31/은 다크 모드에서 무시된다. 그래서 다크 모드 기기에서만 흰 화면이 났다.
android/app/src/main/res/
values-v31/styles.xml ← 라이트
values-night-v31/styles.xml ← 이게 없으면 다크에서 위 파일이 무효
플릿 공통으로 나온 것
- iOS는 세로 고정인데 Android에는
screenOrientation도setPreferredOrientations도 없는 앱 7개. 가로 레이아웃을 아무도 본 적이 없다는 뜻이다. logoform 편집기는 가로에서 캔버스 높이가 0으로 눌렸다. - 부팅 리시버는 선언했는데
RECEIVE_BOOT_COMPLETED권한이 없는 앱 3개. 재부팅하면 예약 알림이 사라진다. - 갤러리 저장 권한이 빠진 앱 3개.
앱별로 나온 것
- bori — 시스템 뒤로가기가 모든 화면에서 앱을 종료했다. 라우트 하나에 오버레이를 쌓는 구조라 뒤로가기가 스택을 찾지 못한 것. 풀던 사건이 날아가고, X 버튼에 붙인 확인 다이얼로그도 우회된다.
- kkumi — 다크 홈 배너의 글자 대비가 1.05:1. 첫 화면에서 글자가 사실상 안 보였다.
- korio — 문법 레퍼런스 화면이 동기 sqlite를 10,635번 호출해 UI를 멈춰 세웠다. 며칠 전 ANR 보고의 원인이었다.
- stickerform — 예외 처리가 특정 예외형만 잡아서 네트워크 오류가 전부 새어 나갔다. 토큰은 차감되는데 화면은 조용했다. 결제 정산이 실패하면 스피너가 영원히 돌았다.
- fandi — 결제에 성공해도 페이월이 안 닫혔다.
buyNonConsumable은 Play 결제창이 열린 시점에 반환하는데, 그 직후 권한을 읽으니 언제나 false였다. 결과는purchaseStream으로 온다. - typotypo — 일본어 프리셋 견본이 판 밖으로 나갔다.
- artoon — 설정의 Our Apps 링크가 다른 개발자 페이지를 가리키고 있었다.
리뷰 조가 올린 지적 중 8건은 근거를 대조해 기각했다. 오탐을 고치면 멀쩡한 걸 망가뜨린다.
고치는 방식
결함마다 회귀 테스트를 하나씩 붙였다. 조건은 "고치면 통과, 되돌리면 실패"까지 확인하는 것. 통과만 보는 테스트는 아무것도 지키지 않는다. 16개 앱 analyze 무결, 5,157개 테스트 통과 상태로 출고했다.
출고하다가 난 사고 둘
첫째. 회전 잠금 주석을 <activity> 태그의 속성 사이에 넣었다. XML로는 깨진 문서인데 analyze도, 5,157개 테스트도, 매니페스트 테스트 16개도 전부 통과했다. 매니페스트 테스트가 파일을 문자열로 읽어 contains만 확인했기 때문이다. 빌드 3개가 실패하고서야 알았다. 주석을 태그 위로 옮기고, 16개 앱에 XML well-formed 테스트를 넣었다. 결함을 재현해 실패하는 것부터 확인했다.
둘째. dart format을 폴더 단위로 돌렸다. 이 리포는 전체가 현재 포매터 기준이 아니라서 손대지 않은 파일 수백 개가 재정렬됐고, logoform에 있던 커밋 전 작업이 덮였다. 테스트를 근거로 복원했다. 이제 이 리포에서는 고친 파일만 지정해서 포맷한다.
남긴 것
- 이식 앱의 결함 유형 8종을 정리해 다음 이식 때 체크리스트로 쓴다. 정적 검사가 못 잡는 것들이라 누군가 화면을 넘겨 봐야 한다.
- 실기기 검증은 이번에 못 했다. 에뮬레이터까지다.