Architecture: The UI layer — MAD Skills[번역]

DwEnn
9 min readApr 13, 2022

--

📝 NOTE : 이 글은 영상의 스크립트를 번역한 것이며 오역이 있을 수 있습니다. 😅

안녕하세요. 환영합니다. 🤗
이번 MAD Skils 에피소드에서는 UI Layer 에 대해 얘기해보겠습니다.

App Architecture

UI 의 역할은 application data 를 화면에 표시하고 사용자 상호 작용의 주요 지점을 제공하는 것입니다. 버튼을 누르는 것과 같은 사용자 상호 작용 또는 네트워크 응답과 같은 외부 입력으로 인해 데이터가 변경될 때마다 UI 는 이러한 변경사항을 업데이트하고 반영해야 합니다. 즉, UI 는 Data Layerapplication state 를 시각적으로 표현한 것입니다. 설명을 쉽게 하기 위해 jet news jetpack compose 샘플을 사용할것입니다. 하지만 여기서 다루는 모든 내용은 view 에도 동일하게 적용됩니다.
UI 는 데이터를 표시하는 ActivityFragment 와 같은 UI 요소를 지칭합니다. view 또는 jectpack compose 같은 UI 작업을 수행하는 api 들과는 무관합니다.

UI Layer Pipeline

Data Layer의 역할은 관리를 유지하고 애플리케이션 데이터에 대한 접근을 제공하는 것이므로 UI Layer 는 다음 단계를 수행해야 합니다.

UI Layer Concepts

이 파이프라인을 이해하기 위해 다음의 개념들을 다루겠습니다.

  1. UI State 를 정의하는 방법
  2. UI State 의 생산을 관리하는 방법
  3. 단방향 데이터 흐름 원칙에 따라 observable 데이터 타입으로 UI State 를 노출하는 방법
  4. observable UI State 를 소비하는 UI 를 구현하는 방법

What is the UI ?

먼저, UI State 를 정의하는 것부터 시작하겠습니다.
jet news 샘플의 UI 는 사용자에게 article 목록을 보여줍니다. 이 정보가 UI State 입니다. 즉, 동전의 양면과 같습니다. UI 는 UI State 의 시작적인 표현입니다. UI 는 UI State 에 대한 변경 사항을 즉시 반영합니다.

다음 사례에서 UI 를 완전히 렌더링하는데 필요한 정보는 다음과 같은 정의된 NewsUiState 데이터 클래스에 캡슐화될 수 있습니다.

data class NewsUiState(
val newsItems: List<NewsItemUiState>,
val userMessages: List<Message>
)

UI State 의 정의는 변경할 수 없다는 것에 주의하세요. 이렇게 하면 UI 가 단일 역할에 집중할 수 있게 됩니다. 상태(UI State)를 읽고 그에 따라 UI 요소(UI Elements)를 업데이트합니다. 결과적으로, UI 자체가 데이터의 유일한 소스가 아닌 이상 UI 에서 UI State 를 직접 수정해서는 안 됩니다. 이 원칙을 위반한다면 동일한 정보에 대한 여러 가지 소스가 생기게 되며 데이터 불일치 및 교묘한 버그로 이어집니다.

UI State 가 정의되었고, 이것을 어떻게 만들것인지에 대해 이야기해 보겠습니다.
데이터의 업데이트 또는 사용자 액션으로 인해 시간이 지남에 따라 앱 데이터는 변경됩니다. 그렇기에 대부분의 경우 UI 의 관리를 별도의 유형으로 위임하는 것이 효과적입니다. 이것은 UI 가 UI 와 state manager 를 동시에 수행하는것을 방지합니다. 이 분리된 타입을 state holder 라고 합니다.

ViewModel as a state holder

state holderScreen, Activity, Fragment, Navigation Destination 을 위해 날짜를 생성하는 UI 요소에 따라 다양한 모양과 크기로 제공됩니다. ViewModel 인스턴스는 보통 기본적인 state holder 입니다.

ViewModel and UI pipeline

위 다이어그램을 분석해보겠습니다.

  1. ViewModel 은 UI 가 소비할 UI State 를 표시하고 유지합니다.
  2. UI 는 ViewModelUser Event 를 알립니다.
  3. ViewModel 은 사용자 액션을 처리하고 UI State 를 업데이트 합니다. 업데이트된 UI State 가 UI 에 피드백되어 렌더링됩니다.
  4. 변화 또는 상태를 유발하는 모든 이벤트들에 대해 위 내용이 반복됩니다.

