|

안드로이드 앱 구조 입문 시리즈 (5) – 화면 상태는 어디에 두는 게 맞을까

안드로이드 앱 구조 입문 시리즈 5편 화면 상태는 어디에 두는 게 맞을까 대표 이미지
화면 상태를 View, ViewModel, SavedStateHandle 기준으로 나눠 설명하는 시리즈 5편

화면 상태는 어디에 두는 게 맞을까라는 질문은 안드로이드 앱 구조를 공부할수록 더 자주 나오게 됩니다. 이번 글에서는 Activity, Fragment, ViewModel 중 무엇이 정답이냐를 먼저 고르기보다, 상태의 종류와 생존 범위를 기준으로 어디에 두는 게 자연스러운지 쉽게 정리해보겠습니다.


먼저 결론부터: 상태는 한 군데에 몰아두는 문제가 아니다

입문자가 가장 많이 하는 실수 중 하나는 상태를 한 군데에만 두려는 것입니다. Fragment에 전부 두거나, 반대로 ViewModel에 전부 올리거나, 상위 Activity가 다 들고 있으려 하면 금방 구조가 무거워집니다.

실제로는 상태를 아래처럼 나눠서 보는 편이 훨씬 자연스럽습니다. 화면을 설명하는 상태, View 자체가 들고 있어도 되는 작은 상태, 그리고 프로세스가 죽어도 복원해야 하는 최소 상태입니다.

  1. 화면을 그리기 위한 화면 수준 상태
  2. View 자체가 들고 있는 작은 상태
  3. 프로세스가 죽어도 복원해야 하는 최소 상태

왜 Activity나 Fragment 안에 다 넣으면 금방 복잡해질까

간단한 검색 화면만 떠올려도 현재 검색어, 로딩 여부, 검색 결과 목록, 에러 메시지, 선택된 필터, 스크롤 위치처럼 서로 성격이 다른 값이 함께 등장합니다. 처음에는 Fragment 안에 모두 두고 싶어지지만, 시간이 조금만 지나도 상태 보관과 화면 갱신과 비동기 흐름 처리가 한곳에 섞이기 시작합니다.

class SearchFragment : Fragment() {

    private var query: String = ""
    private var isLoading: Boolean = false
    private var items: List<String> = emptyList()
    private var errorMessage: String? = null
    private var selectedFilter: String = "recent"

    fun onSearchClicked() {
        isLoading = true
        renderLoading()

        fakeSearch(query, selectedFilter,
            onSuccess = { result ->
                isLoading = false
                items = result
                renderItems(result)
            },
            onError = { message ->
                isLoading = false
                errorMessage = message
                renderError(message)
            }
        )
    }
}

이 구조의 문제는 코드 줄 수보다 책임이 섞이기 시작한다는 점입니다. 상태를 어디에 둘까라는 질문은 사실 책임을 어디에 둘까라는 질문과 거의 같습니다.


상태를 나누는 첫 번째 기준: 화면 상태인가, View 자체 상태인가

제일 먼저 물어볼 질문은 이것입니다. 이 값이 화면 전체를 설명하는 상태인가, 아니면 특정 View 하나가 들고 있어도 되는 작은 상태인가입니다.

  • 화면 수준 상태 예: 로딩 여부, 검색 결과 목록, 에러 메시지, 현재 선택된 필터, 빈 결과 상태
  • View 자체 상태 예: EditText 입력 중인 값, 스크롤 위치, 포커스 여부, 국소적인 펼침/접힘 상태

화면 수준 상태는 특정 버튼 하나의 상태가 아니라 화면 전체가 지금 어떤 의미를 가지는가를 설명합니다. 그래서 이런 값은 보통 ViewModel에 두는 편이 더 자연스럽습니다.

안드로이드 화면 상태를 ViewModel, View, SavedStateHandle로 나누는 판단 흐름도
상태의 성격과 생존 범위로 위치를 판단하면 구조가 훨씬 단순해진다

ViewModel은 어떤 상태에 가장 잘 맞을까

Android Developers의 ViewModel 문서는 ViewModel을 UI 데이터를 lifecycle-aware 하게 관리하는 도구로 설명합니다. 쉽게 말해 화면이 다시 그려져도 이어져야 하는 화면 수준 상태를 두기에 좋은 자리라는 뜻입니다.

data class SearchUiState(
    val query: String = "",
    val isLoading: Boolean = false,
    val items: List<String> = emptyList(),
    val errorMessage: String? = null,
    val selectedFilter: String = "recent"
)
class SearchViewModel : ViewModel() {

    private val _uiState = MutableStateFlow(SearchUiState())
    val uiState: StateFlow<SearchUiState> = _uiState

    fun onQueryChanged(newQuery: String) {
        _uiState.value = _uiState.value.copy(query = newQuery)
    }

    fun onFilterChanged(newFilter: String) {
        _uiState.value = _uiState.value.copy(selectedFilter = newFilter)
    }

    fun onSearchClicked() {
        val current = _uiState.value

        _uiState.value = current.copy(
            isLoading = true,
            errorMessage = null
        )

        fakeSearch(
            query = current.query,
            filter = current.selectedFilter,
            onSuccess = { result ->
                _uiState.value = _uiState.value.copy(
                    isLoading = false,
                    items = result
                )
            },
            onError = { message ->
                _uiState.value = _uiState.value.copy(
                    isLoading = false,
                    errorMessage = message
                )
            }
        )
    }
}

여기서 중요한 것은 최신 문법이 아니라 역할 분리입니다. Fragment는 입력을 전달하고 상태를 그리며, ViewModel은 화면 상태를 들고 흐름을 정리합니다.


그렇다고 모든 값을 ViewModel에 올리면 왜 어색해질까

