Jetpack Compose 초심자 가이드 4: 상태(State) 관리와 Recomposition 이해하기 🚀

윤성현·2025년 5월 11일
post-thumbnail

📌 개요

앞선 글에서 우리는 Text, Button, Column 등 기본적인 컴포저블과 Modifier를 활용한 UI 구성을 배웠습니다. 하지만 실제 앱에서는 단순히 정적인 화면만으로는 부족합니다. 사용자의 입력이나 외부 데이터 등 시간에 따라 변하는 정보, 즉 상태(state) 를 다루는 방법이 핵심입니다.

이번 글에서는 Jetpack Compose에서 상태를 어떻게 정의하고, 변화하는 상태에 따라 UI가 어떻게 자동으로 갱신(Recomposition)되는지를 예제를 통해 함께 배워보겠습니다.


1. Compose에서 상태(state)란?

1-1. 상태란?

앱을 만들다 보면, 사용자의 입력이나 버튼 클릭 등에 따라 화면이 바뀌어야 하는 상황이 많습니다. 예를 들어, 버튼을 누를 때마다 숫자가 올라가는 앱을 만든다고 가정해 봅시다. 여기서 "숫자"는 계속 바뀌는 값이죠. 바로 이런 변할 수 있는 데이터를 Jetpack Compose에서는 상태(state) 라고 부릅니다.

조금 더 정확히 말하면, 상태는 UI에 영향을 주는 데이터입니다. 이 값이 바뀌면 Compose는 자동으로 관련된 UI를 다시 그려주는데, 이 과정을 Recomposition(재구성) 이라고 부릅니다.


1- 2. 상태를 그냥 변수로 선언하면 안 될까요?

( ex. var count = 0)

이렇게 하면 값은 바뀔 수 있지만, Compose는 그 값이 바뀌었는지 추적하지 못합니다. 즉, UI는 자동으로 바뀌지 않습니다.

Compose는 상태 변화를 감지하고 리컴포지션을 일으키기 위해, 특별한 방식으로 상태를 선언해야 합니다. 바로 이때 사용하는 것이 remembermutableStateOf입니다.

  • mutableStateOf(...)
    • 이 값은 “변할 수 있는 상태”라는 의미를 부여합니다.
    • 내부적으로 Compose가 이 값을 관찰하게 해주며, 값이 바뀌면 UI를 다시 그리게 됩니다.
  • remember { ... }
    • 컴포저블 함수가 다시 실행되는 리컴포지션이 발생하더라도 이 값을 기억하게 만듭니다.
    • 즉, 화면이 재구성되어도 값이 유지되게 합니다.

이 두 가지를 함께 써야 Compose는 "이건 상태값이니까 내가 잘 추적해서 화면을 갱신해야겠군!" 하고 자동으로 UI를 바꿔줍니다.


1-3. 예제로 이해하기: 카운터 앱 만들기

@Composable
fun CounterExample() {
    var count by remember { mutableStateOf(0) }

    Column(modifier = Modifier.padding(16.dp)) {
        Text(text = "현재 카운트: $count", fontSize = 20.sp)
        Spacer(modifier = Modifier.height(8.dp))
        Button(onClick = { count++ }) {
            Text("증가")
        }
    }
}

이 코드는 아주 단순한 카운터 앱입니다.

  • count는 숫자의 상태입니다. 처음엔 0에서 시작합니다.
  • 버튼을 누르면 count++로 값이 1씩 증가합니다.
  • 중요한 건, 이 숫자가 바뀔 때마다 Text("현재 카운트: $count") 부분이 자동으로 다시 실행된다는 점입니다.

위 GIF에서는 버튼을 누를 때마다 숫자가 실시간으로 증가하는 모습을 보여줍니다. 이는 상태 변화에 따라 UI가 자동으로 업데이트되는 Compose의 특징을 잘 보여줍니다.


📌 요약

  • 상태(state) = UI에 영향을 주는 변할 수 있는 데이터
  • 상태를 만들려면 mutableStateOf()로 만들고, remember로 기억시켜야 한다
  • 상태가 바뀌면 → Compose는 관련된 UI만 자동으로 다시 그려준다 (Recomposition)
  • 전체 화면이 다시 그려지는 게 아니라, 필요한 부분만 효율적으로 갱신된다