이것은 단방향 데이터 흐름의 핵심 원칙입니다. 이벤트가 들어가고, 상태가 나옵니다. 이는 사용자가 글을 북마크로 지정하려는 사례의 예를 통해 더 잘 설명될 수 있습니다.

Unidirectional Data Flow example

ViewModel 은 먼저 UI 를 표시하기 위해 Data Layer 의 데이터를 변환합니다. 사용자가 글을 북마크로 지정할 때, ViewModel 은 이것을 Data Layer 로 중계합니다. Data Layer 에서 요청을 처리한 후 UI 는 새 UI State 를 소비하여 사용자에게 표시합니다.

그런데 UI 는 UI State 가 변경되었다는 것을 어떻게 아는 것일까요 ? 핵심은 observable 데이터 입니다.
UI StateStateFlow 또는 LiveData 와 같은 observable 데이터 홀더로 노출되어야 합니다. 이를 통해 UI 는 ViewModel 에서 직접 데이터를 가져오지 않고도 상태 변화에 반응할 수 있습니다.

class NewsViewModel(...) : ViewModel() {
val uiState: StateFlow<NewsUiState> = ...
}

또한 이러한 observable 데이터 타입은 최신 버전의 UI State 캐시를 가지고 있습니다. 이것은 구성 변경 후 빠른 상태 복원에 유용합니다. jetpack compose 로 구성된 앱에서는 UI State 노출을 위해 mutableStateOf 또는 snapshotFlow 과 같은 composer 의 obeservable 상태 api 를 사용할 수도 있습니다.

사용자 이벤트는 ViewModel 이 상태를 변경하는 원인이 됩니다. UI 는 UI State 를 관찰하기 때문에 항상 최신 버전을 바라봅니다.

다시 주제로 돌아와서, 카테고리에 대한 뉴스를 가져오고 싶다고 가정해보겠습니다. 우리는 UI State 를 정의합니다. 그리고 뉴스를 가져오기 위한 job 참조를 유지합니다.

class NewsViewModel(...) : ViewModel() {
private val _uiState = MutableStateflow(NewsUiState())
val uiState = _uiState.asStateFlow()
private var fetchJob: Job? = null
}

그리고 글을 가져오는 메서드를 정의합니다. 이전 가져오기가 진행 중일 경우, job 을 취소하고 새로운 글을 가져오는 핵심 코루틴을 실행합니다. 새로운 글을 가져오는 것이 완료되면 UI State 를 업데이트할 수 있습니다. 이러한 변경 사항은 자동으로 UI 로 내보내집니다. 가져오는 동안 오류가 발생한 경우 해당 오류도 UI State 에 반영되어야 합니다. 이는 UI 가 발생한 오류에 대해 사용자에게 알리는 메시지가 표기되는것을 원할 수 있기 때문입니다.

fun fetchArticles(category: String) {
fetchJob?.cancel()
fetchJob = viewModelScope.launch {
try {
val newsItems = repository.newsItemsFor(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)
}
}
}
}

이러한 ViewModelUI Event 루프에 대해 다음 에피소드에서 자세히 알아보겠습니다.

이제, 우리는 UI State 가 무엇인지에 대해 알아보았습니다. 단방향 데이터 흐름이 데이터 흐름을 관리하는데 어떻게 도움이 되는지, 그리고 ViewModel 이 데이터를 생산하고 소비하는 state holder 로써의 역할에 대해 말이죠. 꽤 많은 양입니다. 마지막 단계는 UI 에서 UI State 를 소비하는 것입니다.

UI state consumption

UI State 소비는 observable 데이터 유형에서 종단 연산자와 함께 수행됩니다. LiveData 에서 이것은 observe 메서드이고 Flow 에서는 collect 메서드 입니다. UI 에서 UI State 를 관찰할때 유의해야 할 중요한 것은 UI 의 라이프 사이클입니다. 종종 ViewModel 과 같은 state holder 들은 그들을 관찰하는 UI 들보다 훨씬 더 오래 살아있습니다. 그래서 우리는 UI 가 보이지 않을때 UI State 를 관찰하지 않도록 확실히 해야 합니다.

class NewsActivity : ComponentActivity() {
private val viewModel: NewsViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect {
// Update UI elements
}
}
}
}
}

예를 들어, 뉴스 Activity 에서 우리는 Activity 를 시작할 때만 flow 를 수집함으로서 flow 수집이 Activity 라이프 사이클을 따르고 있는지 확인합니다.

지금까지 UI Layer 에서 UI State 를 생산하고 소비하는 방법에 대해 다루었습니다. 다음 에피소드에서는 ViewModel 과 UI 사이의 상호작용에 대해 알아보겠습니다. 🙌

--

--