viewModelScope: 개요, ViewModel 연결 및 Android에서의 작동 방식

저자: IT Sectr 게시일: 2026-06-23 읽는 시간: 9 분

viewModelScope는 androidx.lifecycle 라이브러리의 내장 CoroutineScope로, ViewModel의 생명주기에 연결되어 있으며 ViewModel이 정리되면 자동으로 취소됩니다. Google Android Developers, 2025에 따르면, viewModelScope는 MVVM 아키텍처에서 코루틴을 시작하는 표준 메커니즘으로, 메모리 누수 위험 없이 안전한 비동기 작업을 보장합니다. ViewModelScope는 기본적으로 Dispatchers.Main을 사용하며, 내부의 모든 IO 작업은 withContext를 통해 실행되어야 합니다.

핵심 사항

  • viewModelScope — lifecycle-viewmodel-ktx의 CoroutineScope, ViewModel.onCleared()에서 취소됨
  • Dispatchers.Main — 기본 디스패처, 코루틴 내 UI 업데이트 안전
  • onCleared — viewModelScope의 모든 활성 코루틴을 자동으로 취소하는 콜백
  • clear() vs onCleared() — clear()는 프레임워크가 onCleared 전에 호출하여 scope 취소 보장
  • launch — fire-and-forget 작업을 위해 viewModelScope에서 코루틴을 시작하는 주요 방법

Android에서 viewModelScope란?

viewModelScope는 ViewModel 인터페이스의 확장 속성으로, lifecycle-viewmodel-ktx 라이브러리(버전 2.1.0부터)에 추가되었습니다. ViewModel 생명주기에 연결된 즉시 사용 가능한 CoroutineScope를 제공합니다.

kotlin
// 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 스레드에 있는 경우 추가 디스패칭 없이 메인 스레드에서 코드를 실행합니다.

viewModelScope가 정리 알림을 받는 방법

ViewModel이 생명주기를 벗어나면(Activity 종료 또는 Fragment 제거), 시스템이 clear()를 호출하여 onCleared()를 트리거합니다. 이 콜백에서 viewModelScope는 Job을 취소하여 모든 활성 코루틴을 재귀적으로 종료합니다. 이 메커니즘은 Closeable 인터페이스를 통해 구현되며, scope Job이 자동 닫기를 위한 리소스로 등록됩니다.

viewModelScope 작동 방식: ViewModel 생명주기 연결

viewModelScope의 ViewModel 생명주기 연결 메커니즘은 태깅과 onCleared 콜백을 기반으로 합니다. 단계별로 살펴보겠습니다.

1단계: 첫 번째 접근 시 scope 생성

ViewModel이 viewModelScope.launch { ... }를 실행하면 getter가 JOB_KEY 태그 아래에 이미 scope가 저장되어 있는지 확인합니다. scope가 없으면 새 CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) 인스턴스가 생성됩니다. scope는 내부 태그 맵을 통해 ViewModel 내에 저장됩니다.

2단계: 코루틴 생명주기

viewModelScope.launch 또는 viewModelScope.async를 통해 시작된 모든 코루틴은 scope의 SupervisorJob의 자식이 됩니다. 기본적으로 메인 스레드에서 실행됩니다(withContext를 통해 다른 디스패처가 지정되지 않은 경우). ViewModel이 활성화되어 있는 한 코루틴은 활성, 일시 중단 또는 완료 상태일 수 있습니다.

3단계: onCleared에서 취소

시스템이 ViewModel을 소멸시킬 때 ViewModel.clear()가 호출됩니다. clear() 내부에서는 다음이 발생합니다:

  • 사용자 정의 정리 로직을 위해 onCleared() 호출
  • addCloseable을 통해 등록된 모든 Closeable 리소스 닫기
  • viewModelScope의 Job이 Cancelled 상태로 전환
  • 모든 자식 코루틴 재귀적으로 취소
  • 가비지 컬렉션을 위해 scope에 대한 참조 해제

화면 회전 내성

화면이 회전하면 Activity가 다시 생성되지만 ViewModel은 유지됩니다(ViewModelStoreOwner 덕분에). 이는 viewModelScope가 활성 상태를 유지하고 코루틴이 중단 없이 계속 실행됨을 의미합니다. Activity가 다시 생성된 후에도 동일한 ViewModel(및 동일한 scope)이 재사용되므로 데이터 로딩이 처음부터 다시 시작되지 않습니다.

