Case Study · 10

스토어 첫 출시부터 1.0.5까지 — 앱 경량화와 심사 대응

MeetMate 모바일을 App Store·Google Play에 처음 출시하고 5주 동안 1.0.0(빌드 10)부터 1.0.5(빌드 29)까지 이어서 배포했습니다. R8을 처음 켜서 Android DEX를 22.95MB에서 5.60MB로 줄였고(다운로드 −30.6%), Hermes 바이트코드는 두 차례에 걸쳐 각각 −22%, −20.6% 줄였습니다. 반려 사유는 제출 전에 미리 걷어냈고, 실제로 받은 반려는 바이너리 재업로드 없이 회신으로 마무리했습니다.

Headline Outcome
23MB23MB

Android 압축 해제 DEX 크기 (R8 적용 전후)

Product
MeetMate (Mobile)
Role
Frontend Engineer
Period
2026.08.28 — 진행 중
Stack
React Native 0.77 Hermes R8 Xcode / XCUITest
Implementation AI-paired (Claude agent)
  • Android DEX **22.95MB → 5.60MB** — R8 full mode를 처음 켜면서 keep 규칙은 꼭 필요한 만큼만 두어 다운로드 크기 −30.6%. Play Console 앱 최적화 지적 대상(DEX 10MB 초과)에서도 벗어났습니다.
  • Hermes 바이트코드 2단계 감량 — 쓰지 않는 의존성을 걷어내 **−22%**, 아이콘 1,767개를 통째로 싣던 배럴 import를 실제로 쓰는 85개만 재수출하는 방식으로 바꿔 **−20.6%**를 더 줄였습니다(9.03MB → 7.17MB).
  • 5주 동안 **빌드 10 → 29**, 1.0.0 → 1.0.5 연속 배포 — 정형 반려 사유가 되는 `UIBackgroundModes` voip 선언을 제출 전에 제거했고, 실제로 받은 반려(Guideline 2.3.10)와 정보 요청(2.1)은 바이너리 재업로드 없이 회신으로 끝냈습니다.
  • 라이브러리가 끼워 넣던 위험 권한 **4종 → 0건**, 앱 안에서 계정을 탈퇴하는 경로(App Review 5.1.1(v)), targetSdk 36 edge-to-edge까지 처리해 스토어 요건 블로커를 정리했습니다.
  • 릴리스 위생을 계약 테스트로 고정 — 이전 번들 ID로 만든 IPA가 업로드될 수 있던 경로를 실측으로 찾아 막았고, 스토어 스크린샷은 접근성 트리로 조작하는 UI 테스트로 자동 촬영합니다.

배경

웹 리뉴얼에 맞춰 모바일 앱을 정비한 직후 바로 첫 스토어 출시를 준비했습니다. 그런데 출시 직전 Play Console이 앱 최적화 항목을 지적했습니다. 압축 해제 DEX가 22.95MB였는데 R8이 한 번도 켜진 적이 없었습니다. Metro는 트리셰이킹을 하지 않아서, 아이콘 라이브러리를 배럴로 import하면 실제로 쓰지 않는 아이콘까지 모두 번들에 들어가고 있었습니다.

심사 쪽도 위험 요소가 많았습니다. iOS Info.plist에는 기능상 효과가 없는 voip 배경 모드가 선언되어 있었고, 심사 메모에는 그 사유가 적혀 있었습니다. 병합된 Android 매니페스트에는 앱이 쓰지 않는 생체 인증·부팅 수신·정확 알람 권한이 라이브러리를 통해 들어와 있었습니다. 계정 탈퇴 경로도 없었습니다.

접근

먼저 Android 바이너리를 줄였습니다. R8 full mode와 리소스 축소를 켜고, keep 규칙은 라이브러리마다 이미 제공하는 consumer 규칙에 맡겼습니다. 직접 추가한 규칙은 리플렉션으로만 접근해 R8이 알아보지 못하는 지점에 한정했습니다. 통째로 keep하면 최적화율이 떨어지기 때문입니다. 실제 Play 배포 경로를 재현하려고 AAB에서 split APK를 뽑아 Maestro로 로그인부터 회의 입장까지 확인했습니다. 결과는 DEX 22.95 → 5.60MB, 다운로드 −30.6%였습니다.

