UI 레이어

UI의 역할은 화면에 애플리케이션 데이터를 표시하고 사용자 상호작용의 기본 지점으로서의 역할을 수행하는 것 입니다. 사용자 상호작용(예: 버튼 누르기) 또는 외부 입력(예: 네트워크 응답)으로 인해 데이터가 변할 때마다 변경사항을 반영하도록 UI가 업데이트되어야 합니다. 사실상 UI는 데이터 레이어에서 가져온 애플리케이션 상태를 시각적으로 나타냅니다.

하지만 일반적으로 데이터 레이어에서 가져오는 애플리케이션 데이터는 표시해야 하는 정보와 다른 형식입니다. 예를 들어 UI용으로 데이터의 일부만 필요하거나 사용자에게 관련성 있는 정보를 표시하기 위해 서로 다른 두 데이터 소스를 병합해야 할 수도 있습니다. 적용하는 로직과 관계없이 완전히 렌더링하는 데 필요한 모든 정보를 UI에 전달해야 합니다. UI 레이어는 애플리케이션 데이터 변경사항을 UI가 표시할 수 있는 형식으로 변환한 후에 표시하는 파이프라인입니다.

일반 아키텍처에서 UI 레이어의 UI 요소는 상태 홀더에 종속되며, 상태 홀더는 데이터 레이어의 클래스 또는 선택적 도메인 레이어의 클래스에 종속됩니다.
그림 1. 앱 아키텍처에서 UI 레이어의 역할

기본 우수사례

사용자가 읽을 수 있도록 뉴스 기사를 가져오는 앱이 있다고 가정하겠습니다. 앱에는 읽을 수 있는 기사를 표시하는 기사 화면이 있으며, 로그인한 사용자는 눈에 띄는 기사를 북마크할 수 있습니다. 어느 때든 많은 기사가 있을 수 있으므로 독자는 카테고리별로 기사를 둘러볼 수 있어야 합니다. 요약하면 앱에서 사용자는 다음 작업을 할 수 있습니다.

  • 읽을 수 있는 기사 보기
  • 카테고리별로 기사 둘러보기
  • 로그인하여 특정 기사 북마크
  • 자격이 있는 경우 일부 프리미엄 기능에 액세스
그림 2. UI 우수사례를 보여주는 샘플 뉴스 앱

다음 섹션에서는 이 예를 우수사례로 사용하여 단방향 데이터 흐름의 원칙을 소개하고 이러한 원칙이 UI 레이어의 앱 아키텍처와 관련하여 해결할 수 있는 문제를 설명합니다.

UI 레이어 아키텍처

UI라는 용어는 사용하는 API(뷰 또는 Jetpack Compose)와 관계없이 데이터를 표시하는 활동 및 프래그먼트와 같은 UI 요소를 가리킵니다. 데이터 레이어의 역할은 앱 데이터를 보유하고 관리하며 앱 데이터에 액세스할 권한을 제공하는 것이므로 UI 레이어에서 다음 단계를 실행해야 합니다.

  1. 앱 데이터를 사용하고 UI에서 쉽게 렌더링할 수 있는 데이터로 변환합니다.
  2. UI 렌더링 가능 데이터를 사용하고 사용자에게 표시할 UI 요소로 변환합니다.
  3. 이렇게 조합된 UI 요소의 사용자 입력 이벤트를 사용하고 입력 이벤트의 결과를 필요에 따라 UI 데이터에 반영합니다.
  4. 1~3단계를 필요한 만큼 반복합니다.

이 가이드의 나머지 부분에서는 이러한 단계를 실행하는 UI 레이어를 구현하는 방법을 설명합니다. 특히 이 가이드에서는 다음과 같은 작업과 개념을 설명합니다.

  • UI 상태를 정의하는 방법
  • UI 상태를 생성하고 관리하기 위한 단방향 데이터 흐름(UDF)
  • UDF 원칙에 따라 관찰 가능한 데이터 유형으로 UI 상태를 노출하는 방법
  • 관찰 가능한 UI 상태를 사용하는 UI를 구현하는 방법

가장 기본적인 개념은 UI 상태의 정의입니다.

UI 상태 정의