2. 상태를 관리하는 두 가지 방법

앞에서 우리는 remembermutableStateOf를 함께 사용해 기본적인 상태를 선언하고 UI를 자동으로 갱신하는 방식을 살펴봤습니다. 이 조합은 Compose에서 가장 많이 사용되는 기본 형태이지만, 모든 상황에 항상 이 방식만으로 충분하진 않습니다.

Jetpack Compose에서는 상태를 정의하고 관리하는 다양한 방법을 제공합니다. 이번에는 상태를 정의하는 두 가지 대표적인 방식인remember + mutableStateOf, rememberSaveable + mutableStateOf 비교하며 정리해보겠습니다.

각 방식은 쓰임새가 뚜렷하게 다르기 때문에, 상황에 따라 어떤 방식을 선택해야 할지 판단하는 기준을 아는 것이 매우 중요합니다. 예제를 중심으로 개념과 함께 이해해봅시다!


2-1. remember + mutableStateOf

가장 기본적이고 자주 사용되는 상태 선언 방식

Compose에서 상태를 가장 간단하게 선언하는 방법은 remembermutableStateOf를 함께 사용하는 것입니다. 이 조합은 컴포저블 함수 안에서 지역 상태를 선언하고, 해당 상태가 바뀔 때 UI를 자동으로 다시 그리도록 만들어 줍니다.

@Composable
fun GreetingBox() {
    var name by remember { mutableStateOf("Android") }

    Column(modifier = Modifier.padding(16.dp)) {
        Text(text = "안녕하세요, $name 님!")
        Spacer(modifier = Modifier.height(8.dp))
        Button(onClick = { name = "Jetpack Compose" }) {
            Text("이름 바꾸기")
        }
    }
}

코드 해설

  • mutableStateOf("Android")name이라는 값을 Compose가 추적할 수 있는 상태로 만들어줍니다. 이 값이 바뀌면 관련된 UI를 자동으로 다시 그리게 됩니다.
  • remember { ... }는 컴포저블 함수가 리컴포지션되어도 값을 계속 유지하도록 만듭니다.
  • 버튼을 클릭하면 name의 값이 "Jetpack Compose"로 바뀌고, 그에 따라 Text 컴포저블이 자동으로 다시 실행되어 새로운 이름이 화면에 표시됩니다.

📌 언제 사용하나요?

  • 화면 회전과 무관한 간단한 UI 상태를 선언할 때
  • 예: 버튼 클릭 횟수, 간단한 입력값, 토글 상태 등

2-2. rememberSaveable + mutableStateOf

값이 화면 회전이나 프로세스 재시작 이후에도 유지되어야 할 때

remember는 컴포저블 함수가 리컴포지션될 때 상태를 유지해주긴 하지만, 화면 회전이나 다크모드 변경처럼 Activity 자체가 다시 만들어지는 경우에는 값이 초기화됩니다.

왜냐하면 구성 변경이 발생하면 Activity가 재생성되면서 컴포지션 자체가 새로 시작되기 때문입니다. 새로운 컴포지션이 시작되면 이전 컴포지션에서 remember로 저장했던 모든 값들이 사라지고, 처음부터 다시 시작하게 됩니다.

이런 상황에서 상태를 안전하게 유지하고 싶다면, rememberSaveable을 사용하는 것이 좋습니다. 이 도구는 내부적으로 Android의 Bundle을 활용해 값을 저장하고, Activity가 다시 생성될 때 이를 자동으로 복원해줍니다.

@Composable
fun InputForm() {
    var email by rememberSaveable { mutableStateOf("") }

    Column(modifier = Modifier.padding(16.dp)) {
        TextField(
            value = email,
            onValueChange = { email = it },
            label = { Text("이메일 입력") }
        )
        Spacer(modifier = Modifier.height(16.dp))
        Text(text = "입력한 이메일: $email")
    }
}

특징

  • String, Int, Boolean처럼 기본 타입은 자동 저장 가능
  • Parcelable 혹은 Serializable 구현체도 저장 가능

📌 언제 사용하나요?

  • 사용자의 입력값이 화면 회전 시 초기화되면 곤란할 때
  • 폼, 탭 상태, 체크박스 값, 스크롤 위치 등을 유지하고 싶을 때

2-3. 정리: 두 가지 방식 비교

