Android/Compose

[Android Compose] Stateful vs Stateless, 상태 호이스팅과 UDF

참깨빵위에참깨빵_ 2023. 2. 8. 22:20
728x90
반응형

Compose의 컴포저블 함수는 2종류로 나뉜다. 제목의 Stateful Composable과 Stateless Composable이 그것이다.

단순하게 Stateful과 Stateless를 보면 상태유지와 비상태유지라는 뜻이 각각 있어서, Stateful은 상태를 가졌으면 불리는 이름이고 반대로 상태를 갖고 있지 않으면 Stateless라고 불리는 게 아닐까 생각된다.

둘의 차이는 안드로이드 디벨로퍼보다 코드랩에 좀 더 잘 나와있다. 코드랩 전에 디벨로퍼부터 확인해본다. 위 내용은 생략하고 중간만 가져온 것이기 때문에 전체 문서를 확인해 보는 게 좋다.

 

https://developer.android.com/jetpack/compose/state?hl=ko#stateful-vs-stateless 

 

상태 및 Jetpack Compose  |  Android Developers

상태 및 Jetpack Compose 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 앱의 상태는 시간이 지남에 따라 변할 수 있는 값을 의미합니다. 이는 매우 광범위한 정

developer.android.com

remember를 써서 객체를 저장하는 컴포저블은 내부 상태를 생성해서 컴포저블을 Stateful로 만든다. HelloContent는 내부적으로 name 상태를 보존하고 수정하므로 Stateful Composable의 한 예가 된다. 이는 호출자가 상태를 제어할 필요가 없고 상태를 직접 관리하지 않아도 상태를 사용할 수 있는 경우 유용하다. 그러나 내부 상태를 갖는 컴포저블은 재사용 가능성이 적고 테스트가 더 어려운 경향이 있다
Stateless Composable은 상태를 갖지 않는 컴포저블이다. Stateless를 달성하는 한 가지 쉬운 방법은 상태 호이스팅을 사용하는 것이다. 재사용 가능한 컴포저블은 개발 시에는 동일한 컴포저블의 Stateful 버전과 Stateless 버전을 모두 노출해야 하는 경우가 있다. Stateful 버전은 상태를 염두에 두지 않는 호출자에게 편리하며 Stateless 버전은 상태를 제어하거나 끌어올려야 하는 호출자에게 필요하다

 

이 부분을 정리하면 아래와 같다.

 

  • 내부적으로 상태를 갖고 있는 컴포저블 = Stateful Composable
  • 내부적으로 상태를 갖고 있지 않는 컴포저블 = Stateless Composable

 

아래는 코드랩의 설명이다.

 

https://developer.android.com/codelabs/jetpack-compose-state?hl=ko&continue=https%3A%2F%2Fdeveloper.android.com%2Fcourses%2Fpathways%2Fcompose%3Fhl%3Dko%23codelab-https%3A%2F%2Fdeveloper.android.com%2Fcodelabs%2Fjetpack-compose-state#8 

 

Jetpack Compose의 상태  |  Android Developers

이 Codelab에서는 상태를 관리하여 다양한 기능의 대화형 Compose 애플리케이션을 빌드하는 방법을 알아봅니다.

developer.android.com

Composable 함수에서 모든 상태를 추출할 수 있는 경우 Composable 함수를 Stateless라고 한다. Stateless Composable은 상태를 소유하지 않는 컴포저블이다. 즉 새 상태를 보유, 정의, 수정하지 않는다. Stateful Composable은 시간이 지남에 따라 변할 수 있는 상태를 소유하는 컴포저블이다. 실제 앱에선 컴포저블의 기능에 따라 100% Stateless로 하는 건 어려울 수 있다. 컴포저블이 가능한 한 적게 상태를 소유하고 적절한 경우 컴포저블의 API에 상태를 노출해서 상태를 끌어올릴 수 있도록 컴포저블을 디자인해야 한다...(중략)

 

이 부분을 정리하면 아래와 같다.

 

  • 시간이 지나면서 변할 수 있는 상태를 갖는 컴포저블이다 = Stateful Composable
  • 새로운 상태를 보유, 정의, 수정하지 않는 컴포저블이다 = Stateless Composable

 

아래는 위 코드랩에서 Stateless, Stateful을 사용해 작성한 코드 일부다.

 

@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")
       }
   }
}
@Composable
fun StatefulCounter(modifier: Modifier = Modifier) {
   var count by rememberSaveable { mutableStateOf(0) }
   StatelessCounter(count, { count++ }, modifier)
}

 

count라는 변수를 StatelessCounter에서 StatefulCounter로 끌어올렸다. 이제 StatefulCounter는 필요한 컴포저블 함수 안에서 호출해 사용할 수 있다. 이전 내용들을 봐야 이 말이 이해될 것이다.

 

@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
   StatefulCounter(modifier)
}

 

그럼 이걸로 Stateful과 Stateless에 대한 건 끝일까? 아니다. 디벨로퍼 링크에서 조금 위로 스크롤하면 이런 핵심사항이 있다.

 

Compose는 State 객체를 읽어오는 과정에서 자동으로 재구성된다...(중략)

 

Compose는 특정 시점에 UI의 어디를 다시 그려야 하는지 선택한다. 이 과정에서 Compose는 새 데이터를 사용해 Composable 함수를 다시 호출하는 리컴포지션(재구성)을 거친다. 그래서 바뀐 부분만 다시 그리는 게 가능하다.

재구성이 일어나는 조건은 상태의 변경 유무다. 상태가 바뀌면 재구성이 발생한다. 그런데 Stateless Composable은 내부적으로 상태를 갖고 있지 않다. 이걸 바탕으로 Stateful, Stateless Composable을 최종 정리하면 아래와 같다.

 