이전에 설명한 우수사례를 참고하세요. 간단히 말해 UI에는 각 기사의 일부 메타데이터와 함께 기사 목록이 표시됩니다. 앱에서 사용자에게 표시하는 이 정보가 UI 상태입니다.

즉, 사용자가 보는 항목이 UI라면 UI 상태는 앱에서 사용자가 봐야 한다고 지정하는 항목입니다. 동전의 양면과 마찬가지로 UI는 UI 상태를 시각적으로 나타냅니다. UI 상태가 변경되면 변경사항이 즉시 UI에 반영됩니다.

UI는 화면에 있는 UI 요소와 UI 상태를 결합한 결과입니다.
그림 3. UI는 화면에 있는 UI 요소와 UI 상태를 결합한 결과

우수사례를 살펴보겠습니다. 뉴스 앱의 요구사항을 충족하기 위해 UI를 완전히 렌더링하는 데 필요한 정보를 다음과 같이 정의된 NewsUiState 데이터 클래스에 캡슐화할 수 있습니다.

data class NewsUiState(
    val isSignedIn: Boolean = false,
    val isPremium: Boolean = false,
    val newsItems: List<NewsItemUiState> = listOf(),
    val userMessages: List<Message> = listOf()
)

data class NewsItemUiState(
    val title: String,
    val body: String,
    val bookmarked: Boolean = false,
    ...
)

불변성

위 예에서 UI 상태 정의는 변경할 수 없습니다. 불변성의 주요 이점은 변경 불가능한 객체가 순간의 애플리케이션 상태를 보장한다는 점입니다. 덕분에 UI는 상태를 읽고 이에 따라 UI 요소를 업데이트하는 한 가지 역할에 집중할 수 있습니다. 따라서 UI 자체가 데이터의 유일한 소스인 경우를 제외하고 UI에서 UI 상태를 직접 수정해서는 안 됩니다. 이 원칙을 위반하면 동일한 정보가 여러 정보 소스에서 비롯되어 데이터 불일치와 미세한 버그가 발생합니다.

예를 들어 우수사례에서 UI 상태의 NewsItemUiState 객체에 있는 bookmarked 플래그가 Activity 클래스에서 업데이트되면 이 플래그는 북마크된 기사 상태의 소스로서 데이터 레이어와 경합합니다. 변경 불가능한 데이터 클래스는 이러한 종류의 안티패턴을 방지하는 데 매우 유용합니다.

이 가이드의 이름 지정 규칙

이 가이드에서는 화면의 기능이나 묘사되는 화면의 부분에 따라 UI 상태 클래스의 이름을 지정합니다. 규칙은 다음과 같습니다.

기능 + UiState

예를 들어 뉴스를 표시하는 화면의 상태는 NewsUiState이고 뉴스 항목 목록에 있는 뉴스 항목의 상태는 NewsItemUiState일 수 있습니다.

단방향 데이터 흐름으로 상태 관리

이전 섹션에서는 UI 상태가 UI 렌더링에 필요한 세부정보가 포함된 변경 불가능한 스냅샷임을 확인했습니다. 하지만 앱 데이터의 동적 특성에 따라 상태는 시간이 지나면서 변경될 수 있으며 이는 앱을 채우는 데 사용되는 기본 데이터를 수정하는 사용자 상호작용이나 기타 이벤트로 인해 발생하기도 합니다.

여기에서는 중재 요소가 각 이벤트에 적용할 로직을 정의하고 UI 상태를 만들기 위해 지원 데이터 소스에 필요한 변환을 실행하여 상호작용을 처리한다는 이점이 있을 수 있습니다. 상호작용과 이에 따른 로직이 UI 자체에 포함될 수도 있지만 UI가 이름에서 알 수 있는 것 이상의 역할(예: 데이터 소유자, 생성자, 변환자 등)을 담당하기 시작하면 빠르게 복잡해질 수 있습니다. 이렇게 복잡해지면 결과 코드가 뚜렷한 경계 없이 긴밀하게 결합된 혼합체가 되므로 테스트 가능 여부에 영향을 미칠 수 있습니다. 궁극적으로 UI에 주는 부담을 줄여야 합니다. UI 상태가 매우 단순하지 않은 이상 UI의 역할은 오직 UI 상태를 사용 및 표시하는 것이어야 합니다.

