2026/05/08

오늘의 이야기

앱 업데이트



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
최종 상태: ✅ 완료 및 빌드 성공





댓글 없음:

댓글 쓰기

오늘의 이야기

🔍 프로젝트 진단 | Health501 아키텍처 & 코드 품질 개선 로드맵 샘플이미지   📊 개요 (Executive Summary) 작업 일자: 2025-1...