안녕하세요! 지금까지 Text, Button, Image, Column, Row 등을 이용해서 화면에 정적인 UI를 그리는 법에 대해 소개했습니다. 하지만 앱은 사용자와 상호작용하며 UI가 계속 변화하게 됩니다. Compose에서는 상태(state)라는 핵심 개념을 이용해서 이것을 가능하게 합니다.
상태(state)란?
Compose에서 상태는 시간이 지남에 따라 변할 수 있는 모든 값을 의미합니다.
- 체크박스의 체크 여부(boolean)
- 사용자가 입력한 텍스트(String)
- 버튼을 누른 횟수(Int)
- 서버에서 받아온 데이터 목록(List <Data>)
이 모든 것이 상태가 될 수 있습니다. Compose에서는 상태가 변경되는 것을 감지하면 해당 상태를 사용하는 UI 부분을 자동으로 다시 그려줍니다. 이 과정을 Recomposition(재구성)이라고 부릅니다.
Compose는 상태가 변경되는 것을 어떻게 감지할까?
Compose에서는 mutableStateOf와 remember라는 두 가지 핵심 함수를 사용해서 변경을 감지합니다.
- mutableStateOf(): Compose가 변화를 추적할 수 있는 특별한 상태 객체를 만드는 함수
- remember { }: Compose가 재구성될 때 값이 초기화되지 않고 기억되도록 합니다.
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
@Composable
fun CounterApp() {
// 1. mutableStateOf로 '상태'를 만들고, remember로 기억합니다.
// 'by' 키워드를 사용하면 .value 없이 값을 바로 쓸 수 있어 편리합니다.
var count by remember { mutableStateOf(0) }
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(text = "버튼을 누른 횟수: $count")
Button(onClick = {
// 2. 버튼을 누르면 상태(count)의 값이 변경됩니다.
count++
}) {
Text(text = "클릭하세요!")
}
}
}
var count by remember { mutableStateOf(0) }: count라는 이름의 상태 변수를 선언하고 초기값을 0으로 설정했습니다. remember로 감싸주었기 때문에, count 값이 1, 2, 3으로 변해도 화면이 재구성될 때 다시 0으로 돌아가지 않습니다.
count++: 버튼이 클릭되면 count 상태의 값이 1 증가합니다.
Compose는 count가 변경된 것을 감지하고, count를 사용하고 있는 Text(text = "...") 부분을 자동으로 다시 그립니다.
원시타입용 상태 홀더
아마도 mutableStateOf(0)를 사용했다면 Prefer mutableIntStateOf instead of mutableStateOf 워닝을 만나게 될 것입니다.
Compose에서는 Int, Long, Float, Double에 대한 원시타입용 상태 홀더를 제공하고 있습니다.
- mutableIntStateOf(초기값): Int 타입을 위한 상태
- mutableLongStateOf(초기값): Long 타입을 위한 상태
- mutableFloatStateOf(초기값): Float 타입을 위한 상태
- mutableDoubleStateOf(초기값): Double 타입을 위한 상태
// 1. 일반적인 방법
val countGeneric by remember { mutableStateOf(0) }
// 2. 원시 타입 특화 방법
val countPrimitive by remember { mutableIntStateOf(0) }
두 코드의 사용법은 완전히 동일하지만, 내부적으로는 중요한 차이가 있습니다. 바로 박싱(Boxing)의 유무입니다.
박싱(Boxing)이란?
Java와 Kotlin의 세계에서 Int, Float 같은 원시 타입은 객체(Object)가 아닙니다. 하지만 mutableStateOf<T>와 같은 제네릭(Generic) 함수는 모든 타입을 다루기 위해 내부적으로 객체(Any?)로 값을 다룹니다.
이때, 원시 타입인 Int를 객체 타입인 Integer로 포장하는 과정이 발생하는데, 이것을 박싱이라고 합니다.
- mutableStateOf(0): Int 값 0이 Integer 객체로 박싱되어 저장됩니다.
- mutableIntStateOf(0): Int 값 0이 박싱 과정 없이 원시 타입 그대로 저장됩니다.
박싱은 아주 작은 메모리 할당과 처리 비용을 유발합니다. 앱의 상태가 몇 개 안 되고 가끔 업데이트된다면 이 차이는 무시해도 좋을 만큼 미미합니다.
하지만 상태 변경이 매우 잦은 UI(예: 복잡한 애니메이션, 실시간으로 변하는 그래프, 스크롤에 따라 계속 계산되는 값)에서는 이런 작은 오버헤드가 쌓여 앱의 버벅거림(Jank)을 유발할 수 있습니다.
mutableIntStateOf와 같은 특화된 상태 홀더는 이 박싱을 피함으로써 불필요한 메모리 할당을 줄여 성능을 극대화합니다.
블로그 글에 대해 궁금한 점이 있다면 아래 카카오톡 오픈채팅에 들어와서 질문해주세요
Android Kotlin Compose QnA
open.kakao.com
'개발 > Compose' 카테고리의 다른 글
(Compose) LazyColumn (3) | 2025.08.05 |
---|---|
(Compose) Modifier (2) | 2025.08.04 |
(Compose) @Preview (2) | 2025.08.04 |
(Compose) Row, Column (2) | 2025.07.30 |
(Compose) Image (1) | 2025.07.29 |