이 섹션에서는 역할을 원활하게 분리하는 데 도움이 되는 아키텍처 패턴인 단방향 데이터 흐름(UDF)을 설명합니다.

상태 홀더

UI 상태를 생성하는 역할을 담당하고 생성 작업에 필요한 로직을 포함하는 클래스를 상태 홀더라고 합니다. 상태 홀더의 크기는 하단 앱 바와 같은 단일 위젯부터 전체 화면이나 탐색 대상에 이르기까지 관리 대상 UI 요소의 범위에 따라 다양합니다.

전체 화면이나 탐색 대상의 경우 일반적인 구현은 ViewModel의 인스턴스이지만 애플리케이션의 요구사항에 따라 간단한 클래스로도 충분할 수 있습니다. 예를 들어 우수사례의 뉴스 앱은 NewsViewModel 클래스를 상태 홀더로 사용하여 섹션에 표시되는 화면의 UI 상태를 생성합니다.

UI와 상태 생성자 간의 상호 종속을 모델링하는 방법은 다양합니다. 하지만 UI와 ViewModel 클래스 사이의 상호작용은 대체로 이벤트 입력과 입력의 후속 상태인 출력으로 간주될 수 있으므로 관계는 다음 다이어그램과 같습니다.

애플리케이션 데이터 흐름은 데이터 레이어에서 ViewModel로 향합니다. UI 상태 흐름은 ViewModel에서 UI 요소로 향하고 이벤트 흐름은 UI 요소에서 다시 ViewModel로 향합니다.
그림 4. 앱 아키텍처에서 UDF의 작동 방식을 보여주는 다이어그램

상태가 아래로 향하고 이벤트는 위로 향하는 패턴을 단방향 데이터 흐름(UDF)이라고 합니다. 이 패턴이 앱 아키텍처에 미치는 영향은 다음과 같습니다.

  • ViewModel이 UI에 사용될 상태를 보유하고 노출합니다. UI 상태는 ViewModel에 의해 변환된 애플리케이션 데이터입니다.
  • UI가 ViewModel에 사용자 이벤트를 알립니다.
  • ViewModel이 사용자 작업을 처리하고 상태를 업데이트합니다.
  • 업데이트된 상태가 렌더링할 UI에 다시 제공됩니다.
  • 상태 변경을 야기하는 모든 이벤트에 위의 작업이 반복됩니다.

탐색 대상이나 화면의 경우 ViewModel은 저장소 또는 사용 사례 클래스와 함께 작동하여 데이터를 가져와 UI 상태로 변환하는 동시에 상태 변경을 야기할 수 있는 이벤트 효과를 통합합니다. 앞에서 언급한 우수사례에는 기사 목록이 포함되며 각 기사의 제목, 설명, 출처, 작성자 이름, 게시일, 북마크 여부가 표시됩니다. 각 기사 항목의 UI는 다음과 같습니다.

그림 5. 우수사례 앱의 기사 항목 UI

사용자의 기사 북마크 요청은 상태 변경을 야기할 수 있는 이벤트의 예입니다. 상태 생성자의 경우 UI 상태의 모든 필드를 채우고 UI가 완전히 렌더링되는 데 필요한 이벤트를 처리하기 위해 모든 필수 로직을 정의하는 역할은 ViewModel이 담당합니다.

사용자가 기사를 북마크할 때 UI 이벤트가 발생합니다. ViewModel이 데이터 레이어에 상태 변경을 알립니다. 데이터 레이어가 데이터 변경사항을 유지하고 애플리케이션 데이터를 업데이트합니다. 북마크된 기사와 함께 새 앱 데이터가 ViewModel에 전달되면 ViewModel이 새 UI 상태를 생성하고 UI 요소에 전달하여 표시합니다.
그림 6. UDF의 이벤트 및 데이터 주기를 보여주는 다이어그램

다음 섹션에서는 상태 변경을 야기하는 이벤트와 UDF를 사용하여 이벤트를 처리하는 방법을 자세히 살펴봅니다.

