Today's

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

모바일 앱(안드로이드)

In-App Update 기능 구현 완료 보고서

Billcorea 2026. 3. 12. 15:10

앱 업데이트

In-App Update 기능 구현 완료 보고서

📋 개요

Google Play In-App Update API (v2.1.0)를 이용하여 DayCnt415 앱에 인앱 업데이트 기능을 추가했습니다.

구현 날짜: 2026-03-10
상태: ✅ 완료 및 테스트 가능


🎯 기능 설명

1. 두 가지 업데이트 모드 지원

IMMEDIATE 모드 (강제 업데이트)

  • 언제 사용: 중요한 보안 업데이트나 필수 기능 업데이트
  • 사용자 경험: 스킵 불가능, 뒤로가기 버튼 비활성화
  • 트리거 조건: 우선순위 ≥ 4 또는 버전 차이 > 5
  • UI: 설치 진행률 표시, 중단 불가

FLEXIBLE 모드 (선택적 업데이트)

  • 언제 사용: 일반 기능 개선사항 또는 버그 픽스
  • 사용자 경험: 나중에, 지금 업데이트 버튼 제공
  • 트리거 조건: 모든 업데이트 사용 가능 (우선순위 < 4)
  • UI: 다운로드 진행률 표시, 유연한 설치 옵션

🏗️ 아키텍처

레이어 구조

AppUpdateProvider (Compose Root)
    ↓
UpdateViewModel (상태 관리)
    ↓
AppUpdateService (비즈니스 로직)
    ↓
Google Play Core Library (네이티브 API)

컴포넌트 설명

컴포넌트 파일경로 책임
AppUpdateService domain/service/AppUpdateService.kt Play Core API 래핑, 라이프사이클 관리, 상태 모니터링
UpdateViewModel presentation/viewmodel/UpdateViewModel.kt StateFlow 기반 상태 관리, UI 로직
AppUpdateDialog presentation/ui/components/AppUpdateDialog.kt Compose UI (IMMEDIATE/FLEXIBLE 모드)
UpdateAvailableBanner presentation/ui/components/AppUpdateDialog.kt 저우선순위 업데이트 배너
AppUpdateProvider presentation/ui/screens/AppUpdateProvider.kt Compose 루트 래퍼, 라이프사이클 동기화
UpdateModule di/UpdateModule.kt Hilt 의존성 주입 설정

📁 생성된 파일

1. 도메인 계층

app/src/main/java/com/billcoreatech/daycnt415/domain/service/
└── AppUpdateService.kt
    ├── AppUpdateService 클래스 (Play Core API 래핑)
    ├── UpdateInstallState sealed class (설치 상태)
    └── UpdateAvailableState data class (업데이트 정보)

주요 메서드:

  • checkForAppUpdate(): Task<AppUpdateInfo> - 업데이트 확인
  • startImmediateUpdateFlow() - 강제 업데이트 시작
  • startFlexibleUpdateFlow() - 선택적 업데이트 시작
  • completeUpdate() - 다운로드된 업데이트 설치
  • registerInstallStateUpdatedListener() - 상태 모니터링 시작
  • unregisterInstallStateUpdatedListener() - 상태 모니터링 종료

2. 프레젠테이션 계층 (ViewModel)

app/src/main/java/com/billcoreatech/daycnt415/presentation/viewmodel/
└── UpdateViewModel.kt
    ├── UpdateViewModel 클래스 (@HiltViewModel)
    ├── UpdateUiState data class
    └── UpdateType enum

주요 메서드:

  • checkForUpdate() - 업데이트 확인 (자동 호출)
  • startImmediateUpdate(activity) - 강제 업데이트 시작
  • startFlexibleUpdate(activity) - 선택적 업데이트 시작
  • installUpdate() - 다운로드 완료 후 설치
  • dismissUpdateDialog() - 다이얼로그 닫기
  • onActivityResumed() / onActivityPaused() - 라이프사이클 관리

3. 프레젠테이션 계층 (UI)

app/src/main/java/com/billcoreatech/daycnt415/presentation/ui/
├── components/
│   └── AppUpdateDialog.kt
│       ├── AppUpdateDialog Composable (메인 다이얼로그)
│       ├── DownloadProgressSection (진행률 표시)
│       ├── CompletedDownloadSection (완료 표시)
│       └── UpdateAvailableBanner (저우선순위 배너)
└── screens/
    └── AppUpdateProvider.kt
        └── AppUpdateProvider Composable (루트 래퍼)

