UI Event 처리 전략

hongbeom
hongbeomi dev
Published in
8 min readJun 2, 2022

--

Android에서 UI Event를 처리하는 방법에 대해 알아봅니다.

Photo by Pablo Heimplatz on Unsplash

본 글은 Manuel Vivo님의 글과 Android 가이드 문서를 읽고 정리한 글입니다. 자세한 내용은 아래 링크를 참고하세요.

https://developer.android.com/topic/architecture/ui-layer/events

👀 UI Event

UI 이벤트란 UI 레이어(Activity, Fragment..)에서 UI 또는 ViewModel로 처리해야 하는 작업을 뜻합니다. 일반적으로 사용자의 버튼 클릭이나 스와이프 등등의 앱과의 상호작용을 통해 이런 이벤트들이 만들어집니다.

이런 이벤트는 ViewModel에서 어떤 비즈니스 로직의 처리가 필요한 비즈니스 로직 이벤트(데이터 새로 고침)와 ViewModel의 처리가 딱히 필요하지 않고 바로 UI에서 처리할 수 있는(토스트 표시) UI 로직 이벤트로 나뉩니다.

비즈니스 로직은 다른 모바일 플랫폼에서도 동일하게 유지되는 반면, UI 로직은 케이스에 따라 다를 수 있는 로직입니다. 안드로이드 공식 가이드 문서에서는 비즈니스 로직을 처리하는 클래스의 솔루션으로 AAC ViewModel 클래스를 추천하고 있습니다.

그렇다면 우리는 UI 이벤트를 처리하기에 앞서, 어떤 종류의 이벤트인지 결정해야할 필요가 있습니다. 안드로이드 가이드 문서에서 권장하는 이벤트 결정 트리는 아래와 같습니다.

cc: https://developer.android.com/topic/libraries/architecture/images/mad-arch-uievents-tree.png

🛠 Event 처리하기

우리는 View의 상태 변경이나, Toast 메시지 표시 등 UI에서 처리해야하는 이벤트는 간단하게 처리할 수 있을 것입니다.

마찬가지로 ViewModel을 거쳐야하는 비즈니스 로직이 필요한 이벤트도 간단하게 처리할 수 있습니다.

그렇다면 RecyclerView의 Item에서 비즈니스 로직 처리가 필요한 이벤트가 발생한다면 어떻게 처리할 수 있을까요? 우리는 아마 몇 가지 방법을 생각해볼 수 있을 것입니다.

  • Adapter 생성자에 콜백을 넘겨서 처리하기
  • Adapter 생성자에 ViewModel을 넘겨서 처리하기
  • Item에 바인딩되는 UI State 객체에 함수 타입의 프로퍼티를 두기

안드로이드 가이드 문서에서 권장하는 방법은 3번에 가깝습니다. 3번 방식은 RecyclerView의 어댑터가 필요한 데이터에만 접근할 수 있게 되며 ViewModel의 모든 부분에 접근할 수 없어지므로 ViewModel에서 노출되는 부분들이 악용될 가능성이 낮아집니다. 다시 말해 어댑터와 ViewModel이 강하게 결합되는 것은 좋지 않다는 말입니다.

ViewModel 이벤트 처리

ViewModel 이벤트에서 발생하는 UI 이벤트 작업은 항상 UI State의 업데이트로 이어져야 합니다. (UDF 원칙 준수) 이렇게 UI State의 업데이트를 통해 Configuration Change와 같은 이벤트 이후에도 UI 이벤트를 재현할 수 있으며 UI 이벤트가 손실되지 않습니다.

ViewModel 이벤트 처리의 안티 패턴

물론 앱에서 Kotlin의 Channel 혹은 SharedFlow 같은 반응형 스트림을 사용하여 UI에 ViewModel 이벤트를 노출할 수도 있습니다. 이 방법은 다른 프로젝트들에서 흔하게 찾아볼 수 있는 패턴입니다. 하지만 이런 방법은 해당 이벤트의 전달과 처리를 보장하지 않습니다. 이로 인해 우리는 향후 버그와 문제를 마주할 수 있습니다.

UI State 업데이트를 유발하는 ViewModel 이벤트는 즉각적으로 처리해야합니다. Channel 혹은 SharedFlow와 같은 다른 반응형 솔루션을 사용하여 이벤트를 객체로 노출하려고 하면 이벤트 전달 및 처리가 보장되지 않습니다. — Manuel Vivo

예시를 살펴보겠습니다. 아래 코드는 무언가 추가하는 로직을 가진 ViewModel 입니다. 이 코드에선 무언가 추가된 결과가 돌아오면 그 추가된 결과가 표시되는 화면으로 이동하도록 UI에게 이벤트를 노출합니다.

그럼 UI는 이 이벤트를 이렇게 사용할 수 있을 것입니다.

위의 코드는 여러 가지 결함이 있습니다.

# 1 : 추가 완료 상태가 손실될 수 있음

