viewModelScope는 androidx.lifecycle 라이브러리의 내장 CoroutineScope로, ViewModel의 생명주기에 연결되어 있으며 ViewModel이 정리되면 자동으로 취소됩니다. Google Android Developers, 2025에 따르면, viewModelScope는 MVVM 아키텍처에서 코루틴을 시작하는 표준 메커니즘으로, 메모리 누수 위험 없이 안전한 비동기 작업을 보장합니다. ViewModelScope는 기본적으로 Dispatchers.Main을 사용하며, 내부의 모든 IO 작업은 withContext를 통해 실행되어야 합니다.
핵심 사항
viewModelScope는 ViewModel 인터페이스의 확장 속성으로, lifecycle-viewmodel-ktx 라이브러리(버전 2.1.0부터)에 추가되었습니다. ViewModel 생명주기에 연결된 즉시 사용 가능한 CoroutineScope를 제공합니다.
// Internal structure (simplified)
val ViewModel.viewModelScope: CoroutineScope
get() {
val scope = this.getTag(JOB_KEY)
if (scope != null) return scope
return CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
.also { setTag(JOB_KEY, it) }
}
scope는 첫 번째 접근 시 지연(lazy) 생성되고 setTag를 통해 캐시됩니다. SupervisorJob을 사용하므로 하나의 자식 코루틴에서 예외가 발생해도 다른 코루틴이 취소되지 않습니다. 기본 디스패처는 Dispatchers.Main.immediate로, 이미 Main 스레드에 있는 경우 추가 디스패칭 없이 메인 스레드에서 코드를 실행합니다.
ViewModel이 생명주기를 벗어나면(Activity 종료 또는 Fragment 제거), 시스템이 clear()를 호출하여 onCleared()를 트리거합니다. 이 콜백에서 viewModelScope는 Job을 취소하여 모든 활성 코루틴을 재귀적으로 종료합니다. 이 메커니즘은 Closeable 인터페이스를 통해 구현되며, scope Job이 자동 닫기를 위한 리소스로 등록됩니다.
viewModelScope의 ViewModel 생명주기 연결 메커니즘은 태깅과 onCleared 콜백을 기반으로 합니다. 단계별로 살펴보겠습니다.
ViewModel이 viewModelScope.launch { ... }를 실행하면 getter가 JOB_KEY 태그 아래에 이미 scope가 저장되어 있는지 확인합니다. scope가 없으면 새 CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) 인스턴스가 생성됩니다. scope는 내부 태그 맵을 통해 ViewModel 내에 저장됩니다.
viewModelScope.launch 또는 viewModelScope.async를 통해 시작된 모든 코루틴은 scope의 SupervisorJob의 자식이 됩니다. 기본적으로 메인 스레드에서 실행됩니다(withContext를 통해 다른 디스패처가 지정되지 않은 경우). ViewModel이 활성화되어 있는 한 코루틴은 활성, 일시 중단 또는 완료 상태일 수 있습니다.
시스템이 ViewModel을 소멸시킬 때 ViewModel.clear()가 호출됩니다. clear() 내부에서는 다음이 발생합니다:
화면이 회전하면 Activity가 다시 생성되지만 ViewModel은 유지됩니다(ViewModelStoreOwner 덕분에). 이는 viewModelScope가 활성 상태를 유지하고 코루틴이 중단 없이 계속 실행됨을 의미합니다. Activity가 다시 생성된 후에도 동일한 ViewModel(및 동일한 scope)이 재사용되므로 데이터 로딩이 처음부터 다시 시작되지 않습니다.
MVVM(Model-View-ViewModel)은 Google이 권장하는 Android 애플리케이션 아키텍처입니다. viewModelScope는 비동기 작업의 실행자로서 이 아키텍처에서 중심적인 역할을 합니다.
| 계층 | 구성 요소 | viewModelScope의 역할 |
|---|---|---|
| UI | Activity / Fragment | ViewModel의 StateFlow/LiveData 관찰 |
| ViewModel | ViewModel | viewModelScope를 통해 코루틴 시작, UI 상태 관리 |
| Repository | Repository | viewModelScope 코루틴에서 호출되는 suspend 함수 제공 |
| Data | DAO / Api | 실제 요청 실행(Room, Retrofit) |
ViewModel은 viewModelScope를 통해 코루틴을 시작하고, 그 내부에서 Repository의 suspend 함수를 호출합니다. 결과는 StateFlow로 변환되어 UI 계층에서 관찰됩니다. 이 설계는 명확한 책임 분리와 각 계층의 독립적인 테스트 가능성을 보장합니다.
코루틴이 Fragment에서 시작된다면 화면 회전 시 Fragment 소멸과 함께 취소됩니다. ViewModel은 회전 후에도 유지되므로 해당 scope에서 시작된 코루틴은 계속 실행됩니다. 이것이 데이터 로딩 시 lifecycleScope보다 viewModelScope의 주요 이점입니다.
Kotlin을 사용한 Android 애플리케이션에서 viewModelScope의 세 가지 실제 시나리오를 살펴보겠습니다.
class ProfileViewModel(
private val repo: ProfileRepository
) : ViewModel() {
private val _profile = MutableStateFlow<Profile?>(null)
val profile: StateFlow<Profile?> = _profile
init {
loadProfile()
}
private fun loadProfile() {
viewModelScope.launch {
val result = repo.getProfile()
_profile.value = result
}
}
}
init 블록에서 프로필 로딩이 즉시 시작됩니다. 코루틴은 기본적으로 메인 스레드에서 실행됩니다. 리포지토리는 자체 suspend 함수 내에서 네트워크 요청에 withContext(Dispatchers.IO)를 사용하므로 ViewModel은 스레드 전환을 신경 쓸 필요가 없습니다.
sealed class UiState {
object Loading : UiState()
data class Success(val data: List<Item>) : UiState()
data class Error(val message: String) : UiState()
}
fun fetchItems() {
_state.value = UiState.Loading
viewModelScope.launch {
try {
val items = repo.getItems()
_state.value = UiState.Success(items)
} catch (e: Exception) {
_state.value = UiState.Error(e.message ?: "Unknown error")
}
}
}
UI 상태는 sealed class UiState를 통해 설명됩니다. ViewModel은 각 변경 시 상태를 업데이트합니다. Fragment는 StateFlow를 구독하고 현재 상태에만 반응하며, 이전 회전의 오래된 호출은 무시합니다.
private var searchJob: Job? = null
fun search(query: String) {
searchJob?.cancel()
searchJob = viewModelScope.launch {
delay(300)
val results = repo.search(query)
_searchResults.value = results
}
}
각 새 검색 쿼리마다 이전 코루틴이 취소됩니다. delay(300)은 디바운스를 구현하여 300ms 비활성 후에만 검색이 실행됩니다. 이는 서버 부하를 줄이고 오래된 결과를 방지합니다.
두 scope 모두 AndroidX Lifecycle 라이브러리에서 제공되지만, 서로 다른 생명주기에 연결되어 있습니다. 선택은 작업 유형에 따라 달라집니다.
| 특성 | viewModelScope | lifecycleScope |
|---|---|---|
| 소유자 | ViewModel | LifecycleOwner(Activity/Fragment) |
| 회전 시 취소 | 아니요(ViewModel 유지) | 예(Activity 재생성) |
| 기본 디스패처 | Dispatchers.Main.immediate | Dispatchers.Main.immediate |
| 사용 가능 위치 | ViewModel | Activity, Fragment, Service |
| 일반적인 사용 사례 | 데이터 로딩, 비즈니스 로직 | UI 상호작용, 애니메이션 |
Google은 모든 데이터 로딩 및 처리 작업에 viewModelScope를 사용할 것을 권장합니다. lifecycleScope는 특정 UI 생명주기 순간에 연결된 작업(예: 첫 화면 표시 시 애니메이션 시작 또는 화면을 벗어날 때 중지되어야 하는 위치 업데이트 구독)에 사용해야 합니다.
잘 문서화된 Android API에서도 개발자는 전형적인 실수를 합니다. 가장 흔한 네 가지 문제를 살펴보겠습니다.
가장 교묘한 실수는 ViewModel이 정리된 후 StateFlow 또는 LiveData를 업데이트하려는 것입니다. viewModelScope는 onCleared()에서 취소되지만, 코루틴은 실제 취소가 적용되기 전에 코드를 실행할 수 있습니다. 확인을 위해 isActive를 사용하거나 catch 블록 완료에 의존하세요.
viewModelScope는 내부적으로 SupervisorJob을 사용하여 코루틴 간 오류를 격리합니다. 그러나 viewModelScope.launch 내에서 자체 Job()으로 코루틴을 시작하면 해당 코루틴은 SupervisorJob의 자식이 되지만 다른 코루틴의 오류로 인한 취소로부터 보호되지 않습니다.
viewModelScope에 엄격한 제한은 없지만, 수천 개의 활성 코루틴은 시스템을 느리게 할 수 있습니다. 긴 데이터 목록의 경우 각 항목에 대해 별도의 코루틴을 만드는 대신 Flow를 collectLatest와 함께 사용하세요.
viewModelScope 대신 실수로 GlobalScope를 가져오면 ViewModel이 정리될 때 코루틴이 취소되지 않습니다. 이는 메모리 누수와 잠재적인 충돌로 이어집니다. 특히 Fragment 하위 클래스에서 코루틴이 viewModelScope를 통해 시작되는지 항상 확인하세요.
자주 묻는 질문
viewModelScope의 디스패처를 직접 변경할 수 없습니다. Dispatchers.Main.immediate로 하드코딩되어 있습니다. 그러나 코루틴 내에서 withContext를 통해 다른 디스패처로 전환할 수 있습니다. 테스트에서 디스패처를 변경하려면 Rule을 통해 TestDispatcher를 사용하세요.
scope를 Repository에 전달하지 마세요. 이는 아키텍처 원칙을 위반합니다. Repository는 suspend 함수를 제공해야 하며, ViewModel 자체가 viewModelScope를 통해 코루틴을 관리합니다. Repository가 scope를 필요로 한다면 Clean Architecture를 위해 아키텍처를 재고하세요.
SupervisorJob은 하나의 코루틴에서 예외(예: 여러 독립적인 요청 중 하나의 로딩 오류)가 다른 코루틴을 취소하지 않도록 보장합니다. 이는 서로 다른 화면이 독립적인 데이터를 로드하는 ViewModel 시나리오에 적합합니다.
네, viewModelScope는 UI 유형(View System 또는 Jetpack Compose)에 관계없이 모든 ViewModel에서 사용할 수 있습니다. Compose에서도 코루틴은 viewModelScope를 통해 시작되며, UI 효과에는 LaunchedEffect와 rememberCoroutineScope가 사용됩니다.
viewModelScope.cancel()을 호출하면 scope가 즉시 취소되어 모든 활성 코루틴이 CancellationException으로 종료됩니다. 이후 viewModelScope.launch를 호출하면 getter에 대한 다음 접근 시 자동으로 새 scope가 생성됩니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.