이 섹션은 안드로이드 View 시스템을 사용한 단방향 데이터 흐름 개념을 소개한다.

이미 단방향 데이터 흐름 및 구조화 된 코드에서 어떤식으로 사용되는지 알고 있다면, 다음 섹션으로 넘어가도 좋다.

UI 업데이트 순환

TODO앱을 만들기 전에, 단방향 데이터 흐름에 대해서 알아보자.

무엇이 state를 갱신하게 만들까? 도입부에서는 state는 시간에 따라 변경되는 어떠한 값이라고 했다. 이건 안드로이드 애플리케이션에서는 단지 state에 대한 일부분에 불과하다.

안드로이드 애플리케이션들에서 state는 event에 반응하여 갱신된다. event는 사용자의 버튼 클릭, EditText의 afterTextChanged 호출, 가속계가 전달하는 새로운 값 등 애플리케이션 밖에서 생성된 입력을 말한다.

Event는 프로그램 일부에 무언가 발생하는 것을 통지한다.

예를 들어, 사용자가 버튼을 누르는 것은 클릭 event를 호출한다.

모든 안드로이드 애플리케이션에서 UI 업데이트 순환에 대한 핵심적인 내용은 다음과 같다.

  • Event – 사용자 또는 또 다른 프로그램 일부로 생성된 이벤트
  • Update State – event 핸들러는 UI에 의해 사용되는 state를 변경한다.
  • Display State – UI는 새로운 state를 나타내기 위해 업데이트 된다.

Compose에서 state를 관리하는 것은 state 및 event가 서로 어떻게 상호작용하는지에 이해하는 것이다.

구조화되지 않은 state

컴포즈로 들어가기 전에, 안드로이드 View 시스템내에 event 및 state에 대해서 알아보자. 우리 만드려고 하는 hello world Activity에서의 “Hello, World” state는 사용자가 이름을 입력할 수 있도록 한다.

우리가 이것을 작성하는 한가지 방법은 event 콜백을 직접적으로 받아 TextView에 state를 적용하는 것이다. 그리고 ViewBinding을 사용하는 코드는 아마 다음과 같이 생겼을 것이다.

class HelloCodelabActivity : AppCompatActivity() {

   private lateinit var binding: ActivityHelloCodelabBinding
   var name = ""

   override fun onCreate(savedInstanceState: Bundle?) {
       /* ... */
       binding.textInput.doAfterTextChanged {text ->
           name = text.toString()
           updateHello()
       }
   }

   private fun updateHello() {
       binding.helloText.text = "Hello, $name"
   }
}

이 코드는 이미 example 패키지에 완성되어 있다. 이 섹션에서 변경할 상항은 없다.

그러나 이 코드는 Activtiy에 저장된 구조화 되지 않은 state를 나타낸다.

이런 코드는 잘 작동한다. 그리고 이런 작은 예제에서는 이런식으로 코드를 짜도 나쁘지 않다. 하지만 UI가 계속해서 복잡해짐에 따라 점점 관리하기 어려워지는 경향이 있다.

더 많은 event 및 state를 만들어진 Activity에 추가함에 따라, 몇가지 문제점들이 도출된다.

  1. 유닛 테스트 – UI의 state가 View들과 뒤석여 있기 때문에, 이 코드에 대한 유닛테스트를 수행하기가 어렵다.
  2. 부분적인 state 업데이트 – 화면이 많은 event들을 가질 때, 이벤트에 대한 응답으로 업데이트 해야 할 state 코드를 놓치기 쉽다. 그런부분 때문에 사용자가 보는 UI 화면이 일관성이 없거나, 부정확하게 된다.
  3. 부분적인 UI 업데이트 – 각 state 변경 후에 수동으로 UI를 갱신해야 하므로, 이를 까먹기가 너무 쉽다. 그런부분 때문에 사용자가 무작위로 갱신되는 UI를 통해 오래된 데이터 보게 된다.
  4. 코드 복잡성 – 이 패턴에서 코드를 작성하게 되면 어떠한 로직을 추출하기가 어려워진다. 그렇기 때문에 코드가 읽기 어렵고, 이해하기 어려워지는 경향이 있다.

단방향 데이터 흐름 사용하기

구조화 되지 않은 state와 함께 발생하는 문제들을 고치기 위해서, 구글은 ViewModel과 LiveData를 포함한 Android Architecture Component를 도입했었다.

ViewModel은 UI로부터 state를 분리하도록 하고, UI가 해당 state를 호출하여 업데이트 할 수 있는 이벤트를 정의했다. ViewModel을 사용하여 작성된 동일한 Activity를 살펴보도록 하자.

