안녕하세요. AOS 개발 Unit, Jasper 입니다.
이번에는 Android의 새로운 UI 툴킷인 Compose에 대한 간략한 소개와 함께, FLO에서 이를 도입하며 얻은 경험에 대해 이야기해보겠습니다.
등장배경
Jetpack Compose는 안드로이드 UI를 선언적으로 구축하는 도구로, 기존의 View 방식(XML + Java/Kotlin)에서 발생하는 다양한 문제를 해결하기 위해 등장했습니다. 이를 위한 주요한 이유는 다음과 같습니다.
UI 개발에서의 높은 복잡성 : XML을 통해 UI를 구성은 간단한 UI는 효과적이지만, 복잡한 UI를 다룰 때는 관리가 어려워집니다. 특히, View의 지나치게 중첩된 계층구조는 성능 저하를 문제를 일으킵니다. 더구나, UI 요소와 코드가 분리돼 있지만, 이 둘 간의 밀접한 로직 결합은 코드의 가독성과 재사용성을 떨어뜨리며 유지보수를 어렵게 만듭니다.
상속으로 인한 문제 : Android가 초기 출시 이후, 사용자들의 요구가 높아짐에 따라 다양한 UI 요소들이 추가되었습니다. 이에 따라서, 공통적으로 사용되는 UI 속성들을 효율적으로 재사용하기 위해 View.java 클래스를 만들어 UI 요소들이 이를 상속받도록 설계했습니다. 초기에는 편리해 보였지만, 시간이 지나면서 유지보수가 어려워졌으며, 새로운 UI 요구사항이 발생할 때마다 View.java 클래스를 수정해야 했습니다. 이로 인해 서로 다른 UI 요구사항을 충족시키기 위해 코드의 복잡성이 늘어나고 문제가 발생했습니다.
Compose는 이러한 문제들을 해결하기 위해 Kotlin 파일에서 선언적으로 UI를 작성합니다. 더불어 상속 대신 합성을 채택하며, UI 개발의 복잡성을 줄이고 유지보수성을 향상시켰습니다.
Compose?
Compose는 선언형(Declarative) UI 프레임워크로서, ‘무엇을 보여줄 것인가?’를 기술하고 구현의 세부 사항은 시스템이나 프레임워크에 위임하는 선언형 프로그래밍 방식을 따릅니다. Kotlin을 사용해 컴포저블(Composable) 함수라는 독립적인 단위로 UI를 구성하므로 직관적이며 재사용성이 높은 코드를 만들 수 있습니다. 또한, 복잡한 UI도 이 함수들을 조합해 만들 수 있으므로 개발 시간도 단축시켜줍니다.
다음은 FLO의 캐릭터 수정 화면입니다. 코드를 통해 Compose와 기존 방식을 비교해보겠습니다.
먼저, 기존 View 방식입니다. 타이틀과 리스트 영역을 그릴 XML 파일 하나와 리스트 아이템에 해당하는 XML 파일을 각각 만들어 UI를 작성해줍니다.
UI 작업이 끝났다면, 캐릭터 리스트를 표시하기 위해 Repository로부터 데이터를 가져와 Adapter를 생성하고, 그것을 RecyclerView에 연결합니다. 마지막으로, 아이템에 해당하는 ViewHolder를 생성한 뒤, XML을 불러와 데이터를 UI에 맞게 표현하면 작업은 완료됩니다. 이 화면은 간단해 보일 수 있지만, 실제 구현은 복잡하며 많은 파일과 코드가 필요합니다.
다음은 Compose 방식으로 구현해보겠습니다. UI 작성은 컴포저블 함수라 불리우는 @Composable 어노테이션이 붙은 함수에서만 가능합니다. 그리고 위 화면을 구현하는데 필요한 코드는 이게 전부입니다.
이와 같이 기존 View 방식에서는 간단한 화면이어도 많은 양의 코드를 작성해야 했습니다. 하지만 Compose를 사용하면 선언적인 방식으로 UI를 구성할 수 있어, 필요한 코드 양이 상당히 줄어듭니다. 이를 통해 개발자는 더 직관적이고 명확한 코드를 작성할 수 있게 되며, UI 요소들을 더욱 쉽게 조합하고 재사용할 수 있습니다. 게다가 Compose는 상태 변화를 자동으로 감지하여 UI를 업데이트하므로, 데이터와 UI 간의 일치를 유지하는 데 도움이 됩니다.
Compose를 도입한 경험
FLO 앱은 모바일 뿐만 아니라 Wear OS, Android Auto, Android Automotive, Android TV 및 Google Cast 플랫폼을 모두 지원하고 있습니다. 특히 2023년에는 Wear OS 및 Android TV용 FLO 앱이 Compose로 개발되었는데, 이에 대해 이야기해보겠습니다.
Wear OS
View 기반으로 개발된 Wear OS용 FLO 앱(이하 FloWear)은 다른 앱들과 비교했을 때 화면이 적고 단순하여 Compose를 처음 도입하기에 매우 적합했습니다. 게다가 Compose로 마이그레이션함으로써 APK 크기와 빌드 시간을 줄일 뿐만 아니라 런타임 성능도 향상시킬 수 있었기에 미룰 이유도 없었습니다. 그래서 그해 초부터 FloWear의 본격적인 Compose 마이그레이션 작업을 시작했고, 초기에는 모든 것이 원활하게 진행되는 것 같았습니다. 그러던 중 단독 재생의 메뉴를 개발하며 큰 문제가 발생했습니다.
기존에 사용된 리스트 구현 방식은 RecyclerView를, Compose에서는 LazyColumn과 LazyRow 같은 Lazy list를 활용합니다. 이 두 방식 사이에는 여러 차이점이 있지만, 주요한 차이 중 하나는 RecyclerView가 View를 재사용하여 화면을 구성하는 데 반해 Lazy list는 화면에 보이는 아이템만을 생성하고 재사용하지 않는다는 것입니다. 물론, 일반적으로 Android View 인스턴스를 새로 생성하는 것보다 비용이 적으므로 성능 향상된다고 말하지만, 실제 구현을 통해 확인해본 바에 의하면 그렇지만도 않았습니다.
다음은 위에서 언급한 문제에 관한 것으로, FloWear에서 Compose로 마이그레이션한 단독 재생 메뉴입니다. 빠르게 상단 또는 하단으로 스크롤링 하고 있으나 상대적으로 성능이 떨어지는 Wear OS 기기에서는 스크롤이 부드럽지 않고, 가끔씩 버벅거립니다. 또한, 음원이 재생 중일 때는 음악이 끊기기도 합니다.
저희는 이 문제를 해결하기 위해 기존의 View 방식과 Compose를 혼합하여 사용하기로 결정했습니다. Compose에서도 상호 운용성 API를 지원하며 AndroidView 컴포저블 함수를 활용해 RecyclerView를 래핑하여 구현할 수 있었습니다. 이를 통해 문제들은 해결할 수 있었지만, 결국 모든 XML을 제거하지는 못했고 완전한 Compose로의 마이그레이션은 이룰 수 없었습니다. 다만, Compose에 대한 지식과 숙련도가 쌓였다는 점에 있어 큰 의의를 두고 있습니다.
Android TV
2023년 10월에 새롭게 출시된 Android TV용 FLO 앱(이하 FloTV)은 Compose로 개발되었습니다. 앞선 FloWear와 다르게 기기 성능 면에서 더 뛰어나며 Compose를 사용함에 있어 문제가 없다고 판단했기 때문입니다. 그러나 일반적으로 TV용 앱을 개발할 때 사용하는 Leanback은 Compose와 함께 활용할 수 없어서 필요한 기능을 직접 구현해야 했습니다. 특히, TV는 모바일과는 달리 리모콘을 통해 동작하기 때문에 포커스 처리가 매우 중요한 요소인데, 이 부분에 관하여 간단하게 살펴보겠습니다.
Compose에서는 FocusRequester를 사용하여 포커스를 처리합니다. UI 요소에 포커스를 설정하려면 FocusRequester를 등록하고 requestFocus()를 호출하면 됩니다. 그리고 이를 활용해 자연스러운 포커스 처리를 위하여 다음과 같이 규칙을 적용했습니다.
UI 요소를 그룹으로 묶어 포커스를 관리합니다.
그룹은 다른 그룹을 자신의 하위에 배치할 수 있습니다.
그룹 간의 이동 시, 이전 그룹의 마지막 포커스 위치를 기억합니다.
아래 이미지는 위 규칙을 바탕으로 이해하기 쉽도록 UI를 구성한 것입니다. 녹색 계열의 사각형 박스는 FocusLayout으로 그룹을 나타내며, 노란색 사각형 박스는 FocusItem으로 실제로 포커스를 받는 UI 요소입니다.
이미지에서 메뉴의 오른쪽을 보면 FocusLayout 안에 여러 FocusLayout이 존재하고 있는 것을 보실 수 있습니다. 이는 리모콘을 통해 상/하단으로 포커스 이동 시에 그룹 단위로 화면을 이동시킬 때 유용합니다.
포커스 처리를 위한 알고리즘은 매우 단순합니다. FocusLayout과 FocusItem 컴포저블 함수는 각각 FocusRequester를 가지고 있습니다. 포커스를 받으면 FocusLayout은 하위 요소에 포커스를 재귀적으로 전파하며, 이 과정에서 자신의 상위 요소에 FocusRequester를 전달해 우선 순위를 맨 앞으로 이동시켜 포커스 위치를 저장합니다.
아래는 FocusLayout과 FocusItem 컴포저블 함수의 코드 일부입니다. 주석과 함께 위에서 설명한 규칙을 참고하면 이해가 더 쉬울 것입니다.
개발 경험을 토대로 본 Compose 장단점
FloWear와 FloTV를 개발하며 얻은 경험을 바탕으로 Compose의 장단점을 적어보았습니다.
장점
선언적 UI : Kotlin 기반의 선언적 방식으로 UI를 작성할 수 있습니다. 이는 코드를 더 읽기 쉽고, 유지보수를 용이하게 만듭니다.
State 관리 : Compose는 상태 변화에 반응하여 UI를 자동으로 업데이트합니다. 따라서, 개발자가 직접적으로 관리할 필요가 없습니다.
커스터마이징 및 재사용성 : Compose는 상속이 아닌 합성 개념을 택했습니다. 작은 컴포저블 함수로 UI를 작성할 수 있으며, 이를 레고처럼 조립하여 커스텀 뷰를 쉽게 생성할 수 있습니다. 또한, 이는 재사용성을 높일 수 있습니다.
동적 UI 처리와 애니메이션 : Compose는 UI 요소의 동적인 변경과 애니메이션 처리를 쉽게 구현할 수 있도록 도와줍니다. AnimatedVisibility, animateContentSize 등의 함수를 사용하여 애니메이션을 손쉽게 적용할 수 있습니다.
단점
러닝 커브 : UI 개발은 간단하지만, Compose에는 몇 가지 어려운 개념이 존재합니다. 예를 들어 Side effect는 코드 실행에 영향을 미치는 외부 요소에 관한 것으로 이해가기 쉽지 않았습니다. 또한, 성능 저하를 초래할 수 있는 Recomposition을 최적화하는 데 초기에 시행착오도 있었습니다. 이러한 개념을 숙지하고 실전 경험을 통해 익숙해져야 합니다.
안정성과 성숙도 : Compose는 아직 초기 단계이며, 빠르게 발전하고 있어 API 변동이 있을 수 있습니다. 이에 따라 코드를 업데이트해야 할 수 있습니다.
라이브러리의 한계 : Compose는 번들이 아닌 라이브러리로 제공됩니다. 이는 앱을 설치할 때 완전히 컴파일되지 않는다는 의미입니다. 이러한 문제를 해결하기 위해 앱은 Baseline profile을 도입하여 개선할 필요가 있습니다.
부족한 IDE 지원 : XML 대비하여 IDE의 미리 보기 기능이 부족하며, 이는 코드 생산성을 저하시키는 요소 중 하나입니다. 하지만 Compose 개발에 적응되면 뇌에서 미리 보기 기능이 지원되므로 크게 걱정하실 필요는 없습니다.
마치며
FLO 앱을 Wear OS와 Android TV를 위해 Compose를 활용하여 개발하면서, 코드 생산성이 크게 향상된다는 것을 경험했습니다. 그러나, 아직까지 Compose 개발을 위한 IDE 지원이 많이 부족한 실정입니다. IDE에서 더 나은 지원과 최적화가 이루어진다면, Compose를 활용한 개발 생산성이 더욱 향상될 것으로 예상됩니다. 이러한 발전에 기대를 가지고 앞으로의 Compose 기술 발전을 더 크게 기대하고 있습니다.
긴 글 읽어주셔서 감사합니다.
Comments