로직의 유형

기사 북마크는 앱에 가치를 부여하므로 비즈니스 로직의 예입니다. 자세한 내용은 데이터 레이어 페이지를 참고하세요. 하지만 다음과 같이 정의해야 하는 다양한 로직 유형이 있습니다.

  • 비즈니스 로직은 상태 변경에 따라 진행해야 할 작업입니다. 앞서 언급했듯이 우수사례 앱에서 기사를 북마크하는 것을 예로 들 수 있습니다. 비즈니스 로직은 일반적으로 도메인 또는 데이터 레이어에 배치되지만 UI 레이어에는 배치되지 않습니다.
  • UI 동작 로직 또는 UI 로직은 상태 변경사항을 표시하는 방법입니다. 예를 들어 Android Resources를 사용하여 화면에 표시할 올바른 텍스트를 가져오거나, 사용자가 버튼을 클릭할 때 특정 화면으로 이동하거나, 토스트 메시지 또는 스낵바를 사용하여 화면에 사용자 메시지를 표시합니다.

특히 Context 같은 UI 유형의 경우 UI 로직은 ViewModel이 아닌 UI에 있어야 합니다. 테스트 가능성을 높이고 문제 구분에 도움이 되도록 UI 로직을 다른 클래스에 위임하고자 하며 UI가 점점 복잡해지는 경우 간단한 클래스를 상태 홀더로 만들 수 있습니다. UI에서 생성된 간단한 클래스는 UI의 수명 주기를 따르기 때문에 Android SDK 종속 항목을 사용할 수 있습니다. ViewModel 객체의 수명은 더 깁니다.

상태 홀더 및 상태 홀더의 UI 빌드 적합성에 관한 자세한 내용은 Jetpack Compose 상태 가이드를 참고하세요.

UDF를 사용하는 이유

UDF는 그림 4와 같이 상태 생성 주기를 모델링합니다. 또한 상태 변경이 발생하는 위치, 변환되는 위치, 최종적으로 사용되는 위치를 구분합니다. 이렇게 구분하면 UI가 이름에 드러난 의미 그대로 동작할 수 있습니다. 즉, 상태 변경사항을 관찰하여 정보를 표시하고 변경사항을 ViewModel에 전달하여 사용자 인텐트를 전달합니다.

UDF를 사용하면 다음이 가능합니다.

  • 데이터 일관성: UI용 정보 소스가 하나입니다.
  • 테스트 가능성: 상태 소스가 분리되므로 UI와 별개로 테스트할 수 있습니다.
  • 유지 관리성: 상태 변경은 잘 정의된 패턴을 따릅니다. 즉, 변경은 사용자 이벤트 및 데이터를 가져온 소스 모두의 영향을 받습니다.

UI 상태 노출

UI 상태를 정의하고 이 상태의 생성을 관리할 방법을 결정한 후에는 생성된 상태를 UI에 표시하는 단계를 진행합니다. UDF를 사용하여 상태 생성을 관리하므로 생성된 상태를 스트림으로 간주할 수 있습니다. 즉, 시간 경과에 따라 여러 버전의 상태가 생성됩니다. 따라서 LiveData 또는 StateFlow와 같이 관찰 가능한 데이터 홀더에 UI 상태를 노출해야 합니다. 이유는 ViewModel에서 데이터를 직접 가져오지 않고도 UI가 상태 변경사항에 반응할 수 있도록 하기 위해서입니다. 이러한 유형은 항상 최신 버전의 UI 상태를 캐시한다는 이점도 있습니다. 이는 구성 변경 후 빠른 상태 복원에 유용합니다.

class NewsViewModel(...) : ViewModel() {

    val uiState: StateFlow<NewsUiState> = …
}

Compose

class NewsViewModel(...) : ViewModel() {

    val uiState: NewsUiState = …
}

관찰 가능한 데이터 홀더로서의 LiveData에 관해서는 이 Codelab에서 소개합니다. Kotlin 흐름에 관한 비슷한 소개는 Android의 Kotlin 흐름을 참고하세요.

