Today's

길을 나서지 않으면 그 길에서 만날 수 있는 사람을 만날 수 없다

모바일 앱(안드로이드)

습관관리 앱 : 개발 일기, 인앱 업데이트, 코드 최적화, 그리고 험난했던 Gradle 플러그인 설정기

Billcorea 2025. 10. 24. 15:59
반응형

 

습관관리 앱 : 개발 일기, 인앱 업데이트, 코드 최적화, 그리고 험난했던 Gradle 플러그인 설정기

앱 가이드 이미지

 

오늘의 목표: 사용자를 위한 편의 기능 추가와 Play Store 출시 준비!

오늘은 앱에 두 가지 중요한 기능을 추가하고 출시 준비를 하는 날입니다. 하나는 사용자가 앱을 항상 최신 버전으로 유지할 수 있도록 '인앱 업데이트' 기능을 구현하는 것이고, 다른 하나는 출시를 위해 앱 용량을 줄이고 코드를 보호하는 '최적화' 작업입니다. 그리고 이 과정에서 예상치 못한 Gradle 플러그인 설정이라는 큰 산을 만났습니다. 그 험난했던 여정을 기록해 봅니다.

1. In-App Update 구현하기

사용자가 Play Store에 직접 방문하지 않아도 앱 내에서 업데이트를 확인하고 설치할 수 있게 하는 것은 사용자 경험에 매우 중요합니다. Google Play에서 제공하는 라이브러리를 사용하면 간단하게 구현할 수 있습니다.

1-1. 의존성 추가

먼저 app/build.gradle.kts 파일에 Google Play In-App Update 라이브러리를 추가합니다.


// app/build.gradle.kts
dependencies {
    // ... 다른 의존성들
    implementation("com.google.android.play:app-update-ktx:2.1.0") // 인앱 업데이트 라이브러리
}
    

1-2. MainActivity에 코드 구현

앱이 시작되는 MainActivity에서 업데이트를 확인하는 로직을 추가합니다. 사용자가 앱을 사용할 때 방해받지 않는 'FLEXIBLE' 타입으로 구현했습니다.


// MainActivity.kt

class MainActivity : ComponentActivity() {

    private lateinit var appUpdateManager: AppUpdateManager

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // AppUpdateManager 초기화
        appUpdateManager = AppUpdateManagerFactory.create(applicationContext)
        
        // 업데이트 확인 및 요청
        checkForAppUpdate()

        // ...
    }

    private fun checkForAppUpdate() {
        appUpdateManager.appUpdateInfo.addOnSuccessListener { info ->
            // 업데이트가 가능하고, 'FLEXIBLE' 타입이 허용되는지 확인
            val isUpdateAvailable = info.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
            val isUpdateAllowed = info.isFlexibleUpdateAllowed

            if (isUpdateAvailable && isUpdateAllowed) {
                // 업데이트 흐름 시작
                appUpdateManager.startUpdateFlowForResult(
                    info,
                    activityResultLauncher, // ActivityResultLauncher
                    AppUpdateOptions.newBuilder(AppUpdateType.FLEXIBLE).build()
                )
            }
        }
    }
    
    // 다운로드가 완료되면 사용자에게 알리고, 앱 재시작을 통해 업데이트를 설치하도록 유도
    private val installStateUpdatedListener = InstallStateUpdatedListener { state ->
        if (state.installStatus() == InstallStatus.DOWNLOADED) {
            Toast.makeText(applicationContext, "새 버전 다운로드가 완료되었습니다. 앱을 재시작하면 적용됩니다.", Toast.LENGTH_LONG).show()
            // 앱 재시작
            lifecycleScope.launch {
                appUpdateManager.completeUpdate()
            }
        }
    }
}
    

2. 출시를 위한 앱 최적화 (R8 활성화)

Play Store에 앱을 출시하기 전, 코드 난독화와 리소스 축소를 통해 APK/AAB 파일 크기를 줄이고, 코드를 보호하는 것이 좋습니다. isMinifyEnabledisShrinkResources 옵션을 활성화하면 R8 컴파일러가 이 작업을 수행합니다.


// app/build.gradle.kts