방식상태 유지 범위사용 목적
remember + mutableStateOf리컴포지션 간 유지됨간단한 UI 상태 (클릭 수, 입력 등)
rememberSaveable회전 등 화면 재생성에도 유지됨사용자 입력, 폼 상태 등 유지 필요할 때

3. 상태가 바뀌면 화면은 어떻게 바뀌나요? (Recomposition)

Jetpack Compose는 선언형 UI(Declarative UI) 방식으로 동작합니다.

즉, “지금 UI가 어떻게 보여야 하는지”를 코드로 선언하면, Compose가 그 선언을 바탕으로 화면을 자동으로 구성하고, 상태가 바뀌었을 때 필요한 부분만 다시 그려줍니다. 이 과정을 리컴포지션(Recomposition) 이라고 부릅니다.


3-1. 선언형 vs 명령형 방식

기존 Android View 시스템(XML 기반)은 명령형 방식(Imperative UI)이었습니다. 값이 바뀌면 개발자가 직접 View를 찾아서 수동으로 업데이트해야 했습니다.

// 기존 방식 (명령형 방식)
textView.text = "안녕하세요, 홍길동 님!"

하지만 Compose에서는 UI가 상태에 따라 자동으로 반응하기 때문에, 이렇게 직접 갱신하는 코드가 필요 없습니다.


3-2. 예제로 보는 Recomposition

var name by remember { mutableStateOf("Android") }

Text("안녕하세요, $name 님!")

이 코드는 처음 렌더링될 때 다음과 같은 화면을 보여줍니다:

안녕하세요, Android 님!

이후 어떤 동작(예: 버튼 클릭)을 통해 name = "Compose"로 값을 바꾸면, Compose는 다음처럼 동작합니다:

  1. name 상태가 변경됨
  2. 이 상태를 참조하고 있는 Text() 컴포저블이 자동으로 다시 실행됨
  3. 화면에는 다음과 같이 업데이트된 텍스트가 표시됨:
안녕하세요, Compose 님!

단지 name 값을 변경했을 뿐인데, Compose가 관련 UI를 자동으로 다시 그려줍니다.

UI 갱신을 위해 별도로 TextView.text = ... 같은 코드를 쓸 필요가 없습니다!


3-3. 리컴포지션의 특징

  • 상태가 바뀔 때마다 해당 상태를 사용하는 컴포저블 함수가 자동으로 다시 실행(Recomposition) 됩니다.
  • 하지만 화면 전체를 다시 그리는 것이 아니라, 변경된 상태를 참조하는 UI만 효율적으로 갱신됩니다.
  • Compose는 어떤 컴포저블을 다시 실행해야 하는지 매우 정교하게 판단합니다.

3-4. 이점

  • UI 상태를 관리하는 코드가 훨씬 간단해지고 명확해집니다.
  • 상태 변화에 따라 직접 뷰를 조작할 필요가 없기 때문에 버그 발생 가능성이 줄어듭니다.
  • 화면이 복잡해져도 상태와 UI 사이의 일관성을 쉽게 유지할 수 있습니다.
  • 선언형 방식 덕분에 UI 테스트 작성이나 리팩토링도 쉬워집니다.

🎯 다음 글 예고: "이벤트 처리와 UI 상호작용"

이번 글에서는 Jetpack Compose에서 상태(state) 를 어떻게 정의하고, 상태가 바뀌었을 때 UI가 어떻게 자동으로 갱신(Recomposition) 되는지를 실습을 통해 살펴보았습니다. 이제 상태 기반 UI를 이해한 만큼, 다음 단계로는 사용자와의 상호작용을 어떻게 구현할 수 있는지를 알아볼 차례입니다.

다음 글에서는 Compose에서 버튼 클릭, 텍스트 입력 등 기본적인 사용자 입력 이벤트를 어떻게 처리하는지 살펴보고, onClick이나 onValueChange와 같은 이벤트 핸들러를 통해 UI 요소가 동작에 반응하도록 만드는 방법을 익혀보겠습니다. 또한, 드래그나 스와이프처럼 제스처를 활용한 상호작용을 추가하는 방법까지 함께 다뤄보며, 정적인 화면을 넘어 인터랙티브한 Compose UI 만들기에 한 걸음 더 다가가 보겠습니다. 🚀

0개의 댓글