이번 포스트에서는 Compose에서 매우 중요한 개념인
상태(State)와 Recomposition, 그리고 레이아웃(Layout) 기초에 대해 살펴본다.
Compose가 선언형(Declarative) UI를 지향한다는 점을 제대로 이해하기 위해서는,
상태 변화에 따른 UI 자동 갱신 과정을 익히는 것이 필수적이다.
1. Compose의 상태(State) 개념
1.1 선언형 UI와 상태 관리
Jetpack Compose는 선언형 UI 패러다임을 따르기 때문에, 화면(UI)은 “현재 데이터 상태”를 토대로 자동으로 렌더링된다.
• 예를 들어, @Composable 함수가 특정 값을 표시하고 있을 때, 그 값이 변경되면 별도의 코드(notifyDataSetChanged() 등)를 호출하지 않아도 UI가 자동으로 다시 그려진다(Recomposition).
1.2 remember와 mutableStateOf
Compose에서 상태를 간단히 만들려면, remember 함수와 mutableStateOf를 함께 사용한다.
@Composable
fun Counter() {
// count 변수를 기억하고, 상태가 변경되면 UI를 재구성한다
var count by remember { mutableStateOf(0) }
Column {
Text("Count: $count")
Button(onClick = { count++ }) {
Text("증가")
}
}
}
• remember: Composable 함수가 재호출되더라도, 이전에 저장한 값을 “기억”해두는 역할.
• mutableStateOf(0): 내부적으로 State 객체를 생성한다. count 값이 변할 때마다 Compose는 해당 부분을 다시 그려준다.
2. Recomposition이란?
2.1 Recomposition의 작동 원리
Recomposition은 상태가 바뀔 때, “화면을 다시 그려야 하는 부분(Composable 함수)”을 새로 호출하는 과정을 의미한다. Compose는 내부적으로 “상태가 변화된 UI 요소”만 효율적으로 갱신하려고 노력한다.
• Composition: 처음 Composable 함수를 호출해 UI 트리를 구성하는 과정.
• Recomposition: 이후 상태 변화에 따라, 필요한 곳만 다시 호출해 UI 트리를 갱신하는 과정.
2.2 과도한 Recomposition 방지
상태 관리가 잘못되면 의도치 않게 과도한 Recomposition이 발생할 수 있다. 예를 들어, 상위 함수에서 불필요한 변수를 remember 없이 선언하면, 하위 Composable 함수가 매번 재호출되는 일이 생길 수 있다.
• 따라서 상태를 다룰 때는 스코프(범위)와 remember 사용 위치를 신중하게 결정해야 한다.
3. Layout 기초: Box, Column, Row, LazyColumn
Compose에서 제공하는 기본 레이아웃은 이미 2편에서 간단히 소개했다. 이번에는 **상태(State)**와 함께 사용하기 좋은 몇 가지 레이아웃을 더 살펴본다.
3.1 Box
Box는 기본적으로 자식 요소를 겹쳐서 배치할 수 있도록 해준다. 또한, 각각의 자식에 Modifier.align()을 적용해 원하는 위치에 배치할 수 있다.
@Composable
fun BoxExample() {
Box(modifier = Modifier.size(100.dp)) {
Text("첫 번째", modifier = Modifier.align(Alignment.TopStart))
Text("두 번째", modifier = Modifier.align(Alignment.BottomEnd))
}
}
• 두 Text가 동일한 영역(가로 100dp, 세로 100dp) 안에서 서로 다른 구석에 배치된다.
• 겹치는 요소나 배경 이미지를 깔아놓고 그 위에 다른 Composable을 올리고 싶을 때 주로 사용한다.
3.2 Column & Row
Column과 Row는 각각 수직, 수평으로 자식을 정렬한다.
• verticalArrangement, horizontalArrangement 등을 통해 간격 조절이 가능하다.
• 상위 modifier에 padding이나 fillMaxSize()를 지정해 레이아웃 크기를 유연하게 조절할 수 있다.
@Composable
fun LayoutSample() {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.SpaceBetween
) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Text("Row 아이템 1")
Text("Row 아이템 2")
}
Text("Column의 마지막 아이템")
}
}
3.3 LazyColumn
스크롤 가능한 리스트를 구현할 때는 LazyColumn을 사용한다. 기존 RecyclerView + Adapter 구조 대신, 코드 몇 줄로 리스트를 구현할 수 있다.
@Composable
fun LazyColumnExample(items: List<String>) {
LazyColumn {
items(items.size) { index ->
Text("아이템: ${items[index]}", modifier = Modifier.padding(8.dp))
}
}
}
• 상단에서 LazyColumn을 호출하고, items() 블록 안에서 아이템별 Composable을 정의한다.
• 스크롤 처리나 ViewHolder 개념 없이도, 무한 스크롤 리스트를 깔끔하게 구성할 수 있다.
4. 간단한 예제: 상태 변화에 따른 리스트 업데이트
아래 예시에서는 버튼 클릭 시 리스트 아이템을 추가하고, LazyColumn을 통해 해당 리스트를 화면에 렌더링한다.
@Composable
fun DynamicListExample() {
// 리스트를 기억하고, mutableStateOf로 관리
var items by remember { mutableStateOf(listOf<String>()) }
Column(modifier = Modifier.padding(16.dp)) {
Button(onClick = {
// 아이템 추가
items = items + "새 아이템"
}) {
Text("아이템 추가")
}
LazyColumn(modifier = Modifier.padding(top = 8.dp)) {
items(items.size) { index ->
Text(text = "Index $index: ${items[index]}", modifier = Modifier.padding(8.dp))
}
}
}
}
• items 리스트가 변경되면, Compose가 해당 부분을 재구성(Recompose)하여 새 아이템이 화면에 추가된다.
• 별도의 Adapter나 notifyDataSetChanged() 호출 없이도, 자동 UI 갱신이 이루어지는 것이 선언형 UI의 핵심 매력이다.
5. 마무리
이번 글에서는 Jetpack Compose의 필수 개념인 상태(State)와 Recomposition, 그리고 레이아웃 기초(Box, Column, Row, LazyColumn)를 살펴보았다.
Compose를 실제 프로젝트에 적용하다 보면, 상태 관리가 곧 UI 흐름을 좌우하는 핵심이라는 점을 체감하게 된다.
• 기억하자: remember, mutableStateOf를 올바르게 활용해 필요한 데이터를 상태로 관리하면, Compose가 자동으로 화면을 재구성한다.
• 레이아웃: Box, Column, Row, LazyColumn 등 기본 레이아웃을 잘 이해해두면, 다채로운 UI 구성이 가능하다.
'코딩' 카테고리의 다른 글
Compose 애니메이션, 퍼포먼스 & 테스트 (0) | 2025.01.09 |
---|---|
Compose Navigation & 아키텍처 컴포넌트 연동 (0) | 2025.01.06 |
Material Design & 테마 커스터마이징 (1) | 2025.01.05 |
Composable 함수와 기본 UI 컴포넌트 (0) | 2025.01.02 |
Jetpack Compose 소개 & 개발환경 세팅 (0) | 2025.01.01 |