4. DI 설정

app/src/main/java/com/billcoreatech/daycnt415/di/
└── UpdateModule.kt
    ├── AppUpdateManager 제공
    └── AppUpdateService 제공

🔄 데이터 흐름

상태 전이 다이어그램

IDLE
  ↓
CHECK_AVAILABLE
  ├─→ updateAvailable=false → IDLE
  └─→ updateAvailable=true
       ↓
    [USER SEES DIALOG]
       ├─→ IMMEDIATE MODE
       │    ├─→ DOWNLOADING
       │    ├─→ INSTALLED
       │    └─→ (스킵 불가)
       │
       └─→ FLEXIBLE MODE
            ├─→ "나중에" 클릭
            │    └─→ IDLE (다이얼로그 닫음)
            │
            └─→ "지금 업데이트" 클릭
                 ├─→ DOWNLOADING
                 ├─→ DOWNLOADED
                 ├─→ "지금 설치" 클릭
                 ├─→ INSTALLING
                 └─→ INSTALLED

UpdateUiState 필드

data class UpdateUiState(
    val updateAvailable: Boolean = false,           // 업데이트 가능 여부
    val updatePriority: Int = 0,                    // 우선순위 (0-5)
    val clientVersionStalenessDays: Int? = null,    // 버전 경과 일수
    val appUpdateInfo: AppUpdateInfo? = null,       // 업데이트 정보
    val isCheckingForUpdate: Boolean = false,       // 확인 중
    val isUpdating: Boolean = false,                // 업데이트 중
    val isDownloading: Boolean = false,             // 다운로드 중
    val isDownloadCompleted: Boolean = false,       // 다운로드 완료
    val downloadProgress: Int = 0,                  // 진행률 (0-100)
    val shouldShowUpdateDialog: Boolean = false,    // 다이얼로그 표시
    val updateType: UpdateType = UpdateType.NONE,   // IMMEDIATE/FLEXIBLE
    val errorMessage: String? = null                // 에러 메시지
)

🛠️ 통합 방법

1. MainActivity 수정 (이미 완료됨)

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()

        setContent {
            DaycntTheme {
                Surface(modifier = Modifier.fillMaxSize()) {
                    AppUpdateProvider {  // ← 추가됨
                        DayCntNavGraph()
                    }
                }
            }
        }
    }
}

2. MyApplication.kt (이미 구성됨)

@HiltAndroidApp
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        // Hilt 자동으로 초기화됨
    }
}

3. AndroidManifest.xml (변경 없음)

  • 기존 권한 유지
  • Play Core 라이브러리는 매니페스트 병합으로 자동 추가

📊 상태 구독 예시 (필요시 추가 화면에서 사용)

@Composable
fun SomeScreen() {
    val updateViewModel: UpdateViewModel = hiltViewModel()
    val uiState by updateViewModel.uiState.collectAsStateWithLifecycle()

    // 업데이트 진행률 표시
    if (uiState.isDownloading) {
        Text("다운로드: ${uiState.downloadProgress}%")
    }

    // 커스텀 UI 추가 가능
    if (uiState.updateAvailable && !uiState.shouldShowUpdateDialog) {
        UpdateAvailableBanner(
            isVisible = true,
            updatePriority = uiState.updatePriority,
            onUpdateClick = { updateViewModel.startFlexibleUpdate(context as Activity) },
            onDismiss = { updateViewModel.dismissUpdateDialog() }
        )
    }
}

🧪 테스트 방법

Google Play Console 설정

  1. 내부 테스트 트랙 활용:
    • Google Play Console → 출시 → 내부 테스트
    • 새 버전 APK 업로드 (versionCode 증가)
    • 테스트 기기 추가
  2. 테스트 우선순위 설정:
    • Google Play Console → 설정 → 우선순위
    • IMMEDIATE: 우선순위 4 이상
    • FLEXIBLE: 우선순위 1-3
  3. 로컬 테스트 (선택):
    • Play Core 라이브러리의 FakeAppUpdateManager 사용
    • 단위 테스트 작성

테스트 체크리스트

  • IMMEDIATE 모드 업데이트 다이얼로그 표시
  • FLEXIBLE 모드 업데이트 다이얼로그 표시
  • 다운로드 진행률 표시 (FLEXIBLE 모드)
  • "나중에" 버튼 동작 (FLEXIBLE 모드)
  • "지금 설치" 버튼 동작 (FLEXIBLE 모드)
  • 설치 완료 후 자동 재시작
  • 네트워크 오류 처리
  • Activity 회전 시 상태 유지

