본문 바로가기
Jetpack Compose

Jetpack Compose 상태 관리에 대한 정리

by junjunjun 2023. 7. 8.
반응형

본 글은 정확하지 않을 수 있습니다. 참고용으로만 봐주시면 감사하겠습니다.

 

Jetpack Compose의 Compose 이해하기를 보고 오시는 것을 추천드립니다.

공식문서 Jetpack Compose의 상태와 상태관리를 참고하였습니다.

 

 

 

var count by remember { mutableStateOf(0) }

jetpack compose를 처음 접하면서 변수 선언을 왜 위 코드처럼 하는지 궁금증을 가지게 되면서 자연스럽게 상태라는 것을 공부하게 되었다.

 

 

1. 상태란?

상태는 시간이 지남에 따라 변할 수 있는 값을 의미한다.

예를 들어 버튼을 클릭하면 숫자가 오르는 앱이 있다고 하였을 때 이 숫자를 담는 변수를 상태라고 부를 수 있다.

그 밖에도 아래와 같은 예시들도 상태가 된다.

  • 채팅 앱에서 가장 최근에 수신된 메세지
  • 사용자의 프로필 사진
  • 항목 목록의 스크롤 위치

 

상태 추적

@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
    Column(modifier = modifier.padding(16.dp)) {
        var count = 0  // 상태!!
        Text("You've had $count glasses.")

        // 버튼 이벤트
        Button(onClick = { count++ }, Modifier.padding(top = 8.dp)) {
            Text("Add one")
        }
    }
}

버튼을 클릭하면 숫자가 증가하는 코드이다. 하지만 실제로 테스트를 해보면 버튼을 아무리 클릭해도 숫자는 증가되지 않는다.

 

그 이유를 알기 전에 UI가 업데이트되는 과정을 알아보자.

  1.  사용자 또는 프로그램에 의해 버튼 누르기와 같은 이벤트가 발생된다.
  2.  이벤트 핸들러가 UI에서 사용하는 상태를 업데이트시킨다.
  3.  새로운 상태를 표시하도록 UI가 업데이트된다. (재구성=리컴포지션)

 

즉 위 코드의 문제점은 상태가 변경될 때 3번인 새로운 상태를 UI에 업데이트되도록 알리지 않았기 때문이다. 변수인 count를 Compose가 추적할 수 있도록 지정을 해줘야 상태가 변경될 때 재구성을 예약할 수 있다.

(재구성에 대한 설명은 여기서 확인하실 수 있습니다.)

 

Compose가 추적할 수 있도록 지정해 주기 위해 State 및 MutableState 유형을 사용한다.

@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
   Column(modifier = modifier.padding(16.dp)) {
      // compose에 의해 추적당하도록 변경
       val count: MutableState<Int> = mutableStateOf(0) 

       Text("You've had ${count.value} glasses.")
        Button(onClick = { count.value++ }, Modifier.padding(top = 8.dp)) {
           Text("Add one")
       }
   }
}

하지만 아직은 컴파일 경고가 발생한다.

 

상태 관리

count 변수에 추적을 걸어주었기 때문에 이제는 count값이 변경돼도 재구성은 잘 작동한다.

하지만 재구성이 되면서 결국 변수도 다시 0으로 초기화되는 문제가 발생한다.

 

이를 위해 remember를 사용할 수 있다. remember를 사용하여 메모리에 객체를 저장할 수 있다.

remember로 저장된 값은 재구성이 되어도 값이 유지된다.

 

구성 변경(화면 회전이나 크기 조절 같은) 전반에서는 상태가 유지되지 않는다. 이 경우에는 rememberSaveable을 사용해야 한다.

 

상태 선언 방법 3가지

val mutableState = remember { mutableStateOf(default) }
// 읽기 : mutableState.value
// 수정 : mutableState.value = " "

var value by remember { mutableStateOf(default) }
// 읽기 : value
// 수정 : value = " "
- 추가적으로 import 필요

val (value, setValue) = remember { mutableStateOf(default) }
// 읽기 : value
// 수정 : setValue

 

 

2. 상태 호이스팅

 

상태 호이스팅을 이해하기 전에 Stateful과 Stateless 개념을 알아야 한다.

 

Stateful

remember을 사용하여 객체를 저장하는 컴포저블(=구성 가능한 함수)에는 내부 상태가 포함되어 있어 컴포저블을 Stateful로 만든다.

WaterCounter 내부적으로 count 상태를 보존하고 수정하므로 Stateful 컴포저블의 한 예가 된다.

