|

ViewModelStoreOwner란 무엇일까: ViewModel 범위를 정하는 기준

ViewModelStoreOwner 대표 이미지
같은 ViewModel 클래스도 어느 owner에 묶이느냐에 따라 공유 범위가 달라진다

ViewModelStoreOwner가 어려운 이유는 보통 직접 이름을 마주치기 전에 viewModels(), activityViewModels(), hiltViewModel() 같은 편한 API부터 만나기 때문입니다. 하지만 범위를 이해하려면 결국 누가 ViewModel을 보관하는가로 돌아와야 합니다.

결론부터 말하면, 같은 ViewModel 클래스라도 Fragment owner를 쓰면 화면 전용이 되고, Activity owner를 쓰면 같은 Activity 아래에서 공유되며, NavBackStackEntry owner를 쓰면 특정 navigation graph 범위에서 공유됩니다. 이번 글에서는 이 차이를 실무 예시 중심으로 풀어보겠습니다.

Fragment, Activity, NavBackStackEntry owner 범위를 비교한 카드
먼저 owner 범위를 한눈에 잡는 요약 카드

기본 개념은 ViewModelStoreOwner API reference, 범위별 API 감각은 ViewModel APIs, 생존과 state holder 관점은 ViewModel overview, navigation graph 범위 공유는 Navigation component 가이드에서 확인할 수 있습니다.

ViewModelStoreOwner는 무엇인가

이름을 그대로 풀면 ViewModelStore를 가진 주체입니다. ViewModelStore는 ViewModel 인스턴스를 보관하는 창고처럼 생각하면 쉽습니다. 즉 ViewModelStoreOwner는 그 창고의 주인입니다.

그래서 범위가 달라진다는 말은 ViewModel 코드가 달라진다는 뜻이 아니라, 같은 클래스를 어느 창고에 넣느냐가 달라진다는 뜻입니다. 같은 owner를 보면 같은 인스턴스를 다시 쓰고, owner가 달라지면 다른 인스턴스를 보게 됩니다.


왜 owner가 핵심일까

공식 문서는 ViewModel이 configuration change를 넘어 UI 관련 데이터를 유지하는 state holder라고 설명합니다. 하지만 실무에서 더 중요한 것은, 그 유지가 항상 아무 데서나 일어나는 것이 아니라 특정 owner의 lifetime 안에서 일어난다는 점입니다.

예를 들어 같은 UserViewModel이라도 Fragment의 viewModels(), Fragment의 activityViewModels(), 그리고 navGraphViewModels()는 서로 다른 owner를 볼 수 있습니다. 그래서 결과도 달라집니다.


Fragment owner: 화면 전용 범위

class DetailFragment : Fragment() {
    private val viewModel: DetailViewModel by viewModels()
}

이 패턴은 현재 Fragment 자신을 owner로 사용합니다. 그래서 화면 전용 입력 상태, 로딩 상태, 화면 내부 필터처럼 이 화면 안에서만 의미가 있는 값에 잘 맞습니다.

중요한 점은 같은 Activity 아래 있어도 sibling Fragment끼리 자동 공유되지 않는다는 것입니다. 기본 owner는 각 Fragment 자신이기 때문입니다.


Activity owner: 여러 화면 공유 범위

class ListFragment : Fragment() {
    private val sharedViewModel: SharedFilterViewModel by activityViewModels()
}

class DetailFragment : Fragment() {
    private val sharedViewModel: SharedFilterViewModel by activityViewModels()
}

이 경우 두 Fragment는 같은 Activity의 ViewModelStore를 봅니다. 그래서 마스터-디테일 화면, 한 Activity 아래 여러 Fragment가 함께 보는 임시 편집 상태, 바텀시트와 본문이 같이 보는 공통 UI state 같은 장면에서 자연스럽습니다.

다만 Activity 범위는 생각보다 넓습니다. 그래서 단지 공유가 가능하다는 이유만으로 Activity owner를 고르면, 사실은 특정 flow에만 필요한 상태가 Activity 전체 lifetime으로 퍼질 수 있습니다.


Parent Fragment owner는 언제 쓸까

class ChildFragment : Fragment() {
    private val parentViewModel: EditorViewModel by viewModels(
        ownerProducer = { requireParentFragment() }
    )
}

Activity 전체는 너무 넓고 Fragment 하나는 너무 좁을 때, parent Fragment owner는 좋은 절충안이 됩니다. 부모 Fragment가 사라지면 함께 정리되고, 그 아래 child Fragment끼리는 자연스럽게 공유할 수 있습니다.


NavBackStackEntry owner 범위

class StepTwoFragment : Fragment() {
    private val checkoutViewModel: CheckoutViewModel by navGraphViewModels(R.id.checkout_graph)
}