JS 번들은 실제 hermesc로 측정한 다음 잘라냈습니다. 쓰지 않는 SDK 대입 한 줄 때문에 597KB가 딸려 들어오고 있었고, throttle 하나 때문에 lodash 전체가 실려 있었습니다. 이것들을 걷어내 Hermes −22%를 얻었습니다. 아이콘은 실제로 쓰는 85개만 딥임포트해서 다시 내보내는 배럴로 바꿔 −20.6%를 더 줄였습니다. 이름 규칙만으로는 맞힐 수 없는 별칭 아이콘 9개는 실제로 렌더한 SVG 도형까지 원본과 비교하는 테스트로 고정했습니다.

아이콘이 “조용히 다른 모양으로 그려지는” 회귀는 사람이 눈으로 찾기 어렵습니다. 그래서 계약 테스트가 원본 배럴 표 전체와 대조하도록 했습니다.

iOS에는 R8에 해당하는 빠진 단계가 없었습니다. 그래서 숨어 있던 기본값 11개를 빌드 설정에 명시하고 Swift whole module만 실제로 바꿨습니다. 측정해 보니 IPA 33MB 중 47.8%가 사용자에게 전달되지 않는 심볼 파일이었고, 실제 다운로드는 약 17MB였습니다. 없는 이득은 주장하지 않는다는 원칙에 따라 효과가 측정되지 않은 LTO와 에셋 옵션은 되돌렸습니다.

심사 대응은 반려 사유를 미리 걷어내는 쪽으로 진행했습니다. voip 배경 모드와 그 사유를 적은 심사 메모를 함께 지웠고, 라이브러리가 넣던 위험 권한 4종은 매니페스트 병합 결과를 실측하며 제거하거나 상한을 걸었습니다. 앱 내 계정 탈퇴는 서버 동작이 실제로는 비활성화라는 점을 확인한 뒤 확인창 문구도 사실에 맞게 적었습니다. 그래도 받은 반려(2.3.10, 앱 설명에 다른 플랫폼 이름을 쓴 한 줄)는 문안 수정과 회신으로 처리했습니다.

마지막으로 릴리스 경로의 함정을 테스트로 막았습니다. 앱 이름을 바꾼 뒤 업로드 스크립트가 같은 자리에 남아 있던 이전 번들 ID의 옛 IPA를 가리키던 문제는, 가장 최근 산출물을 동적으로 고르고 없으면 즉시 실패하도록 바꿨습니다. 스토어 스크린샷은 XCUITest 타깃이 접근성 트리로 화면을 조작하며 iPhone 6.9”와 iPad 13”용을 자동으로 촬영합니다.

결과

  • Android DEX 22.95MB → 5.60MB, 다운로드 −30.6%, R8 예외 0건.
  • Hermes 바이트코드 −22% (쓰지 않는 의존성 제거), 이어서 −20.6% (아이콘 배럴 1,767 → 85개).
  • iOS 빌드 설정 11개 명시, 크래시 심볼화 보증. IPA 33MB 중 실제 다운로드 약 17MB.
  • 첫 출시 1.0.0(빌드 10) 이후 5주 동안 1.0.5(빌드 29)까지 연속 배포.
  • 위험 권한 4종 → 0건, 앱 내 계정 탈퇴, targetSdk 36 edge-to-edge 대응.
  • 반려 1건과 정보 요청 1건을 바이너리 재업로드 없이 해결.

배운 점

출시 직전의 최적화는 측정 경로가 실제 배포 경로와 같을 때만 의미가 있었습니다. universal APK에서 통과해도 Play가 내려보내는 split APK에서 깨질 수 있어서, AAB에서 split을 직접 뽑아 검증한 것이 R8을 안심하고 켤 수 있었던 이유였습니다.

심사도 비슷했습니다. 반려는 대부분 앱이 실제로 하지 않는 일을 선언한 곳에서 나옵니다. 쓰지 않는 배경 모드, 쓰지 않는 권한, 사실과 다른 확인창 문구가 그런 예입니다. 선언과 실제 구현을 맞추고, 그 상태가 다시 어긋나지 않도록 테스트로 고정해 두는 것이 반려를 줄이는 가장 싼 방법이었습니다.