UI에 노출되는 데이터가 비교적 간단할 때는 UI 상태 유형으로 데이터를 래핑하는 것이 좋은 경우가 많습니다. 내보낸 상태 홀더와 관련 화면/UI 요소 간의 관계를 전달하기 때문입니다. 또한 UI 요소가 더 복잡해질 때 언제나 간편하게 UI 상태 정의를 추가하여 UI 요소를 렌더링하는 데 필요한 더 많은 정보를 포함할 수 있습니다.

UiState 스트림을 만드는 일반적인 방법은 ViewModel에서 지원되는 변경 가능한 스트림을 변경 불가능한 스트림으로 노출하는 것입니다. 예를 들어 MutableStateFlow<UiState>StateFlow<UiState>로 노출합니다.

class NewsViewModel(...) : ViewModel() {

    private val _uiState = MutableStateFlow(NewsUiState())
    val uiState: StateFlow<NewsUiState> = _uiState.asStateFlow()

    ...

}

Compose

class NewsViewModel(...) : ViewModel() {

    var uiState by mutableStateOf(NewsUiState())
        private set

    ...
}

그런 다음 ViewModel은 상태를 내부적으로 변경하는 메서드를 노출하여 UI에 사용되도록 업데이트를 게시합니다. 예를 들어 비동기 작업을 실행해야 하는 경우 viewModelScope를 사용하여 코루틴을 실행하고 코루틴이 완료되면 변경 가능한 상태를 업데이트할 수 있습니다.

class NewsViewModel(
    private val repository: NewsRepository,
    ...
) : ViewModel() {

    private val _uiState = MutableStateFlow(NewsUiState())
    val uiState: StateFlow<NewsUiState> = _uiState.asStateFlow()

    private var fetchJob: Job? = null

    fun fetchArticles(category: String) {
        fetchJob?.cancel()
        fetchJob = viewModelScope.launch {
            try {
                val newsItems = repository.newsItemsForCategory(category)
                _uiState.update {
                    it.copy(newsItems = newsItems)
                }
            } catch (ioe: IOException) {
                // Handle the error and notify the UI when appropriate.
                _uiState.update {
                    val messages = getMessagesFromThrowable(ioe)
                    it.copy(userMessages = messages)
                 }
            }
        }
    }
}

Compose

class NewsViewModel(
    private val repository: NewsRepository,
    ...
) : ViewModel() {

   var uiState by mutableStateOf(NewsUiState())
        private set

    private var fetchJob: Job? = null

    fun fetchArticles(category: String) {
        fetchJob?.cancel()
        fetchJob = viewModelScope.launch {
            try {
                val newsItems = repository.newsItemsForCategory(category)
                uiState = uiState.copy(newsItems = newsItems)
            } catch (ioe: IOException) {
                // Handle the error and notify the UI when appropriate.
                val messages = getMessagesFromThrowable(ioe)
                uiState = uiState.copy(userMessages = messages)
            }
        }
    }
}

위의 예에서 NewsViewModel 클래스는 특정 카테고리의 기사를 가져오려고 시도한 후에 결과에 따라 UI가 적절하게 반응할 수 있도록 시도의 성공 또는 실패 결과를 UI 상태에 반영합니다. 오류 처리에 관한 자세한 내용은 화면에 오류 표시 섹션을 참고하세요.

추가 고려사항

