앱 용량 최적화: Android AAB와 iOS App Thinning

모바일 개발

앱 최적화Android AABiOS App Thinning앱 용량ProGuard

이 글은 누구를 위한 것인가

  • 앱 용량이 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: 최적화된 다운로드 크기 확인