android {
    buildTypes {
        release {
            isMinifyEnabled = true       // 코드 축소 및 난독화 활성화
            isShrinkResources = true   // 사용하지 않는 리소스 제거 활성화
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
}
    

🚨 중요 이슈: Proguard와 런타임 오류

isMinifyEnabled = true를 설정하면 R8이 코드의 클래스, 함수, 변수 이름을 짧게 바꾸어 버립니다. 그런데 Room(DB)이나 Retrofit(네트워킹) 같은 라이브러리는 특정 이름의 클래스나 함수를 '찾아서' 실행하는 '리플렉션' 기술을 사용합니다. R8이 이름을 바꿔버리면 라이브러리가 대상을 찾지 못해 앱이 비정상 종료될 수 있습니다.

해결책: Proguard 규칙 추가

proguard-rules.pro 파일에 R8이 변경해서는 안 될 클래스를 명시해주어야 합니다. 데이터 모델(DTO, Entity 등)이 담긴 패키지 전체를 유지하도록 설정하는 것이 일반적입니다.


# proguard-rules.pro

# Room 데이터베이스 및 Retrofit DTO 클래스가 난독화되지 않도록 유지
-keep class com.billcorea.habit1007.data.** { *; }
-keep class com.billcorea.habit1007.data.remote.coupang.dto.** { *; }
        

3. Play Store 자동화를 향한 험난한 여정

코드 난독화를 적용하면, Play Console에 올라온 비정상 종료 리포트(Crash Report)를 알아볼 수 없게 됩니다. 이를 해결하려면 빌드 시 생성되는 mapping.txt 파일을 Play Console에 업로드해야 합니다. 이 과정을 자동화하기 위해 'Gradle Play Publisher' 플러그인을 도입하기로 했습니다. 그리고 여기서부터 대장정이 시작되었습니다.

😭 첫 번째 실수: 잘못된 플러그인 ID

가장 큰 실수는 플러그인의 ID를 완전히 잘못 알고 있었다는 것입니다. 저는 계속해서 존재하지 않는 com.google.android.play라는 ID를 사용하며 빌드 실패의 늪에 빠졌습니다.

"Plugin [id: 'com.google.android.play'] was not found in any of the following sources..."

이 오류 메시지를 몇 번이나 봤는지 모릅니다. Gradle 설정 문법을 바꿔보고, 레거시 방식을 시도하고, 심지어 settings.gradle.kts 파일의 저장소 필터까지 의심하며 엉뚱한 곳을 파고 있었습니다.

💡 깨달음과 해결: 진짜 플러그인 ID는 따로 있었다!

수많은 실패 끝에, 진짜 플러그인 ID는 com.github.triplet.play라는 것을 알게 되었습니다. 등잔 밑이 어둡다는 말이 딱 맞았습니다.

올바른 ID를 사용하여 최종적으로 버전 카탈로그(libs.versions.toml) 방식으로 깔끔하게 설정을 완료했습니다.

최종 성공 설정 (3단계)

1. gradle/libs.versions.toml 파일: 플러그인 별칭 정의


# [versions] 섹션
play-publisher = "3.8.6" 

# [plugins] 섹션
play-publisher = { id = "com.github.triplet.play", version.ref = "play-publisher" }
        

2. 최상위 build.gradle.kts 파일: 플러그인 별칭 적용


plugins {
    // ...
    alias(libs.plugins.play.publisher) apply false
}
        

3. app/build.gradle.kts 파일: 앱 모듈에 플러그인 적용 및 설정


plugins {
    // ...
    alias(libs.plugins.play.publisher)
}

// ...

// play 블록 추가 및 서비스 계정 json 파일 경로 지정
play {
    serviceAccountCredentials.set(project.file("play-console-credentials.json"))
}
        

마치며

오늘은 간단할 거라 생각했던 작업이 예상치 못한 Gradle 설정 문제로 길어졌습니다. 하지만 이 과정을 통해 플러그인 ID의 중요성과 Gradle의 저장소 검색 메커니즘에 대해 깊이 배울 수 있었습니다. 역시 개발은 끊임없는 문제 해결과 배움의 연속인 것 같습니다. 이 글이 저처럼 Gradle 플러그인 설정으로 고통받는 누군가에게 작은 도움이 되기를 바랍니다.

반응형