Jetpack Compose
Jetpack Compose, 안드로이드 UI 개발의 뉴 패러다임
오늘은 구글이 선보인 최신 UI 툴킷, Jetpack Compose에 대해 정리해 보기로 했다. "Compose가 대세라던데, 대체 뭐가 어떻게 좋은 걸까?", "기존의 XML 방식과 무엇이 다를까?" 궁금해하셨던 분들에게는 어느 정도 답이 될 수 있을 것 같다.
1. Jetpack Compose란 무엇인가?
Jetpack Compose는 안드로이드 네이티브 UI를 빌드하기 위한 구글의 최신 선언형(Declarative) UI 툴킷이다. 기존의 명령형(Imperative) 방식인 XML 레이아웃과 작별을 고하고, 더 적은 코드로 더 직관적이고 강력한 UI를 구축할 수 있게 해준다
Compose는 안드로이드 개발의 오랜 숙제였던 UI 개발의 복잡성을 해결하기 위해 등장했다. 이제 우리는 UI의 '상태(State)'를 코드로 기술하기만 하면, Compose가 알아서 UI를 갱신해 준다.
명령형 UI vs 선언형 UI
Compose를 이해하기 위해 가장 먼저 알아야 할 개념은 '명령형'과 '선언형'의 차이이다.
- 명령형 (Imperative) UI - 기존의 XML 방식
- "어떻게" 그릴 것인가에 집중한다.
- 개발자가 UI 컴포넌트(예: TextView)를 직접 가져와(findViewById) 상태가 바뀔 때마다 수동으로 변경(textView.text = "새로운 텍스트")해야 한다.
- UI의 상태와 로직이 분리되어 있어 코드가 복잡해지고, 상태가 많아질수록 관리하기 어려워진다.
// 명령형 방식의 예시 val myTextView = findViewById<TextView>(R.id.myTextView) myTextView.text = "Hello, Android!" // ... 버튼 클릭 후 ... myTextView.text = "Hello, Compose!"
- 선언형 (Declarative) UI - Jetpack Compose 방식
- "무엇을" 그릴 것인가에 집중한다.
- 특정 상태(State)에 따라 UI가 어떻게 보여야 하는지를 코드로 '선언'한다.
- 상태가 변경되면, Compose 런타임이 알아서 UI를 다시 그려주게 되므로(Recomposition). 개발자는 UI를 직접 제어할 필요가 없다.
// 선언형 방식의 예시 @Composable fun MyScreen() { var textState by remember { mutableStateOf("Hello, Android!") } Column { Text(text = textState) Button(onClick = { textState = "Hello, Compose!" }) { Text("Change Text") } } }
이는 단순한 코드 작성 방식의 변화를 넘어, UI 개발에 대한 우리의 사고방식 자체를 바꾸어 놓는다.
2. 왜 Jetpack Compose일까?
"이미 XML로 잘 하고 있는데, 굳이 새로운 걸 배워야 할까?"라고 생각할 수 있다. 하지만 Compose가 제공하는 명확한 이점들을 일단 알아는 둘 필요가 있다.
1) 코드 감소 및 생산성 향상
Compose는 놀라울 정도로 적은 코드로 동일한 UI를 구현할 수 있다. XML 레이아웃 파일과 이를 제어하는 Kotlin/Java 코드로 분리할 필요 없이, 오직 Kotlin 코드만으로 UI를 완성한다. findViewById, 데이터 바인딩(DataBinding) 같은 상용구(Boilerplate) 코드가 사라져 코드베이스가 훨씬 간결해진다.
2) 직관적인 개발
UI 로직이 Kotlin 코드 안에 자연스럽게 녹아들어 있다. if문으로 특정 UI를 조건부로 보여주거나, for 루프로 리스트를 그리는 등, 기존 프로그래밍 언어의 문법을 그대로 활용하여 동적인 UI를 손쉽게 만들 수 있다.
@Composable
fun UserProfile(user: User?) {
if (user != null) {
Text("Welcome, ${user.name}")
} else {
Text("Please log in.")
}
}
3) 개발 속도 가속화
Android Studio의 실시간 미리보기(Live Preview) 기능은 Compose의 가장 강력한 무기 중 하나다. 코드를 수정하는 즉시 에디터 창에서 UI 변화를 확인할 수 있어, 앱을 다시 빌드하고 실행하는 지루한 과정을 생략할 수 있다.
4) 강력하고 유연한 기능
복잡한 커스텀 뷰, 애니메이션, 제스처 등을 구현하는 것이 기존 View 시스템보다 훨씬 간단하고 유연하다. 수정자(Modifier)라는 강력한 체이닝 API를 통해 UI 컴포넌트의 속성(패딩, 배경색, 클릭 이벤트 등)을 쉽고 일관성 있게 설정할 수 있다.
5) 100% Kotlin 및 상호 운용성
Compose는 100% Kotlin으로 작성되어, 코루틴(Coroutines), 흐름(Flow) 등 최신 Kotlin 언어의 기능을 최대한 활용할 수 있다. 또한, 기존의 XML 기반 프로젝트와 완벽하게 상호 운용된다. 기존 화면에 Compose UI를 추가하거나, Compose UI 안에 기존 View를 포함하는 것이 모두 가능하여 점진적인 마이그레이션이 용이하다.
3. Jetpack Compose 핵심 개념
Compose를 제대로 사용하려면 몇 가지 핵심 개념을 반드시 이해해야 한다.
1) 컴포저블 함수 (Composable Function)
- Compose UI의 기본 빌딩 블록.
- @Composable 어노테이션을 붙인 일반적인 Kotlin 함수.
- Text, Button, Image와 같은 기본 UI 요소를 내보내거나, 다른 컴포저블을 호출하여 UI 계층을 구성한다.
- 순서에 상관없이 실행될 수 있으며, 병렬 실행도 가능. 또한, 상태가 변경되면 언제든지 다시 실행(Recomposition)될 수 있다.
2) 상태 (State)와 리컴포지션 (Recomposition)
- 상태(State): 시간이 지남에 따라 변할 수 있는 값으로, UI에 영향을 준다. Compose에서는 mutableStateOf를 사용하여 상태를 만든다.
- 리컴포지션(Recomposition): State의 값이 변경되었을 때 State 객체가 관찰하는 컴포저블 함수가 다시 호출되는 프로세스. 이 과정을 통해 UI는 최신 상태로 자동 업데이트된다.
- remember: 리컴포지션이 발생해도 상태가 초기화되지 않고 유지되도록 값을 메모리에 저장하는 역할을 한다.
@Composable
fun Counter() {
// count는 상태(State)이며, remember를 통해 리컴포지션에도 값을 유지
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) { // 버튼을 누르면 count 상태가 변경
// count가 변경되면 Text 컴포저블이 리컴포지션되어 화면이 갱신
Text(text = "Count: $count")
}
}
3) 상태 호이스팅 (State Hoisting)
컴포저블을 재사용 가능하고 테스트하기 쉽게 만들기 위한 디자인 패턴이다. 상태를 컴포저블 함수 내부에서 직접 갖는 대신, 호출하는 쪽으로 상태를 '끌어올리고(hoist)', 상태 변경을 위한 콜백 람다(lambda)를 파라미터로 받는 방식이다.
- Stateless Composable: 상태를 직접 가지지 않고, 외부에서 전달받는 컴포저블. 재사용성이 높다.
- Stateful Composable: 상태를 직접 소유하고 관리하는 컴포저블.
// Stateful Composable (상태를 직접 소유)
@Composable
fun StatefulCounter() {
var count by remember { mutableStateOf(0) }
StatelessCounter(count = count, onIncrement = { count++ })
}
// Stateless Composable (상태를 외부에서 받음)
@Composable
fun StatelessCounter(count: Int, onIncrement: () -> Unit) {
Button(onClick = onIncrement) {
Text("Count: $count")
}
}
4) 수정자 (Modifier)
컴포저블을 꾸미거나(decorate) 동작을 추가하는 데 사용되는 객체들의 집합. 점(.)을 이용한 체이닝(chaining) 방식으로 여러 속성을 순차적으로 적용할 수 있어 코드가 매우 직관적이다.
@Composable
fun DecoratedText() {
Text(
text = "Hello, Modifier!",
modifier = Modifier
.padding(16.dp) // 16dp의 패딩
.background(Color.Yellow) // 노란색 배경
.fillMaxWidth() // 가로 꽉 채우기
.clickable { /* 클릭 이벤트 처리 */ }
)
}
Jetpack Compose는 구글이 공식적으로 안드로이드 UI 개발의 미래로 선언했으며, 수많은 글로벌 앱들이 이미 Compose를 도입하여 그 안정성과 효율성을 입증하고 있다. 처음에는 선언형 패러다임이 낯설게 느껴질 수 있지만, 일단 익숙해지면 기존 XML 방식으로는 돌아가기 어려울 만큼 강력하고 편리한 개발 경험을 제공한다. 새로운 프로젝트를 Compose로 시작하거나, 기존 프로젝트에 조금씩 적용해 보면서 살살 적응해 볼 필요가 있겠다.