🔐 에러 처리

UpdateViewModel의 에러 처리 전략

// 1. 업데이트 확인 실패
try {
    val appUpdateInfo = appUpdateService.checkForAppUpdate().await()
} catch (e: Exception) {
    _uiState.value = _uiState.value.copy(
        errorMessage = e.message ?: "Failed to check for update"
    )
}

// 2. 업데이트 시작 실패
if (!success) {
    _uiState.value = _uiState.value.copy(
        errorMessage = "Failed to start update"
    )
}

// 3. 설치 실패
UpdateInstallState.Failed → errorMessage 업데이트

에러 로깅

모든 에러는 android.util.Log로 기록됨:

  • TAG: "UpdateViewModel", "AppUpdateService", "AppUpdateProvider"
  • Level: ERROR (심각) 또는 WARNING (경미)

📈 로깅 포인트

AppUpdateService 로깅

  • checkForAppUpdate(): 업데이트 확인 시작
  • startImmediateUpdateFlow(): 강제 업데이트 시작
  • startFlexibleUpdateFlow(): 선택적 업데이트 시작
  • registerInstallStateUpdatedListener(): 상태 모니터링 시작
  • 설치 상태 변경: PENDING, DOWNLOADING, DOWNLOADED, INSTALLING, INSTALLED, FAILED, CANCELED

UpdateViewModel 로깅

  • 업데이트 확인 결과 (우선순위, 경과일수)
  • 설치 상태 업데이트
  • 에러 발생

AppUpdateProvider 로깅

  • 에러 메시지: "Update error: ..."

🚀 향후 개선 사항

Phase 1 (기본 기능 - 완료)

  • ✅ IMMEDIATE/FLEXIBLE 모드 구현
  • ✅ Compose UI 통합
  • ✅ Hilt DI 설정
  • ✅ 라이프사이클 관리

Phase 2 (선택 사항)

  • Firebase Crashlytics 연동 (업데이트 이벤트 기록)
  • Timber 로깅 통합
  • 업데이트 버전 정보 표시 (현재 vs 최신)
  • 네트워크 재시도 로직
  • 앱 내 알림 (Snackbar) 추가

Phase 3 (고급 기능)

  • 예약된 업데이트 (특정 시간대에만 업데이트)
  • 업데이트 거부 이유 추적
  • A/B 테스트 (업데이트 UI 변형)
  • 단위 테스트 추가 (FakeAppUpdateManager 사용)

📚 참고 자료


🎓 학습 포인트

이 구현에서 다룬 안드로이드 개념

  1. Google Play Core Library: 인앱 업데이트, 인앱 리뷰 등
  2. Compose Lifecycle: DisposableEffect, LaunchedEffect
  3. StateFlow: 반응형 상태 관리
  4. Hilt Dependency Injection: 싱글톤 패턴, 의존성 주입
  5. Coroutines: Task.await() 활용
  6. Activity 라이프사이클: onResume(), onPause()

✅ 완료 체크리스트

  • AppUpdateService 구현
  • UpdateViewModel 구현
  • AppUpdateDialog Composable 구현
  • UpdateModule (Hilt) 구현
  • MainActivity 통합
  • 빌드 성공
  • 문서 작성
  • 테스트 기기에서 테스트 (배포 후)
  • Google Play Console 설정 및 내부 테스트 트랙 배포

📞 문제 해결

Q: "컴파일 에러: Cannot infer type for type parameter"

A: Task 객체의 제네릭 타입을 명시적으로 지정하세요: appUpdateService.checkForAppUpdate().await<AppUpdateInfo>()

Q: "업데이트 다이얼로그가 표시되지 않습니다"

A:

  1. Google Play Console에서 새 버전(높은 versionCode)이 업로드되었는지 확인
  2. 내부 테스트 트랙에서 테스트하고 있는지 확인
  3. 테스트 기기가 해당 트랙에 추가되었는지 확인
  4. 앱을 재시작하고 Play Store 앱 업데이트 확인

Q: "Activity가 null입니다"

A: LocalContext.current가 Activity로 캐스트되지 않을 수 있습니다. @SuppressLint("ContextCastToActivity") 사용하고 null 체크하세요.


작성 일자: 2026-03-10
최종 상태: ✅ 완료 및 빌드 성공

반응형