MVVM 아키텍처에서의 viewModelScope

MVVM(Model-View-ViewModel)은 Google이 권장하는 Android 애플리케이션 아키텍처입니다. viewModelScope는 비동기 작업의 실행자로서 이 아키텍처에서 중심적인 역할을 합니다.

아키텍처 계층에서 viewModelScope의 역할

계층구성 요소viewModelScope의 역할
UIActivity / FragmentViewModel의 StateFlow/LiveData 관찰
ViewModelViewModelviewModelScope를 통해 코루틴 시작, UI 상태 관리
RepositoryRepositoryviewModelScope 코루틴에서 호출되는 suspend 함수 제공
DataDAO / Api실제 요청 실행(Room, Retrofit)

ViewModel은 viewModelScope를 통해 코루틴을 시작하고, 그 내부에서 Repository의 suspend 함수를 호출합니다. 결과는 StateFlow로 변환되어 UI 계층에서 관찰됩니다. 이 설계는 명확한 책임 분리와 각 계층의 독립적인 테스트 가능성을 보장합니다.

왜 ViewModel에서 viewModelScope를 사용하고 Fragment에서는 사용하지 않는가

코루틴이 Fragment에서 시작된다면 화면 회전 시 Fragment 소멸과 함께 취소됩니다. ViewModel은 회전 후에도 유지되므로 해당 scope에서 시작된 코루틴은 계속 실행됩니다. 이것이 데이터 로딩 시 lifecycleScope보다 viewModelScope의 주요 이점입니다.

viewModelScope 사용 예제

Kotlin을 사용한 Android 애플리케이션에서 viewModelScope의 세 가지 실제 시나리오를 살펴보겠습니다.

예제 1: ViewModel 생성 시 데이터 로딩

kotlin
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은 스레드 전환을 신경 쓸 필요가 없습니다.

예제 2: sealed class를 통한 오류 처리

kotlin
sealed class UiState {
    object Loading : UiState()
    data class Success(val data: List<Item>) : UiState()
    data class Error(val message: String) : UiState()
}
kotlin
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를 구독하고 현재 상태에만 반응하며, 이전 회전의 오래된 호출은 무시합니다.

예제 3: 새 요청 시 이전 코루틴 취소

kotlin
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 비활성 후에만 검색이 실행됩니다. 이는 서버 부하를 줄이고 오래된 결과를 방지합니다.

viewModelScope vs lifecycleScope: 언제 무엇을 선택할까

두 scope 모두 AndroidX Lifecycle 라이브러리에서 제공되지만, 서로 다른 생명주기에 연결되어 있습니다. 선택은 작업 유형에 따라 달라집니다.

Scope 비교

특성viewModelScopelifecycleScope
소유자ViewModelLifecycleOwner(Activity/Fragment)
회전 시 취소아니요(ViewModel 유지)예(Activity 재생성)
기본 디스패처Dispatchers.Main.immediateDispatchers.Main.immediate
사용 가능 위치ViewModelActivity, Fragment, Service
일반적인 사용 사례데이터 로딩, 비즈니스 로직UI 상호작용, 애니메이션

Google 권장사항

Google은 모든 데이터 로딩 및 처리 작업에 viewModelScope를 사용할 것을 권장합니다. lifecycleScope는 특정 UI 생명주기 순간에 연결된 작업(예: 첫 화면 표시 시 애니메이션 시작 또는 화면을 벗어날 때 중지되어야 하는 위치 업데이트 구독)에 사용해야 합니다.

viewModelScope 작업 시 흔한 실수

잘 문서화된 Android API에서도 개발자는 전형적인 실수를 합니다. 가장 흔한 네 가지 문제를 살펴보겠습니다.

실수 1: scope 취소 후 UI 업데이트

가장 교묘한 실수는 ViewModel이 정리된 후 StateFlow 또는 LiveData를 업데이트하려는 것입니다. viewModelScope는 onCleared()에서 취소되지만, 코루틴은 실제 취소가 적용되기 전에 코드를 실행할 수 있습니다. 확인을 위해 isActive를 사용하거나 catch 블록 완료에 의존하세요.

