젯팩 컴포즈로 개발하는 안드로이드 UI을 공부하며 정리한 내용입니다.
저작권에 문제가 될 시, 글을 모두 내리겠습니다.
제가 공부한 내용이 더 많은 분들에게도 도움이 되었으면 좋겠습니다. 부족한 부분은 댓글을 통해서 피드백을 주신다면 언제나 반영하겠습니다. 감사합니다.
책에 대한 링크는 가장 아래에 있습니다.
안드로이드 뷰 시스템 살펴보기
기존 안드로이드 UI 개발은 XML로 정의된 컴포넌트 트리를 기반으로 하며, 런타임에서 이를 조작하여 UI를 변경한다.
<!-- XML을 통해 UI 구성 요소 정의 -->
- XML은 중첩된 트리 구조로 UI 요소들을 정의하며, 레이아웃과 인터랙션 가능한 위젯(버튼, 텍스트필드 등)을 함께 포함한다.
- 레이아웃 요소는 자식 요소들의 크기와 위치를 결정하는 역할을 한다.
- ScrollView처럼 일부 예외는 상호작용도 가능하다.
레이아웃 파일 인플레이팅
- Activity는 onCreate()에서 setContentView()를 호출해 XML로 작성된 UI를 인플레이트한다.
- 이후 findViewById()를 통해 컴포넌트에 접근한다.
val doneButton = findViewById<Button>(R.id.done)
- 하지만 이 방식은 초기화 전 접근 시 예외 발생, 코드 중복 증가, 변경 시 컴포넌트 추적 어려움 등의 문제를 가진다.
이를 해결하기 위해 ViewBinding이 도입되었다:
class MainActivity : AppCompatActivity() {
private lateinit var binding: MainBinding
override fun onCreate(savedInstanceState: Bundle?) {
binding = MainBinding.inflate(layoutInflater)
setContentView(binding.root)
// binding을 통해 컴포넌트 직접 접근
}
}
- XML 파일에 대응되는 …Binding 클래스가 자동 생성되어 참조를 한 번에 처리할 수 있다.
- build.gradle에서 viewBinding { enabled = true } 설정 필요.
UI 수정
private fun enableOrDisableButton() {
binding.done.isEnabled = binding.name.text.isNotBlank()
}
- 사용자가 텍스트를 입력했는지 감지하여 완료 버튼을 활성/비활성화하는 로직.
- doAfterTextChanged를 활용해 실시간 반응 가능.
- imeOptions="actionDone" + setOnEditorActionListener를 통해 키보드의 ‘완료’ 키 동작 처리 가능.
binding.name.run {
setOnEditorActionListener { _, _, _ ->
binding.done.performClick()
true
}
}
- 완료 버튼 클릭 시 조건 검사 후 인사말 출력 및 UI 숨김 처리:
- visibility = GONE
- binding.message.text = getString(...)
명령형 UI 방식의 한계 정리
- XML → 컴포넌트 트리 인플레이트 → 개별 요소 속성 직접 변경 방식
- UI 상태는 명령적으로 변경해야 하므로, 변경 추적과 유지보수가 어려워짐
- 특히 도메인 데이터 변화에 따라 UI가 동기화되어야 할 때, 관련 컴포넌트를 일일이 관리해야 하므로 복잡도 증가
- 아키텍처 지침 없이 진행하면 비즈니스 로직과 UI 로직이 뒤섞여 관리가 어려워짐
컴포넌트에서 컴포저블 함수로 이동
안드로이드에서 ‘컴포넌트’는 UI 요소를 포함해 시스템을 구성하는 독립적 단위로, 프로퍼티 설정과 메시지 전달을 통해 구성 및 상호작용이 이루어진다.
- 예: TextView는 text, visibility 등 속성으로 설정 가능
- Button은 onClickListener를 통해 클릭 이벤트 처리
- EditText도 setOnEditorActionListener()로 메시지 수신 가능
이러한 속성과 메시지 기반 UI는 드래그앤드롭 방식의 툴 기반 UI 편집에 잘 어울린다.
컴포넌트 계층구조
- 모든 컴포넌트는 고유 속성과 공통 속성(예: layout_width, layout_height)을 가진다.
- 속성은 외형/행위를 정의하며, 전문화 정도에 따라 차별화된다.
- EditText > TextView (더 구체적)
- CheckBox, Switch: 두 가지 상태를 표현할 수 있는 전문화된 버튼
- 객체지향 언어에서는 상속을 통해 이런 관계를 쉽게 표현 가능
- 그러나 UI 구성 자체는 객체지향에 국한되지 않는다
XML 태그와 속성은 실제로 클래스와 멤버 변수에 대응하며, inflate()는 이를 바탕으로 객체 트리를 생성한다.
- 레이아웃 파일은 UI 상태와 무관하게 정적인 선언을 하고, 변경은 런타임에 명령적으로 수행된다.
- 예: 텍스트가 비어 있으면 버튼을 비활성화해야 한다는 선언은 없음
즉, 전통적인 안드로이드 뷰 시스템은 명령형 패러다임이며, 젯팩 컴포즈는 상태 기반 선언형 UI로 이를 대체한다.
클래스 계층 구조 예시
안드로이드 UI 요소는 다음과 같은 상속 계층을 따른다:
ConstraintLayout
java.lang.Object
→ android.view.View
→ android.view.ViewGroup
→ androidx.constraintlayout.widget.ConstraintLayout
Button
java.lang.Object
→ android.view.View
→ android.widget.TextView
→ android.widget.Button
EditText
java.lang.Object
→ android.view.View
→ android.widget.TextView
→ android.widget.EditText
- 모든 안드로이드 UI 요소는 android.view.View를 기반으로 한다.
- 컨테이너 역할을 하는 컴포넌트(ConstraintLayout 등)는 ViewGroup을 확장한다.
- 텍스트 기반 컴포넌트는 TextView를 상속하고, 이를 통해 공통 기능을 재사용한다.
컴포넌트 계층 구조의 한계
안드로이드 UI 계층 구조는 일반적으로 상속을 통해 전문화된다. 예를 들어 Button은 TextView를, ImageButton은 ImageView를 상속받는다.
java.lang.Object
→ android.view.View
→ android.widget.ImageView
→ android.widget.ImageButton
- 텍스트 버튼은 Button, 이미지 버튼은 ImageButton으로 구성된다.
- 하지만 텍스트와 이미지를 함께 보여주는 버튼은 이 구조에서 직접적으로 만들기 어렵다.
- Button과 ImageButton은 서로 다른 상속 구조이기 때문에 공통 기능을 공유할 수 없다.
- 이는 자바의 단일 상속 제약 때문이다.
또한, 예를 들어 마진은 ViewGroup에, 패딩은 View에 정의되어 있어
특정 기능을 위해 원하지 않는 모든 기능을 함께 상속받게 되는 구조적 한계가 있다.
기능 재활용이 어렵고, 기능 분리가 불가능한 구조가 클래스 기반 UI 프레임워크의 핵심적인 한계다.
구성(Composition)을 중심으로 한 접근
이 문제를 해결하기 위해 플러터(Flutter)는 상속보다는 구성(composition over inheritance) 원칙을 채택했다.
- UI 요소의 동작과 외형을 Container, Padding, Align, GestureDetector와 같은 작은 구성 요소를 조합해 정의
- 더 이상 부모 클래스를 수정하거나 상속하지 않아도 됨
Jetpack Compose 역시 컴포저블 함수 기반의 구성 방식을 따르며, UI를 조합하는 방식으로 동작한다.
상속 제한의 실제 사례
자바나 코틀린에서는 특정 클래스의 상속을 명시적으로 금지할 수 있다.
- final 클래스 (Java)
- open 키워드가 없는 클래스 (Kotlin)
예:
- android.widget.Space: 뷰 간 간격을 위한 final 클래스
- android.view.ViewStub: 지연 인플레이트 용도의 final 클래스
이들은 대부분 확장 필요성이 없기 때문에 문제되지 않지만,
프레임워크 자체가 상속 구조에 의존한다면 잠재적 제약이 될 수 있다.
반면, 컴포지션 중심 프레임워크(Compose, Flutter 등)에서는
이러한 제한은 전혀 문제가 되지 않음
함수를 통한 UI 구성
팩토리얼 계산 함수는 아래와 같이 정의된다:
fun factorialAsString(n: Int): String {
var result = 1L
for (i in 1..n) {
result *= i
}
return "$n! = $result"
}
- 입력값 n에 대해 1부터 n까지 곱한 결과를 문자열로 반환
- Long 타입 한계를 넘는 경우 정상 작동하지 않음
@Composable
fun Factorial() {
var expanded by remember { mutableStateOf(false) }
var text by remember { mutableStateOf(factorialAsString(0)) }
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
modifier = Modifier.clickable {
expanded = true
},
text = text,
style = MaterialTheme.typography.h2
)
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
for (n in 0 until 10) {
DropdownMenuItem(onClick = {
expanded = false
text = factorialAsString(n)
}) {
Text("${n.toString()}!")
}
}
}
}
}
- Factorial()은 Box 레이아웃을 기반으로 중앙에 Text와 DropdownMenu를 배치함
- Text() 클릭 → expanded = true로 드롭다운 표시
- 메뉴에서 아이템 클릭 → 텍스트 갱신 및 드롭다운 닫힘
- DropdownMenu는 목록 UI 제공, DropdownMenuItem으로 각 항목 구성
- 상태 변수 expanded, text는 mutableStateOf()로 선언되고 remember를 통해 기억됨
- 이 변수들이 변경되면 컴포저블 함수는 **자동 재구성(recomposition)**된다
선언적 UI의 핵심 원리
- 컴포넌트 기반 XML UI는 상태와 무관하게 초기 구조만 정의
- 반면, 컴포즈 UI는 실제 상태 기반으로 항상 최신 상태의 UI를 선언함
- 즉, UI는 상태(state)와 직접 연결되어 있으며 UI = 함수(상태) 형태로 구성됨
상태와 재구성
- text, expanded는 UI 상태
- 이 값이 바뀌면 컴포즈 런타임은 해당 컴포저블 함수만 재실행하여 UI를 갱신
- 예: 드롭다운 열기/닫기, 텍스트 값 변경 등
아키텍처 관점에서 설명
컴포넌트 기반 vs 컴포저블 함수 기반
- 기존 안드로이드 뷰 시스템은 클래스 기반 상속 구조를 따르며, 기본 시각/상호작용 기능은 상위 클래스에 구현된다.
- 반면, 컴포저블 함수는 공유 속성 없이 구성되며, @Composable 어노테이션으로만 해당 UI 함수임을 지정한다.
- 이로 인해 구조적으로 자유로우며, 예측 가능한 단순한 API 제공이 가능하다.
클릭 동작에 반응
기존 안드로이드:
- setOnClickListener() 사용
- clickable 속성과 함께 사용해야 정상 작동
컴포즈에서는 2가지 방법 제공:
1. 전용 onClick 매개변수 사용:
@Composable
@Preview
fun ButtonDemo() {
Box {
Button(onClick = {
println("clicked")
}) {
Text("Click me!")
}
}
}
- onClick은 필수 매개변수이며, 클릭 불가 상태로 설정하려면 enabled = false 사용
Button(onClick = { println("clicked") }, enabled = false)
2. modifier.clickable {} 사용:
Text(
text = "Hello",
modifier = Modifier.clickable { /* 클릭 처리 */ }
)
- Text()처럼 기본적으로 클릭 이벤트를 처리하지 않는 컴포저블에 클릭 기능을 부여
Modifier는 시각적 스타일과 동작을 조정하는 컴포저블 함수의 확장 도구
UI 요소 크기 조절과 배치
전통 UI 프레임워크:
- ViewGroup 하위 클래스들(LinearLayout, FrameLayout 등)이 자식 컴포넌트의 배치와 크기 지정을 담당
컴포즈에서는 다음과 같은 레이아웃 컴포저블을 제공:
- Row(), Column(): 수평/수직 배치
- Box(): 겹치는 뷰 배치 (FrameLayout과 유사)
@Composable
@Preview
fun BoxDemo() {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Box(
modifier = Modifier
.size(width = 100.dp, height = 100.dp)
.background(Color.Green)
)
Box(
modifier = Modifier
.size(width = 80.dp, height = 80.dp)
.background(Color.Yellow)
)
Text(
text = "Hello",
color = Color.Black,
modifier = Modifier.align(Alignment.TopStart)
)
}
}
Modifier 기능 요약:
- Modifier.fillMaxSize() : 가능한 최대 크기 차지
- Modifier.size() : 고정 크기 설정
- Modifier.align() : 개별 요소의 정렬 위치 지정
- Modifier.background() : 배경 색상 설정
체이닝 가능한 구조이며, 시작점은 항상 Modifier 객체다.
커스텀 Modifier도 정의 가능해, UI 확장성이 매우 높다.
도서 링크 바로가기
https://www.yes24.com/Product/Goods/116413696
젯팩 컴포즈로 개발하는 안드로이드 UI - 예스24
젯팩 컴포즈는 안드로이드 UI 개발의 새로운 패러다임이다. 이 책은 젯팩 컴포즈를 통해 안드로이드 애플리케이션을 개발할 수 있도록 도와줄 것이다. 젯팩 컴포즈로 안드로이드를 처음 개발해
www.yes24.com
'Android > Study' 카테고리의 다른 글
| [Jetpack Compose로 개발하는 안드로이드 UI] 4장. UI 요소 배치 (0) | 2025.05.13 |
|---|---|
| [Jetpack Compose로 개발하는 안드로이드 UI] 3장. 컴포즈 핵심 원칙 자세히 알아보기 (0) | 2025.05.07 |
| [Jetpack Compose로 개발하는 안드로이드 UI] 1장. Jetpack Compose 기본 요소 정리하기 (0) | 2025.04.15 |
| [Android] Gson vs Moshi vs kotlinx.serialization Deep Dive (0) | 2025.03.26 |
| [Android] Retrofit의 내부구조와 동작 알아보기 (0) | 2025.03.26 |