이 글은 누구를 위한 것인가
- 앱 용량이 100MB를 넘어 다운로드 전환율이 떨어진 팀
- Android AAB와 Dynamic Delivery를 처음 적용하는 개발자
- iOS On Demand Resources로 초기 용량을 줄이려는 팀
들어가며
앱 용량은 다운로드 전환율에 직접 영향을 준다. 100MB 넘는 앱은 Wi-Fi에서만 다운로드되며, 50MB 이하 앱 대비 전환율이 20-30% 낮다. Android AAB와 iOS App Thinning은 기기별 최적화된 앱을 제공해 실질 다운로드 크기를 크게 줄인다.
이 글은 bluefoxdev.kr의 앱 경량화 가이드 를 참고하여 작성했습니다.
1. 앱 용량 분석과 최적화 전략
[Android 앱 크기 분석]
Android Studio: Build > Analyze APK
DEX: 코드 (Kotlin/Java 바이트코드)
Resources: 이미지, 레이아웃 XML
Assets: 폰트, 사운드, 데이터
Lib: Native .so 파일 (ABI별)
META-INF: 서명
크기 주범 (순서대로):
1. 이미지 리소스 (PNG, JPEG)
2. Native 라이브러리 (여러 ABI)
3. 폰트 파일
4. DEX (코드 + 라이브러리)
[iOS 앱 크기 분석]
Xcode: Product > Archive > Distribute > App Store Connect
App Store Connect: App Size Report 다운로드
주요 항목:
Executable (컴파일된 코드)
Assets.xcassets (이미지)
Frameworks (동적 라이브러리)
Resources (기타 파일)
[목표 크기]
초기 다운로드: < 50MB (셀룰러 자동 다운로드)
앱 실행에 필요한 최소한만 포함
나머지는 ODR/Dynamic Delivery로 지연 로딩
2. Android App Bundle과 iOS App Thinning
// Android - App Bundle 설정 (build.gradle.kts)
android {
bundle {
// ABI 분리 (arm64, x86_64 각각 별도 APK)
abi { enableSplit = true }
// 밀도 분리 (hdpi, xhdpi 각각)
density { enableSplit = true }
// 언어 분리 (필요 언어만 포함)
language { enableSplit = true }
}
buildTypes {
release {
// R8 코드 축소 + 난독화
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
// ABI 필터 (불필요한 아키텍처 제외)
defaultConfig {
ndk {
abiFilters += listOf("arm64-v8a", "x86_64")
}
}
}
// 이미지 WebP 변환 (Android Studio 우클릭 > Convert to WebP)
// PNG 대비 25-34% 작음
// 벡터 드로어블 사용 (PNG 대체)
// res/drawable/ic_logo.xml 대신 PNG 세트 불필요
# proguard-rules.pro
# 기본 설정
-optimizationpasses 5
-dontusemixedcaseclassnames
-dontskipnonpubliclibraryclasses
-verbose
# 불필요한 로그 제거
-assumenosideeffects class android.util.Log {
public static *** d(...);
public static *** v(...);
public static *** i(...);
}
# Kotlin Serialization
-keepattributes *Annotation*, InnerClasses
-dontnote kotlinx.serialization.AnnotationsKt
# Retrofit
-keepattributes Signature, Exceptions
-keep class retrofit2.** { *; }
// Dynamic Feature Module - 대용량 콘텐츠 분리
// settings.gradle.kts
// include(":app", ":feature_ar", ":feature_premium")
// feature_ar/build.gradle.kts
plugins {
id("com.android.dynamic-feature")
}
android {
// 기본 설치 안 됨, 필요시 다운로드
}
// 앱에서 Dynamic Feature 요청
class MainActivity : AppCompatActivity() {
private lateinit var splitInstallManager: SplitInstallManager
fun downloadArFeature() {
splitInstallManager = SplitInstallManagerFactory.create(this)
val request = SplitInstallRequest.newBuilder()
.addModule("feature_ar")
.build()
splitInstallManager.startInstall(request)
.addOnSuccessListener { sessionId ->
// AR 기능 다운로드 시작
startArActivity()
}
.addOnFailureListener { exception ->
when ((exception as SplitInstallException).errorCode) {
SplitInstallErrorCode.NETWORK_ERROR -> showRetryDialog()
else -> showErrorDialog()
}
}
}
private fun startArActivity() {
// 다운로드 완료 후 AR 화면 시작
val intent = Intent().setClassName(
packageName,
"com.app.feature_ar.ArActivity"
)
startActivity(intent)
}
}
// iOS - On Demand Resources (ODR)
// 초기 앱에 포함하지 않고 필요시 다운로드
import Foundation
class ODRManager {
private var resourceRequest: NSBundleResourceRequest?
// AR 콘텐츠 태그로 그룹화된 리소스 요청
func loadArAssets(completion: @escaping (Result<Void, Error>) -> Void) {
let tags: Set<String> = ["ar-content"]
resourceRequest = NSBundleResourceRequest(tags: tags)
// 우선순위 설정
resourceRequest?.loadingPriority = NSBundleResourceRequestLoadingPriorityUrgent
resourceRequest?.beginAccessingResources { [weak self] error in
DispatchQueue.main.async {
if let error = error {
completion(.failure(error))
} else {
// 리소스 사용 가능
completion(.success(()))
}
}
}
}
func releaseArAssets() {
resourceRequest?.endAccessingResources()
resourceRequest = nil
}
// 조건부 요청 (이미 다운로드됐는지 확인)
func prefetchArAssets() {
let tags: Set<String> = ["ar-content"]
let request = NSBundleResourceRequest(tags: tags)
request.conditionallyBeginAccessingResources { available in
if !available {
// 백그라운드에서 미리 다운로드
request.beginAccessingResources { _ in }
}
}
}
}
// Asset Catalog 최적화
// Xcode에서 이미지 압축 설정:
// Build Settings > Compress PNG Files = YES
// Build Settings > Remove Text Metadata From PNG Files = YES
// HEIC 이미지 사용 (iOS 11+)
// PNG보다 50% 작음
// Assets.xcassets에서 HEIC 포맷 지원
// App Thinning 확인
// Xcode > Product > Archive
// Distribute App > Development > Export
// ExportOptions.plist에서 thinning 옵션 확인
#!/bin/bash
# 이미지 최적화 스크립트
# Android: PNG → WebP 변환
find . -name "*.png" -not -path "*/\.git/*" | while read file; do
cwebp -q 85 "$file" -o "${file%.png}.webp"
echo "변환: $file → ${file%.png}.webp"
done
# iOS: ImageOptim으로 PNG 최적화 (별도 설치 필요)
# imageoptim Assets.xcassets/**/*.png
# 미사용 리소스 찾기 (Android)
# Android Studio: Refactor > Remove Unused Resources
# 미사용 Swift 코드 찾기
# periphery scan --workspace App.xcworkspace --schemes App
# 앱 크기 측정 (Android)
bundletool build-apks \
--bundle=app-release.aab \
--output=app.apks \
--ks=keystore.jks \
--ks-pass=pass:password \
--ks-key-alias=alias
bundletool get-size total --apks=app.apks
# Downloads Size: 최적화된 다운로드 크기 확인