쉽게 말하면 상태를 가지는 컴포저블을 말한다.

 

Stateless 

Stateless 컴포저블은 상태를 갖지 않는 컴포저블이다.

상태 호이스팅을 사용하면 Stateless 컴포저블을 쉽게 만들 수 있다.

 

 

상태 호이스팅이란,

Compose에서 컴포저블을 Stateless로 만들기 위해 상태를 컴포저블의 호출자로 옮기는 패턴이다.

다시 말해, 컴포저블 내부에 상태를 두는 것이 아닌 매개변수로부터 상태를 받는 형식을 말한다.

 

상태 호리스팅을 통해 상태가 내려가고 이벤트가 올라가는 이 패턴을 단방향 데이터 흐름(UDF)이라고 한다.

단방향 데이터 흐름을 따르면 UI에 상태를 표시하는 컴포저블상태를 저장하고 변경하는 앱 부분을 서로 분리할 수 있습니다.

 

일반적 패턴은 상태 변수를 다음 두 개의 매개변수로 바꾸는 것이다.

  • value: T - 표시할 현재 값입니다.
  • onValueChange: (T) -> Unit - T가 제안된 새 값인 경우 값을 변경하도록 요청하는 이벤트

 

// stateful 컴포저블
@Composable
fun StatefulCounter(modifier: Modifier = Modifier) {
    var count by rememberSaveable { mutableStateOf(0) }
    StatelessCounter(count, { count++ }, modifier)
}

// 상태 호이스팅한 코드 = stateless 컴포저블
@Composable
fun StatelessCounter(count: Int, onIncrement: () -> Unit, modifier: Modifier = Modifier) {
   Column(modifier = modifier.padding(16.dp)) {
       if (count > 0) {
           Text("You've had $count glasses.")
       }
       Button(onClick = onIncrement, Modifier.padding(top = 8.dp), enabled = count < 10) {
           Text("Add one")
       }
   }
}

 

 

상태 호이스팅으로 끌어올린 상태에는 5가지 속성(이점)이 있다.

  • 단일 정보 소스 : 상태를 복제하는 대신 옮겼기 때문에 정보 소스가 하나만 있다. 따라서 버그 방지에 도움이 된다.
  • 캡슐화됨 : Stateful 컴포저블만 상태를 수정할 수 있기에 stateless 컴포저블은 철저히 내부적 속성이다.
  • 공유 가능함 : 끌어올린 상태를 여러 컴포저블과 공유할 수 있다.
  • 가로채기 가능함 : Stateless 컴포저블의 호출자는 상태를 변경하기 전에 이벤트를 무시할지 수정할지 결정할 수 있습니다.
  • 분리됨 : Stateless 컴포저블의 상태는 어디에든(상위 컴포저블, ViewModel) 저장할 수 있다.

 

 

상태를 끌어올릴 때 상태의 이동 위치를 쉽게 파악할 수 있는 세 가지 규칙이 있습니다.

1. 상태는 적어도 그 상태를 사용하는 모든 컴포저블의 가장 낮은 공통 상위 요소로 끌어올려야 한다.(읽기).

@Composable
fun HelloMain() {
    // var name by rememberSaveable { mutableStateOf("") }  여기는 가장 낮지 않은 공통 상위 위치임
    HelloScreen()
}
@Composable
fun HelloScreen() {
    var name by rememberSaveable { mutableStateOf("") } // 가장 낮은 공통 상위 위치임

    HelloContent(name = name, onNameChange = { name = it })
    HelloContent(name = name, onNameChange = { name = it })
}

 

2. 상태는 최소한 변경될 수 있는 가장 높은 수준으로 끌어올려야 한다.(쓰기).

@Composable
fun ParentComponent() {
    val name = remember { mutableStateOf("") } // name 상태를 변경할 수 있는 최상위 위치

    ChildComponent1(name = name.value)

    Button(onClick = {
        name.value = "John Doe"
    }) {
        Text("Change Name")
    }
    ChildComponent2(name = name.value)
}

@Composable
fun ChildComponent1(name: String) {
    Text(text = "Hello from Child 1, $name")
}

@Composable
fun ChildComponent2(name: String) {
    Text(text = "Hello from Child 2, $name")
}
// name은 ParentComponent랑 ChildComponent1,2 둘다 상태 변경에 영향을 끼친다. 
// 그 중 최상위에 상태를 올려야 한다.

 

3. 두 상태가 동일한 이벤트에 대한 응답으로 변경되는 경우 두 상태는 동일한 수준으로 끌어올려야 한다.