검색창에 한 글자씩 입력하는 값, 스크롤 위치, 포커스 여부 같은 것까지 모두 ViewModel로 옮기면 구조가 오히려 무거워질 수 있습니다. 이런 값은 때로는 View 자신이 이미 잘 알고 있고, 화면 전체의 핵심 의미와는 거리가 있기 때문입니다.

즉, 화면 전체를 다시 그리는 판단 기준이 되는가를 먼저 봐야 합니다. 그 기준이 약하면 ViewModel로 올리기보다 View 쪽에 두는 편이 더 낫습니다.


화면 상태는 어디에 두는 게 맞을까: EditText 값부터 다시 보자

이 질문은 매우 자주 나오지만 정답은 항상 하나가 아닙니다. 단순 입력 폼이고 아직 제출 전 임시 입력값일 뿐이라면 View 쪽에 남겨도 됩니다. 반대로 그 값이 검색, 필터, 검증 흐름을 바로 바꾸고 화면 의미를 결정한다면 ViewModel 쪽이 더 자연스럽습니다.

  • View에 남겨도 자연스러운 경우: 단순 임시 입력값, 다른 UI 로직을 크게 바꾸지 않는 경우, View 시스템의 상태 복원만으로 충분한 경우
  • ViewModel에 두는 편이 자연스러운 경우: 입력값이 검색/필터/검증 흐름과 직접 연결되는 경우, 화면 상태를 설명하는 핵심 기준인 경우, 재생성 이후에도 같은 화면 의미를 유지해야 하는 경우

중요한 것은 EditText라는 위젯 이름이 아니라 그 값이 화면 구조 안에서 어떤 역할을 하느냐입니다.


구성 변경까지만 버티면 되는 상태와 process death까지 버텨야 하는 상태는 다르다

여기서 두 번째 기준이 나옵니다. 이 상태는 화면 회전 같은 configuration change까지만 버티면 되는가, 아니면 process death 뒤에도 복원해야 하는가입니다. 이 차이를 구분하지 않으면 ViewModel을 만능 저장소처럼 오해하게 됩니다.

Android Developers의 UI states 문서는 process death와 SavedStateHandle을 별도로 다룹니다. 그래서 화면 재생성 동안 이어져야 하는 상태는 ViewModel 후보로 보고, 프로세스 종료 뒤에도 꼭 복원해야 하는 최소 상태는 SavedStateHandle이나 저장 계층을 함께 고려하는 편이 안전합니다.


SavedStateHandle은 언제 떠올리면 좋을까

예를 들어 상세 화면의 itemId, 검색 화면의 마지막 검색어, 현재 선택된 탭 번호처럼 화면을 다시 복구하는 실마리가 되는 값들이 있습니다. 이럴 때는 전체 결과 리스트를 통째로 저장하려 하기보다, 다시 화면을 복원할 최소 키를 남기는 쪽이 보통 더 현실적입니다.

즉, itemId나 query 정도를 SavedStateHandle로 복구하고, 실제 데이터는 다시 로드하는 식이 더 실용적일 때가 많습니다.


Activity, Fragment, ViewModel을 각각 어떻게 보면 좋을까

  • Activity에 두기 좋은 것: 상위 네비게이션 연결, 여러 Fragment를 묶는 큰 흐름 조정, 앱 바깥과 연결된 호스팅 역할
  • Fragment에 두기 좋은 것: View 바인딩, 클릭 리스너 연결, 화면 렌더링, 아주 국소적인 UI 처리
  • ViewModel에 두기 좋은 것: 화면 전체를 설명하는 UI state, 사용자 액션 이후의 처리 흐름, 로딩/성공/실패/빈 상태, 재생성 이후에도 이어져야 하는 상태

결국 ViewModel은 화면을 위한 상태 보관자이지, 화면 객체 자체가 아니다라고 이해하면 방향을 잡기 훨씬 쉽습니다.


한 번에 판단하기 어려울 때 쓰는 4가지 질문

  1. 이 값이 화면 전체를 설명하는가, 아니면 특정 View 하나의 상태에 가까운가
  2. 화면이 다시 만들어져도 유지되어야 하는가
  3. 프로세스가 죽어도 꼭 복원해야 하는가
  4. 이 값을 바꾸는 로직이 화면 의미를 크게 바꾸는가

이 질문에 대한 답이 분명해지면 판단도 쉬워집니다. 화면 전체 의미를 설명하면 ViewModel 쪽이 유력하고, 특정 View의 일시적 상태라면 View 또는 saved state 쪽이 더 유력합니다. process death 복원이 중요하다면 SavedStateHandle이나 저장 계층까지 같이 검토하면 됩니다.


마무리

상태를 전부 Fragment에 두면 화면 코드가 금방 무거워지고, 반대로 상태를 전부 ViewModel에 올리면 작은 UI 상태까지 과하게 구조화하게 될 수 있습니다. 더 좋은 방법은 상태를 종류별로 나누는 것입니다.

정리하면 화면 전체를 설명하는 상태는 ViewModel 쪽으로, View 자체가 잘 다루는 작은 상태는 View 쪽으로, 프로세스 종료 이후에도 꼭 복원해야 하는 최소 상태는 SavedStateHandle이나 저장 계층까지 함께 보는 편이 좋습니다.

이전 흐름을 먼저 정리하고 싶다면 생명주기를 알아야 설계가 쉬워지는 이유ViewModel은 왜 필요할까를 먼저 함께 읽어보면 좋습니다. 공식 기준은 Android Developers의 ViewModel overviewUI states / saving states 문서를 함께 보면 더 분명합니다. 다음 글에서는 여기서 한 걸음 더 나아가 안드로이드에서 비동기 작업은 구조와 어떻게 연결될까를 다뤄보겠습니다.

함께보면 좋은 글