작성자: Manuel Vivo (Developer Relation Engineer)
앱 아키텍처 가이드를 현대화할 수 있도록, 저희는 다양한 UI 패턴 중 어떤 것이 가장 효과적인지 살펴보고, 여러 대안 간의 유사점 및 차이점을 확인 및 정리하여 권장 사항으로 알려 드리고자 합니다.
저희가 확인한 사항을 쉽게 알려 드리려면, 너무 복잡하지 않으면서도 친숙한 비즈니스 사례의 예시가 필요했습니다. 그래서... TODO 앱 만드는 법을 모르는 분은 없으시겠죠? 저희가 선택한 앱은 Architecture Blueprints였습니다. Blueprints는 기존에도 여러 아키텍처를 마음껏 실험할 수 있는 공간이었기 때문에 정말 적합한 선택이었습니다.
작동 중인 Architecture Blueprints 앱
최근에는 API 사용이 UI 패턴에 큰 영향을 미칩니다. 그중 주목할 만한한 것이 바로 Jetpack Compose의 상태 API입니다. Compose는 모든 단방향 데이터 흐름 패턴에서 원활하게 작동하므로, 객관적인 비교를 위해 Compose를 사용해 UI를 렌더링했습니다.
이번 블로그 게시물에서는 어떻게 Architecture Blueprints를 Jetpack Compose로 마이그레이션했는 지 알려드리겠습니다. LiveData를 사용하실 분들을 위해 마이그레이션 당시의 샘플을 그대로 두었으며, 리팩터링에서 ViewModel 클래스와 데이터 레이어는 수정하지 않았습니다.
⚠️ 이 LiveData 기반 코드베이스에 사용된 아키텍처는 최신 아키텍처 권장사항을 완벽하게 따르지는 않습니다. 특히 LiveData를 데이터 영역 또는 도메인 레이어에서 사용하면 안 되며, 대신에 플로우와 코루틴을 사용해야 합니다.
주의 사항에 대해 알려드렸으니, 이제 Blueprints를 Jetpack Compose로 리팩터링한 과정에 대해 자세히 알아보겠습니다. dev-compose branch에서 전체 코드를 확인할 수 있습니다.
실제로 코드를 작성하기에 앞서, 팀원 사이에 혼선이 일어나지 않도록 마이그레이션 계획을 세웠습니다. 최종 목표는 컴포저블을 이용해 Blueprints를 화면이 있는 단일 활동 앱으로 만들고, 권장 Compose Navigation 라이브러리를 사용해 화면 사이를 이동하는 것이었습니다.
다행히도 Blueprints는 이미 Jetpack Navigation을 사용하여 Fragment로 구현된 다양한 화면 사이를 이동하는 단일 활동 앱이었습니다. Compose로 마이그레이션하기 위해, 저희는 하이브리드 앱을 권장하는 Navigation 호환성 가이드에 따라 Fragment 기반 Navigation 구성요소를 사용하고, Fragment로 뷰 기반 화면, Compose 화면, 그리고 뷰와 Compose를 모두 사용하는 화면을 구현했습니다. 아쉽지만 같은 Navigation 그래프에서는 Fragment 및 Compose 대상을 혼용할 수 없습니다.
점진적 마이그레이션의 목표는 마이그레이션 과정 내내 코드 리뷰를 용이하게 하고 제품을 출시 가능한 상태로 유지 관리 하는 것입니다. 마이그레이션 계획은 다음과 같습니다.
저희가 한 일이 바로 이것입니다! 🧑💻 2주간의 내용을 빨리 감기 ⏩ 로 돌이켜보면, 저희는 통계 화면(PR), 추가/편집 작업 화면(PR), 작업 세부정보 화면(PR), 작업 화면(PR)을 마이그레이션하고, Navigation 및 Activity 로직을 Compose로 마이그레이션한 후, 사용하지 않는 View 시스템 종속성 제거까지 마친 최종 PR을 병합했습니다.
Blueprints를 Compose로 점진적 마이그레이션 하는 방법
마이그레이션 과정에서 Compose와 관련해 몇 가지 유의할 만한 특이 사항을 확인할 수 있었습니다.
앱에 Compose를 추가하기 시작하면 Compose UI를 어설션하는 테스트에서는 Compose 테스트 API를 사용해야 합니다.
UI 테스트에서, 저희는 launchFragmentInContainer<FragmentType> API 대신에 테스트에서 문자열 리소스를 확보할 수 있게 해주는 createAndroidComposeRule<ComponentActivity> API를 사용했습니다. 이 테스트는 Espresso와 Robolectric에서 모두 실행 가능합니다. Compose에서는 이미 이 모든 것이 지원되므로 추가 변경이 필요하지 않았습니다. 예시가 필요하다면 AddEditTaskScreenTest로 마이그레이션된 AddEditTaskFragmentTest에서 코드를 비교해 보시기 바랍니다. 단, ComponentActivity를 사용하는 경우 androidx.compose.ui:ui-test-manifest 아티팩트에 의존해야 합니다.
엔드 투 엔드 또는 통합 테스트에서도 문제를 전혀 찾지 못했습니다! Espresso와 Compose의 호환성 덕분에, 저희는 Espresso 어설션을 사용해 Views를 확인하고 Compose API를 사용해 Compose UI를 확인합니다. Compose로 마이그레이션하는 과정에서 AppNavigationTest가 어떻게 구현되는지 확인해 보시기 바랍니다.
Blueprints에서 ViewModel 이벤트가 제대로 처리되지 않았습니다. Blueprints는 ViewModel에서 UI로 명령을 보내는 이벤트 래퍼 솔루션을 사용하는데, 이는 Compose에서 원활하게 작동하지 않습니다. 저희는 마이그레이션 동안 이러한 '이벤트'를 상태로 모델링했고, 최근 가이드에도 권장사항으로 포함해 두었습니다.
저희는 화면에 메시지 표시 이벤트 사용 사례를 살펴보면서 LiveData의 Event<Int> 형식을 Int?로 바꾸었습니다. 이 형식에서는 표시할 메시지가 없는 경우의 시나리오도 모델링합니다. 이러한 특정 사용 사례에서는 메시지가 표시될 때마다 UI를 통해 ViewModel을 확인해야 합니다. 다음 코드에서 두 방식의 차이점을 확인해보시기 바랍니다.
얼핏 보기에는 작업량이 더 많아 보이지만, 이 방식을 사용하면 예외 없이 메시지를 화면에 표시할 수 있습니다!
UI 코드에서 이벤트가 한 번만 처리되도록 하려면 event.getContentIfNotHandled()를 호출하는 방법이 있습니다. 이 방법은 Fragment에서는 그럭저럭 괜찮게 작동하지만, Compose에서는 완전히 무용지물이 됩니다! Compose에서는 언제든지 재구성(Recomposition)이 발생할 수 있으므로, 이벤트 래퍼는 유효한 해결책이 아닙니다. 이벤트가 처리되고 함수가 재구성되면(이 방법을 테스트하는 동안 매우 정기적으로 발생) 스낵바가 취소되어 사용자가 메시지를 놓칠 수 있습니다. 절대 발생하면 안 되는 UX 문제죠! Compose 앱에서 이벤트 래퍼 솔루션을 사용해서는 안 됩니다.
특정 시나리오에서 일부 함수를 재구성하지 않는 Compose 코드를 작성할 수도 있지만, 이 경우 이벤트 래퍼 솔루션으로 인해 UI 구현이 제한될 수 있습니다. Compose에서는 이벤트 래퍼 솔루션 사용을 권장드리지 않습니다.
아래의 코드 스니펫에서 이전 (이벤트 래퍼) 코드와 이후 (이벤트를 상태로 모델링) 코드를 비교해 보시기 바랍니다. 화면에 메시지를 표시하는 것은 UI 로직이고 화면 컴포저블이 점점 복잡해지고 있었으므로, 저희는 일반 상태 홀더 클래스를 사용하여 코드를 단순화했습니다(예: AddEditTaskState 참조).
리팩터링하는 동안 모든 것을 Compose로 마이그레이션하고 싶을 수도 있습니다. 물론 그래도 괜찮지만, 앱의 사용자 환경이나 정확성을 훼손해서는 안 됩니다. 점진적 마이그레이션의 핵심은 앱을 항상 출시 가능한 상태로 유지하는 것입니다.
일부 화면을 Compose로 마이그레이션하는 동안 저희도 이런 충동을 느꼈습니다. 그러나 너무 많은 작업을 동시에 수행할 수는 없기에, 이벤트 래퍼에서 마이그레이션하기 전에 일부 화면을 Compose로 마이그레이션했습니다. Compose에서 이벤트 래퍼를 사용하면 사용자에게 최선의 환경을 제공할 수 없으므로, 화면의 나머지 코드를 Compose에 놔둔 채 Fragment에서 해당 메시지를 계속 처리했습니다. 그 예로 마이그레이션 중 TasksFragment의 상태를 살펴보세요.
모든 것이 보이는 것처럼 순조롭지는 않았습니다. 🫤 Fragment 콘텐츠를 Compose로 변환하는 것은 간단하지만, Navigation Fragment에서 Navigation Compose로 마이그레이션하는 데 시간이 꽤 걸렸고 고려할 점도 좀 더 많았습니다.
앞으로 Compose로 더 쉽게 마이그레이션할 수 있도록 다양한 측면에서 가이드를 확장하고 개선해야 한다는 목소리도 있습니다. 이 작업이 토론의 불씨가 되어, 곧 새로운 가이드가 나오기를 바랍니다! 🎊
Navigation 초보자이자 ✋ Navigation Compose 마이그레이션을 수행한 사람으로서, 저는 다음과 같은 문제에 직면했습니다.
Navigation Fragment에서 Navigation Compose로 마이그레이션하는 건 재미있는 작업이었습니다! 실제로 프로젝트를 마이그레이션하는 시간보다 동료 리뷰를 받아 보는 시간이 더 길었다는 점도 예상치 못한 즐거움이었죠. 마이그레이션 계획을 세우고 모두가 내용을 미리 숙지한 덕분에, 조기에 기대치를 설정하고 많은 리뷰를 받을 수 있었습니다.
저희의 Compose 마이그레이션 도전기를 재미있게 읽으셨기를 바라며, 앞으로 Architecture Blueprints에서 수행할 실험 및 개선 사항도 적극적으로 공유드리겠습니다.
Compose 코드로 구현한 Blueprints를 보고 싶으시면 dev-composebranch를 살펴보세요. 그리고 점진적 마이그레이션의 PR을 살펴보고 싶으시면 아래 목록을 참조하시기 바랍니다.