이전의 도움말 외에도 UI 상태를 노출할 때 다음 사항을 고려하세요.

  • UI 상태 객체는 서로 관련성 있는 상태를 처리해야 합니다. 이렇게 하면 불일치가 줄어들고 코드를 이해하기가 더 쉽습니다. 뉴스 항목 목록과 북마크 수를 서로 다른 두 스트림에 노출하면 한 스트림이 업데이트되고 다른 스트림은 업데이트되지 않은 상황이 발생할 수 있습니다. 단일 스트림을 사용하면 두 요소가 모두 최신 상태로 유지됩니다. 또한 일부 비즈니스 로직에는 소스 조합이 필요할 수 있습니다. 예를 들어 로그인한 상태인 동시에 프리미엄 뉴스 서비스의 구독자인 사용자에게만 북마크 버튼을 표시해야 한다면 다음과 같이 UI 상태 클래스를 정의하면 됩니다.

    data class NewsUiState(
        val isSignedIn: Boolean = false,
        val isPremium: Boolean = false,
        val newsItems: List<NewsItemUiState> = listOf()
    )
    
    val NewsUiState.canBookmarkNews: Boolean get() = isSignedIn && isPremium
    

    이 선언에서 북마크 버튼의 표시 여부는 다른 두 속성의 파생 속성입니다. 비즈니스 로직이 더 복잡해짐에 따라 모든 속성을 즉시 사용할 수 있는 단일 UiState 클래스의 중요성이 점차 커집니다.

  • UI 상태: 단일 스트림인지 여러 스트림인지. UI 상태 노출 대상을 단일 스트림과 여러 스트림 중에서 선택할 때 주요 원칙은 이전 글머리기호에서 설명한 내보낸 항목 간의 관계입니다. 단일 스트림 노출의 가장 큰 장점은 편의성과 데이터 일관성입니다. 즉, 상태 사용자자가 언제나 즉시 최신 정보를 확인할 수 있습니다. 하지만 다음과 같이 ViewModel 상태의 스트림이 별개일 때 적합한 경우가 있습니다.

    • 관련 없는 데이터 유형: UI를 렌더링하는 데 필요한 일부 상태는 서로 완전히 별개일 수 있습니다. 이때 서로 다른 상태를 함께 번들로 묶는 데 드는 비용이 이점보다 더 클 수 있으며 이는 상태 중 하나가 다른 상태보다 더 자주 업데이트되는 경우에 특히 그렇습니다.

    • UiState 비교(diff): UiState 객체에 필드가 많을수록 필드 중 하나를 업데이트하면 스트림이 내보내질 가능성이 큽니다. 뷰에는 연속적으로 이루어지는 내보내기가 같은지 다른지 파악하는 비교(diff) 메커니즘이 없으므로 내보내기할 때마다 뷰가 업데이트됩니다. 따라서 Flow API 또는 LiveDatadistinctUntilChanged()와 같은 메서드를 사용한 완화 작업이 필요할 수 있습니다.

UI 상태 사용

UI에서 UiState 객체의 스트림을 사용하려면 사용 중인 관찰 가능한 데이터 유형에 터미널 연산자를 사용합니다. 예를 들어 LiveData의 경우 observe() 메서드를 사용하고 Kotlin 흐름의 경우 collect() 메서드나 이 메서드의 변형을 사용합니다.

UI에서 관찰 가능한 데이터 홀더를 사용할 때는 UI의 수명 주기를 고려해야 합니다. 수명 주기를 고려해야 하는 이유는 사용자에게 뷰가 표시되지 않을 때 UI가 UI 상태를 관찰해서는 안 되기 때문입니다. 이 주제에 관한 자세한 내용은 이 블로그 게시물을 참고하세요. LiveData를 사용하면 LifecycleOwner가 수명 주기 문제를 암시적으로 처리합니다. 흐름을 사용할 때는 적절한 코루틴 범위와 repeatOnLifecycle API로 처리하는 것이 가장 좋습니다.

class NewsActivity : AppCompatActivity() {

    private val viewModel: NewsViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect {
                    // Update UI elements
                }
            }
        }
    }
}

Compose

@Composable
fun LatestNewsScreen(
    viewModel: NewsViewModel = viewModel()
) {
    // Show UI elements based on the viewModel.uiState
}

진행 중인 작업 표시

UiState 클래스의 로드 상태를 나타내는 간단한 방법은 불리언 필드를 사용하는 것입니다.

data class NewsUiState(
    val isFetchingArticles: Boolean = false,
    ...
)

이 플래그의 값은 UI에 진행률 표시줄이 존재하는지를 나타냅니다.

class NewsActivity : AppCompatActivity() {

    private val viewModel: NewsViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                // Bind the visibility of the progressBar to the state
                // of isFetchingArticles.
                viewModel.uiState
                    .map { it.isFetchingArticles }
                    .distinctUntilChanged()
                    .collect { progressBar.isVisible = it }
            }
        }
    }
}

Compose