버튼을 클릭 시 두 개의 상태가 변경될 경우, 각각의 상태는 동일한 수준으로 호이스팅 돼야 한다.

 

이러한 규칙을 따르면 단방향 데이터 흐름을 따르기 쉬워진다.

 

 

3. 상태 호이스팅할 대상 위치

Compose 애플리케이션에서 UI 상태를 어디로 호이스팅해야 하는지는 UI 상태가 UI 로직 비즈니스 로직 중 어느 쪽에서 필요한지에 따라 달라진다.

 

UI 로직

화면에 상태 변경을 표시하는 방법과 관련이 있다.

사용자가 카테고리를 선택했을 때 올바른 검색창 힌트를 가져오는 것, 목록의 특정 항목으로 스크롤하는 것, 또는 사용자가 버튼을 클릭할 때 특정 화면으로의 탐색 로직을 예로 들 수 있다.

 

비즈니스 로직

앱 데이터에 대한 제품 요구사항의 구현이다.

예를 들어 사용자가 버튼을 탭할 때 뉴스 리더 앱에서 기사를 북마크에 추가할 때 북마크를 파일이나 데이터베이스에 저장하는 로직이다.

 

 

UI 상태는 UI 상태를 읽고 쓰는 모든 컴포저블의 가장 낮은 공통 상위 요소로 호이스팅해야 한다.

가장 낮은 공통 상위 요소가 컴포지션 외부에 있을 수도 있다. 비즈니스 로직이 관련되어 있기 때문에 ViewModel에서 상태를 호이스팅 하는 경우를 예로 들 수 있다.

 

 

ui 로직에서 상태를 쓰는 경우

 

1. 상태 소유자로서의 컴포저블

상태와 로직이 간단하다면 컴포저블에 UI 로직과 UI 요소 상태를 사용하는 것이 좋다.

 

2. 상태 호이스팅 불필요

상태를 제어해야 하는 다른 컴포저블이 없는 경우 상태를 컴포저블 내부에 유지할 수 있다.

보통 애니메이션 상태에서 이 방식이 사용됩니다.

 

3. 컴포저블 내부에서 호이스팅

UI 요소 상태를 다른 컴포저블과 공유하고 여러 위치에서 상태에 UI 로직을 적용해야 하는 경우 상태를 UI 계층 구조의 상단으로 호이스팅 할 수 있다.

예를 들어 상태를 쓰는 버튼이 다른 컴포지션에도 쓰일 경우 각각의 컴포저블의 가장 낮은 공통 상위 요소에 호이스팅 하는 게 좋다.

 

4. 상태 소유자로서의 일반 상태 홀더 클래스

컴포저블에 UI 요소의 하나 또는 여러 개의 상태 필드가 사용되는 복잡한 UI 로직이 포함되어 있다면 일반 상태 홀더 클래스와 같은 

상태 홀더로 그 책임을 위임해야 합니다.

 

컴포저블의 로직을 격리된 상태에서 관리하여 복잡성이 줄어든다.

컴포저블이 UI 요소를 방출하고 상태 홀더가 UI 로직과 UI 요소의 상태를 포함한다.

 

 

비즈니스 로직에서 상태를 쓰는 경우

 

상태 소유자로서의 ViewModel(화면 상태 홀더 클래스)

비즈니스 로직에 대한 액세스 권한을 제공하고 화면에 표시하기 위한 애플리케이션 데이터를 준비하는 데는 ViewModel이 적합하다.

 

ViewModel에서 UI 상태를 호이스팅 하면 상태가 컴포지션 외부로 이동된다.

 

 

결론

  • 상태는 시간이 지남에 따라 변할 수 있는 값을 의미한다. 쉽게 생각하면 변수이다.
  • 상태에 추적을 걸기 위해 mutableStateOf를 사용한다.
  • 재구성이 일어나도 기존의 상태를 기억하기 위해 remember을 사용한다.
  • Stateful 컴포저블은 상태를 가진 컴포저블을 말한다.
  • Stateless 컴포저블은 상태를 가지지 않는 컴포저블을 말한다.
  • 상태 호이스팅은 컴포저블을 Stateless로 만들기 위해 상태를 컴포저블의 호출자로 옮기는 패턴이다.
  • 상태 호이스팅시 상태는 적어도 그 상태를 사용하는 모든 컴포저블의 가장 낮은 공통 상위 요소로 끌어올려야 한다.
  • UI 상태를 어디로 호이스팅해야 하는지는 UI 상태가 UI 로직 비즈니스 로직 중 어느 쪽에서 필요한지에 따라 달라진다.
반응형

댓글