
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 설정
- 내부 테스트 트랙 활용:
- Google Play Console → 출시 → 내부 테스트
- 새 버전 APK 업로드 (versionCode 증가)
- 테스트 기기 추가
- 테스트 우선순위 설정:
- Google Play Console → 설정 → 우선순위
- IMMEDIATE: 우선순위 4 이상
- FLEXIBLE: 우선순위 1-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 사용)
📚 참고 자료
🎓 학습 포인트
이 구현에서 다룬 안드로이드 개념
- Google Play Core Library: 인앱 업데이트, 인앱 리뷰 등
- Compose Lifecycle: DisposableEffect, LaunchedEffect
- StateFlow: 반응형 상태 관리
- Hilt Dependency Injection: 싱글톤 패턴, 의존성 주입
- Coroutines: Task.await() 활용
- 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:
- Google Play Console에서 새 버전(높은 versionCode)이 업로드되었는지 확인
- 내부 테스트 트랙에서 테스트하고 있는지 확인
- 테스트 기기가 해당 트랙에 추가되었는지 확인
- 앱을 재시작하고 Play Store 앱 업데이트 확인
Q: "Activity가 null입니다"
A: LocalContext.current가 Activity로 캐스트되지 않을 수 있습니다. @SuppressLint("ContextCastToActivity") 사용하고 null 체크하세요.
작성 일자: 2026-03-10
최종 상태: ✅ 완료 및 빌드 성공
반응형
'모바일 앱(안드로이드)' 카테고리의 다른 글
| 휴게시간 앱 현대화 결과 보기. (2) | 2026.03.10 |
|---|---|
| Google Play Billing Library 업데이트 (7.x → 8.3.0) (0) | 2026.03.06 |
| 휴게시간 앱 화면 xml 에서 compose 로 이전 하기 (0) | 2026.03.04 |
| 휴게시간 (앱) Android Kotlin 프로젝트 현대화 계획 (1) | 2026.02.25 |
| # 프레시틱 (Freshtic) 개발 작업 히스토리 추가. (0) | 2026.02.24 |