채널은 이벤트의 전달 및 처리를 보장하지 않기에 이벤트가 손실되어 UI가 일관되지 않은 상태가 될 수 있습니다. UI 백그라운드로 이동할 때, 추가된 결과(isAddedSuccessful)를 생산하는 생산자가 Channel로 이벤트를 send 하는 직후에 수집 작업이 중지된다면 이런 문제가 발생할 수 있습니다.

하지만 이 결함은 이벤트를 보내고 받을 때 Dispatchers.Main.immediate를 사용하여 완화할 수 있습니다. 하지만 이는 개발자가 쉽게 잊어버릴 수 있으므로 이 방법은 오류가 발생하기 쉽습니다.

#2 : UI에 이벤트를 처리하도록 명령함

우리가 여러가지 화면 크기를 지원한다면, 화면 크기에 따라 ViewModel 이벤트에 따른 수행될 UI 작업이 다를 수 있을 것입니다.

ViewModel은 앱의 상태가 무엇인지 UI에 알려야하고 UI는 이를 반영하는 방법을 결정해야 합니다.

#3 : 일회성 이벤트를 즉시 처리하지 않음

이벤트를 발생시키고 잊어버리게하는 방법으로 모델링하면 문제가 발생합니다. 이는 ACID 트랜잭션을 준수하는 것이 어렵기 때문에 데이터의 신뢰성과 무결성을 보장할 수 없습니다. 이벤트가 처리되지 않는 시간이 길어질수록 문제는 더 어려워집니다. ViewModel 이벤트의 경우 가능한 한 빨리 처리하고 새로운 UI State를 생성하세요.

ViewModel에서 일회성 이벤트를 처리하는 것은 일반적으로 메서드의 호출로 귀결됩니다(ex : UI State 업데이트). 해당하는 메서드를 호출하면 성공적으로 완료되었는지 예외가 발생하는지 알 수 있으며 정확히 한 번 발생했음을 알 수 있습니다.

개선 방법

이런 상황이 발생한 경우 일회성 ViewModel 이벤트가 실제로 UI에 의미하는 바를 다시 생각해봐야합니다. UI State는 특정 시점의 UI를 더 잘 표현하고 더 많은 전달 및 처리의 보장을 제공하고 테스트하기도 쉽습니다.

위 예시의 경우 ViewModel은 UI에 이벤트 처리를 명령하는 대신, 데이터를 노출해야 합니다. 아래는 개선된 코드입니다.

위 코드에서 이벤트는 새로운 데이터를 호출하여 즉각적으로 처리됩니다. 이제 이 이벤트가 손실될 일은 없습니다. 이벤트는 상태로 변환되었으며 _uiState는 추가된 결과 데이터를 반영합니다.

이제 UI는 이 State에 반응하고 처리하면 됩니다.

위 코드에서 uiStateisLoading 플래그와 isAddedSuccessful 플래그가 밀접하게 연결되어 있기 때문에 고유한 UI State 스트림이 노출됩니다. 이들을 만약 분리한다면 UI State간의 불일치가 발생할 수 있습니다. 이들을 동일한 UiState 클래스에 함께 둠으로써, 우리는 화면의 Ui State를 만드는 필드에 대해 더 잘 인지할 수 있게 되어 버그를 최소화할 수 있습니다.

기타

UI State의 업데이트를 통해 UI 이벤트를 해결할 수 없다고 판단된다면 앱의 데이터 흐름을 다시 고려해야 할 수 있습니다. 다음 원칙을 고려하여 모델링해보세요.

  • 각각의 클래스에서 각자의 역할만을 수행합니다. UI는 네비게이션, 클릭 이벤트, 권한 요청 가져오기 같은 화면별 동작 로직을 담당합니다. ViewModel은 비즈니스 로직을 포함하며 더 아래 단계의 레이어(domain, data 등..)에서 얻은 결과를 UI State로 변환합니다.
  • 이벤트가 발생하는 위치를 생각해봅니다.
  • 소비자가 여러 명이고 이벤트가 여러 번 소비되는 것이 우려된다면 앱의 아키텍쳐를 다시 고려해봅니다. 동시에 실행되는 소비자가 여럿인 경우 정확히 한 번만 이벤트를 제공함을 보장하는 것이 매우 어려워지므로 사이드 이펙트가 엄청나게 증가합니다. 이 문제가 발생한다면 UI 트리의 위쪽으로 문제를 보내서 해결해봅시다. UI 트리 위쪽의 다른 엔티티가 필요할 수 있습니다.
  • 상태를 소비해야하는 경우를 생각해봅니다. 어떤 상황에선 앱이 백그라운드에 있다면 소비하지 않는 것이 좋을 수도 있습니다.(ex: Toast 표시) 이 경우 UI가 포그라운드에 있을 때 상태를 소비하는 것이 좋습니다.

이벤트 처리는 앱을 만들 때 매우 중요한 처리 중 하나 입니다. 잘못된 이벤트 처리로 인해 사용자는 의도하지 않은 상황을 마주할 수 있기 때문에 신중하게 처리하는 것이 좋을 것입니다.

참고

https://developer.android.com/topic/architecture/ui-layer/events

--

--