@Composable
fun LatestNewsScreen(
    modifier: Modifier = Modifier,
    viewModel: NewsViewModel = viewModel()
) {
    Box(modifier.fillMaxSize()) {

        if (viewModel.uiState.isFetchingArticles) {
            CircularProgressIndicator(Modifier.align(Alignment.Center))
        }

        // Add other UI elements. For example, the list.
    }
}

화면에 오류 표시

UI에서의 오류 표시는 진행 중인 작업 표시와 비슷합니다. 두 작업은 모두 존재 여부를 나타내는 불리언 값으로 쉽게 표현되기 때문입니다. 하지만 오류에는 사용자에게 다시 전달하는 관련 메시지 또는 실패한 작업을 다시 시도하는 관련 작업이 포함될 수 있습니다. 따라서 진행 중인 작업을 로드하고 있거나 로드하고 있지 않은 동안 오류 컨텍스트에 적절한 메타데이터를 호스팅하는 데이터 클래스를 사용하여 오류 상태를 모델링해야 할 수 있습니다.

예를 들어 기사를 가져오는 동안 진행률 표시줄을 표시하는 이전 섹션의 예를 살펴보겠습니다. 작업에서 오류가 발생하는 경우 오류 상황을 자세히 설명하는 메시지를 하나 이상 사용자에게 표시하는 것이 좋습니다.

data class Message(val id: Long, val message: String)

data class NewsUiState(
    val userMessages: List<Message> = listOf(),
    ...
)

오류 메시지는 스낵바 같은 UI 요소의 형식으로 사용자에게 표시될 수 있습니다. 이러한 기능은 UI 이벤트의 생성 및 사용 방식과 관련이 있으므로 자세한 내용은 UI 이벤트 페이지를 참고하세요.

스레딩 및 동시 실행

ViewModel에서 실행되는 모든 작업은 기본 스레드에서 안전하게 호출된다는 기본 안전성을 갖추어야 합니다. 데이터 레이어와 도메인 레이어가 작업을 다른 스레드로 옮기는 역할을 담당하기 때문입니다.

또한 장기 실행 작업의 경우 ViewModel에서 로직을 백그라운드 스레드로 옮기는 역할을 담당합니다. Kotlin 코루틴은 동시 실행 작업을 관리하는 좋은 방법이며, Jetpack 아키텍처 구성요소는 Kotlin 코루틴을 기본적으로 지원합니다. Android 앱에서 코루틴을 사용하는 방법을 자세히 알아보려면 Android의 Kotlin 코루틴을 참고하세요.

앱 탐색의 변경사항은 주로 이벤트 같은 내보내기에 의해 이루어집니다. 예를 들어 SignInViewModel 클래스가 로그인을 실행하면 UiStateisSignedIn 필드를 true로 설정할 수 있습니다. 이러한 트리거는 위의 UI 상태 사용 섹션에 설명된 대로 사용해야 합니다. 단, 사용 구현이 탐색 구성요소를 지연해야 합니다.

Paging

Paging 라이브러리는 UI에서 PagingData라는 유형과 함께 사용됩니다. PagingData는 시간 경과에 따라 변경될 수 있는(즉, 변경 불가능한 유형이 아님) 항목을 나타내고 포함하므로 변경 불가능한 UI 상태로 표현되어서는 안 됩니다. 대신 ViewModel의 데이터를 독립적으로 자체 스트림에서 노출해야 합니다. 이에 관한 구체적인 예는 Android Paging Codelab을 참고하세요.

애니메이션

부드럽고 원활한 최상위 탐색 전환을 제공하기 위해 다음 화면의 데이터가 로드될 때까지 기다린 후에 애니메이션을 시작하는 것이 좋습니다. Android 뷰 프레임워크는 postponeEnterTransition()startPostponedEnterTransition() API를 사용하여 프래그먼트 대상 간의 전환을 지연하는 후크를 제공합니다. 이러한 API는 다음 화면의 UI 요소(일반적으로 네트워크에서 가져온 이미지)가 표시될 준비가 되면 UI가 다음 화면으로의 전환을 애니메이션 처리할 수 있도록 합니다. 자세한 내용과 구현 세부정보는 Android 모션 샘플을 참고하세요.