Stateful Composable Stateless Composable
내부적으로 시간이 지나면서 변할 수 있는 상태를 갖고 있음 내부적으로 상태를 보유, 정의, 수정하지 않음
상태가 변하면 자신과 자식 Composable을 재구성 상태가 없어서 스스로 재구성할 수 없음

 

그런데 상태를 끌어올린다는 말이 위에서 나왔었다. 이 상태를 끌어올린다는 게 무슨 뜻인지 알 수가 없다.

 

https://developer.android.com/jetpack/compose/state?hl=ko#state-hoisting 

 

상태 및 Jetpack Compose  |  Android Developers

상태 및 Jetpack Compose 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 앱의 상태는 시간이 지남에 따라 변할 수 있는 값을 의미합니다. 이는 매우 광범위한 정

developer.android.com

Compose에서 상태 호이스팅은 컴포저블을 Stateless로 만들기 위해 상태를 컴포저블의 호출자로 옮기는 것이다. Compose에서 상태 호이스팅을 하는 일반적 패턴은 상태 변수를 아래 2개의 매개변수로 바꾸는 것이다

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

하지만 onValueChange로만 제한되지 않는다. 컴포저블에 더 구체적인 이벤트가 있으면 ExpandingCard가 onExpand와 onCollapse를 정의할 때와 같이 람다를 써서 그 이벤트를 정의해야 한다. 이런 방식으로 끌어올린 상태에는 중요 속성이 몇 가지 있다

1. 단일 정보 소스 : 상태를 복제하는 대신 옮겼기 때문에 정보 소스가 하나만 있다. 버그 방지에 도움이 된다
2. 캡슐화됨 : Stateful Composable만 상태를 수정할 수 있다. 철저히 내부적 속성이다
3. 공유 가능함 : 호이스팅한 상태를 여러 컴포저블과 공유할 수 있다. 다른 컴포저블에서 name을 읽으려는 경우 호이스팅을 통해 그렇게 할 수 있다
4. 가로채기 가능함 : Stateless Composable의 호출자는 상태 변경 전에 이벤트를 무시할지 수정할지 결정할 수 있다
5. 분리됨 : Stateless ExpandingCard의 상태는 어디에나 저장할 수 있다. 예를 들어 이제는 name을 ViewModel로 옮길 수 있다

 

ExpandingCard는 어떤 Composable이라고 이해하면 된다. 위 설명대로라면 상태 호이스팅은 컴포저블 안에 있는 상태의 위치를 Composable을 호출하는 Composable 함수 안으로 옮기면 된다. 위에 있던 코드를 다시 확인해본다.

 

@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")
       }
   }
}
@Composable
fun StatefulCounter(modifier: Modifier = Modifier) {
   var count by rememberSaveable { mutableStateOf(0) }
   StatelessCounter(count, { count++ }, modifier)
}

 

해당 코드랩에서 var count 프로퍼티는 원래 WaterCount 컴포저블 안에 있었다. 이후 내부 상태를 갖는 컴포저블은 재사용 가능성이 적고 테스트가 어렵기 때문에 Stateless Composable을 만드는 과정의 일환으로 count 프로퍼티를 StatefulCounter 안으로 가져왔다. 이 과정 이후로 StatelessCounter 컴포저블은 StatefulCounter(=호출자) 안에서 호출되고, count 프로퍼티는 초기값 0에서 시작해 StatefulCounter가 호출될 때마다 1씩 추가된다. 동시에 StatelessCounter 안의 if문이 실행된다. 이것이 count를 StatelessCounter에서 StatefulCounter로 끌어올린 것이다.

이렇게 상태가 내려가고 이벤트가 올라가는 패턴을 단방향 데이터 흐름(UDF)라고 한다.

 

https://developer.android.com/jetpack/compose/architecture?hl=ko#udf 

 

Compose UI 설계  |  Jetpack Compose  |  Android Developers

Compose UI 설계 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Compose의 UI는 변경할 수 없습니다. UI를 설계한 후 업데이트할 수 없습니다. UI 상태는 제어할 수

developer.android.com

단방향 데이터 흐름(UDF)은 상태는 아래로 이동하고 이벤트는 위로 이동하는 디자인 패턴이다. 단방향 데이터 흐름을 따라 UI에 상태를 표시하는 컴포저블과 상태를 저장, 변경하는 앱 부분을 서로 분리할 수 있다. 단방향 데이터 흐름을 사용하는 앱의 UI 업데이트 루프는 아래와 같다

- 이벤트 : UI 일부가 이벤트를 만들어 위쪽으로 전달하거나(처리하기 위해 뷰모델에 전달되는 버튼 클릭 등) 앱의 다른 레이어에서 이벤트가 전달된다(사용자 세션이 종료됐음을 표시)
- 상태 업데이트 : 이벤트 핸들러가 상태를 바꿀 수도 있다
- 상태 표시 : 상태 홀더가 상태를 아래로 전달하고 UI가 상태를 표시한다

Compose 사용 시 이 패턴을 따르면 몇 가지 이점이 있다

- 테스트 가능성 : 상태와 상태를 표시하는 UI를 분리해 격리 상태에서 더 쉽게 테스트 가능
- 상태 캡슐화 : 상태는 한 곳에서만 업데이트할 수 있고 컴포저블의 상태에 관한 정보 소스가 하나뿐이므로, 알려지지 않은 상태로 인해 버그를 만들 가능성이 작다
- UI 일관성 : 관찰 가능한 상태 홀더(StateFlow 또는 LiveData)를 사용함으로써 모든 상태 업데이트가 UI에 즉시 반영된다

 

반응형