실수 2: SupervisorJob을 고려하지 않고 코루틴 시작

viewModelScope는 내부적으로 SupervisorJob을 사용하여 코루틴 간 오류를 격리합니다. 그러나 viewModelScope.launch 내에서 자체 Job()으로 코루틴을 시작하면 해당 코루틴은 SupervisorJob의 자식이 되지만 다른 코루틴의 오류로 인한 취소로부터 보호되지 않습니다.

실수 3: 하나의 scope에 너무 많은 코루틴

viewModelScope에 엄격한 제한은 없지만, 수천 개의 활성 코루틴은 시스템을 느리게 할 수 있습니다. 긴 데이터 목록의 경우 각 항목에 대해 별도의 코루틴을 만드는 대신 Flow를 collectLatest와 함께 사용하세요.

실수 4: viewModelScope 대신 GlobalScope 사용

viewModelScope 대신 실수로 GlobalScope를 가져오면 ViewModel이 정리될 때 코루틴이 취소되지 않습니다. 이는 메모리 누수와 잠재적인 충돌로 이어집니다. 특히 Fragment 하위 클래스에서 코루틴이 viewModelScope를 통해 시작되는지 항상 확인하세요.

자주 묻는 질문

viewModelScope의 기본 디스패처를 변경할 수 있나요?

viewModelScope의 디스패처를 직접 변경할 수 없습니다. Dispatchers.Main.immediate로 하드코딩되어 있습니다. 그러나 코루틴 내에서 withContext를 통해 다른 디스패처로 전환할 수 있습니다. 테스트에서 디스패처를 변경하려면 Rule을 통해 TestDispatcher를 사용하세요.

viewModelScope를 Repository에 어떻게 전달하나요?

scope를 Repository에 전달하지 마세요. 이는 아키텍처 원칙을 위반합니다. Repository는 suspend 함수를 제공해야 하며, ViewModel 자체가 viewModelScope를 통해 코루틴을 관리합니다. Repository가 scope를 필요로 한다면 Clean Architecture를 위해 아키텍처를 재고하세요.

viewModelScope가 SupervisorJob을 사용하는 이유는?

SupervisorJob은 하나의 코루틴에서 예외(예: 여러 독립적인 요청 중 하나의 로딩 오류)가 다른 코루틴을 취소하지 않도록 보장합니다. 이는 서로 다른 화면이 독립적인 데이터를 로드하는 ViewModel 시나리오에 적합합니다.

viewModelScope는 Jetpack Compose에서 사용할 수 있나요?

네, viewModelScope는 UI 유형(View System 또는 Jetpack Compose)에 관계없이 모든 ViewModel에서 사용할 수 있습니다. Compose에서도 코루틴은 viewModelScope를 통해 시작되며, UI 효과에는 LaunchedEffectrememberCoroutineScope가 사용됩니다.

viewModelScope.cancel()을 호출하면 코루틴은 어떻게 되나요?

viewModelScope.cancel()을 호출하면 scope가 즉시 취소되어 모든 활성 코루틴이 CancellationException으로 종료됩니다. 이후 viewModelScope.launch를 호출하면 getter에 대한 다음 접근 시 자동으로 새 scope가 생성됩니다.

요약

  • viewModelScope — ViewModel 생명주기에 연결된 CoroutineScope, onCleared()에서 자동 취소
  • SupervisorJob + Dispatchers.Main — 오류 격리와 안전한 UI 접근을 보장하는 내부 구성
  • 화면 회전 — ViewModel이 유지되므로 viewModelScope의 코루틴이 재시작 없이 계속 실행
  • MVVM 아키텍처 — viewModelScope는 ViewModel 계층에서 비동기 작업의 핵심 요소
  • lifecycleScope — ViewModel이 아닌 Activity/Fragment 생명주기에 연결된 작업을 위한 대안
  • StateFlow — sealed class를 통해 viewModelScope 코루틴에서 UI로 데이터를 전달하는 선호 방식
  • GlobalScope는 위험 — viewModelScope를 GlobalScope로 대체하면 메모리 누수와 앱 충돌 발생

턴키 방식의 모바일 애플리케이션을 개발해 드립니다

IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.

프로젝트 논의

더 읽어보기