Navigation 공식 문서는 NavBackStackEntry가 Lifecycle, ViewModelStore, SavedStateRegistry를 제공한다고 설명합니다. 즉 특정 navigation graph 자체가 owner가 될 수 있다는 뜻입니다.

이 패턴은 주문, 결제, 회원가입, 편집 플로우처럼 여러 destination이 하나의 흐름을 공유할 때 특히 유용합니다. Activity 전체보다 좁고 Fragment 하나보다 넓은 범위를 만들 수 있기 때문입니다.

Fragment owner, Activity owner, graph owner 선택 기준 카드
범위를 넓히기 전에 상태의 실제 lifetime을 먼저 본다

Compose에서도 원리는 같다

Compose의 viewModel()hiltViewModel()도 결국 현재 LocalViewModelStoreOwner 또는 전달한 custom owner를 기준으로 동작합니다. API 모양만 다를 뿐, 범위를 결정하는 핵심은 여전히 owner입니다.

@Composable
fun ProfileRoute(
    viewModel: ProfileViewModel = hiltViewModel()
) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    ProfileScreen(uiState = uiState)
}

보통은 현재 destination owner로 충분하지만, 여러 composable destination이 같은 graph 범위의 ViewModel을 공유해야 한다면 결국 Fragment 때와 같은 질문이 다시 나옵니다. 기본 owner로 충분한가, 더 넓은 graph owner가 필요한가, 오히려 너무 넓어진 것은 아닌가를 봐야 합니다.


activityViewModels vs navGraphViewModels

  • 같은 Activity 아래 여러 화면이 꽤 넓게 공통 상태를 보면 activityViewModels가 자연스럽다.
  • 특정 flow 안에서만 상태가 의미 있고 graph pop과 함께 정리되는 편이 맞다면 navGraphViewModels가 더 정확하다.
  • 실무에서는 activityViewModels가 더 익숙하지만, 많은 shared state는 사실 Activity 전체가 아니라 특정 사용자 흐름 범위에 더 가깝다.

즉 편한 API보다 먼저 상태의 실제 lifetime을 묻는 습관이 중요합니다. 이 감각은 single activity 구조 글과도 자연스럽게 이어집니다.


SavedStateHandle과 함께 보기

ViewModelStoreOwner를 이해하면 SavedStateHandle도 덜 헷갈립니다. 같은 owner 범위에서 만들어진 ViewModel은 그 범위 기준으로 복원 키와 상태를 함께 다루게 되기 때문입니다. navigation argument, SavedStateHandle, shared ViewModel 범위가 같은 흐름 안에서 맞물리는 구조가 실무에서 특히 강합니다.

관련해서는 SavedStateHandle과 navigation argument 글, 안드로이드에서 ViewModel이 왜 필요한가를 같이 보면 연결이 더 선명해집니다.


자주 하는 오해 5가지

  1. 같은 ViewModel 클래스면 자동으로 공유된다 → 아니다. 같은 owner를 써야 같은 인스턴스를 본다.
  2. activityViewModels가 shared ViewModel의 정답이다 → 아니다. 너무 넓을 수 있고 graph 범위가 더 정확할 때가 많다.
  3. Fragment의 viewModels()도 같은 Activity 아래면 공유된다 → 아니다. 기본은 각 Fragment owner다.
  4. Compose에서는 owner를 신경 쓸 필요가 없다 → 아니다. 기본 owner가 숨겨져 있을 뿐 범위 판단은 여전히 중요하다.
  5. ViewModelStoreOwner가 있으면 process death도 다 해결된다 → 아니다. owner는 범위 기준이고 복원 전략은 SavedStateHandle과 데이터 재로딩까지 같이 봐야 한다.

빠른 체크리스트

  • 이 상태는 화면 하나만 보면 충분한가
  • 같은 Activity의 여러 화면이 정말 같이 봐야 하는가
  • 사실 Activity 전체가 아니라 특정 navigation flow만 공유하면 되는가
  • owner lifetime을 한 문장으로 설명할 수 있는가
  • 이 상태가 사라져야 하는 시점이 Fragment 종료인지, graph 종료인지, Activity 종료인지 분명한가

정리

ViewModelStoreOwner는 낯선 내부 용어처럼 보이지만, 실무에서는 ViewModel 공유 범위의 진짜 주인입니다. Fragment owner면 화면 전용, Activity owner면 같은 Activity 아래 공유, NavBackStackEntry owner면 특정 graph 범위 공유라고 정리하면 감각이 빠르게 잡힙니다.

한 문장으로 끝내면 이렇습니다. ViewModelStoreOwner는 ViewModel을 어디까지 함께 쓰고, 언제 함께 버릴지 결정하는 범위의 기준입니다.

외부 기준으로는 ViewModelStoreOwner API reference, ViewModel APIs, ViewModel overview, Navigation component programmatic guide를 함께 보면 가장 단단합니다.

함께보면 좋은 글