// HelloCodelabActivity.kt

class HelloCodelabViewModel: ViewModel() {

   // LiveData는 state를 갖고 있고, UI에 의해 관찰된다.
   // (ViewModel로부터 state가 아래로 전달된다.)
   private val _name = MutableLiveData("")
   val name: LiveData<String> = _name

   // onNameChanged는 UI가 호출할 수 있는 우리가 정의한 이벤트다.
   // (event는 UI로 부터 위로 전달된다.)
   fun onNameChanged(newName: String) {
       _name.value = newName
   }
}

class HelloCodeLabActivityWithViewModel : AppCompatActivity() {
   private val helloViewModel by viewModels<HelloCodelabViewModel>()

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

       binding.textInput.doAfterTextChanged {
           helloViewModel.onNameChanged(it.toString()) 
       }

       helloViewModel.name.observe(this) { name ->
           binding.helloText.text = "Hello, $name"
       }
   }
}

이 예제를 살펴보면, Activity로 부터 ViewModel로 상태를 이동시켰다. ViewModel에서 state는 LiveData로 표현된다. LiveDataobservable state holder(관찰가능한 상태 소유자)로써 변경되는 state를 관찰하고 싶은 누군가에게 변경사항을 전달하는 방법을 제공한다. 그러면 UI에서는 observe 메서드를 사용하여 state가 변경될 때마다 UI를 갱신하도록 한다.

observable은 state 변경을 알고 싶은 누군가에게 이를 수신할 수 있는 방법을 제공하는 state 객체다.

예를들면, LiveData, StateFlow, Flow 및 Observable은 모두 observable하다라고 말할 수 있다.

ViewModel은 또한 onNameChanged라는 이벤트를 노출한다. 예제에서 EditText에서 텍스트가 변경될 때마다 일어나는 일들처럼, 이 event는 사용자 event들에 반응하여 UI에서 호출된다.

UI 업데이트 순환이야기로 다시 돌아가서, ViewModel이 event 및 state와 함께 어떻게 궁합이 잘 맞는지 알 수 있다.

  • Event – 텍스트 입력이 변경될 때 UI에 의해 onNameChanged가 호출된다.
  • Update State – onNameChanged가 처리한 다음, _name의 state를 설정한다.
  • Display State – name의 옵저버들이 호출되어 UI state 변경을 통지한다.

이런식으로 코드를 구조화하는 것으로, ViewModel에게 event를 올려보내는 것으로 생각할 수 있다. 그런 다음 event에 대한 응답으로 ViewModel은 일부 처리를 수행하고 state를 다시 업데이트 할 수 있다. state가 업데이트 될 때, stateActivity를 향해 아래로 흐른다. 아래의 그림을 참조하면 이해하는데 도움이 된다.

이 패턴이 단방향 데이터 흐름이라 불리는 패턴이다. 단방향 데이트 흐름은 state를 아래로 흐르게하고 event를 위로 올리는 설계방식이다. 이런식으로 코드를 구조화하면 몇가지 장점을 얻게 된다.

  • 테스트용이성 – UI로부터 state를 분리하는 것으로, ViewModel 및 Activity의 테스트가 더 쉬워진다.
  • State 캡슐화ViewModel 과 같은 하나의 장소에서만 state가 업데이트 될 수 있기 때문에, UI가 커짐에 따라 부분적인 state 업데이트 버그가 발생할 가능성이 줄어진다.
  • UI 일관성 – 모든 state 갱신은 state를 가지고 있는 observable을 사용하는 UI에서 즉시 반영된다

이러한 방식은 코드가 조금 더 길어지지만, 단방향 데이터 흐름을 사용하면 복잡한 state 및 event를 다루는데 있어서 좀 더 쉽고 신뢰할만한 코드를 만들어 낼 수 있다.

단방향 데이터 흐름은 event가 위로 흐르고 state가 아래로 흐르는 설계방식이다.

예를 들어 ViewModel에서 state는 LiveData를 사용하여 아래로 흐르는 동안 UI의 메서드 호출과 함께 event가 전달된다.

단방향 데이터 흐름이라는게 단지 ViewModel을 묘사하는 용어가는 아니다. event가 위로 흐르고 state가 아래로 흐르는 모든 설계방식은 단방향이라고 말할 수 있다.

다음 섹션에서 어떻게 단방향 데이터 흐름을 컴포즈와 함께 사용할 수 있는지 확인하자.

카테고리: Compose

1개의 댓글

성빈 · 2022년 10월 28일 12:01 오후

좋은 글 감사합니다!

답글 남기기

Avatar placeholder

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.