컴포즈 상태와 재구성

손현수·2024년 3월 8일

안드로이드 Compose

목록 보기
2/25

companion object

  • 코틀린 클래스는 하나의 컴패니언 객체를 포함할 수 있다.
  • 컴패니언 객체의 프로퍼티에 클래스의 인스턴스를 만들지 않고도 접근할 수 있다.
class MyClass {
	companion object {
    	var counter = 1
        fun counterUp() {
        	counter += 1
        }
    }
}

fun main(args: Array<String>) {
	println(MyClass.counter) 	// 1
    MyClass.counterUp()
    println(MyClass.counter)	// 2
}

상태(state)

컴포즈와 같은 선언적 언어에서 일반적으로 상태란 '시간에 따라 변경될 수 있는 값'이라 불린다.

재구성(recomposition)

컴포즈를 이용해 개발할 때는 컴포저블 함수의 계층을 생성해 앱을 구축한다.

한 컴포저블 함수에서 다른 함수로 전달된 데이터는 대부분 부모 함수에서 상태로서 선언된다. 즉, 부모 컴포저블의 상탯값 변화가 모든 자식 컴포저블에 반영되며, 해당 상태가 전달된다는 것을 의미한다. 컴포즈에서 이를 재구성이라는 동작으로 실행한다.

재구성은 컴포저블 함수의 계층 안에서 상탯값이 변경될 때 일어난다. 컴포즈는 상태의 변화가 감지되면 영향을 받는 모든 함수를 재구성한다. 재구성이란 해당 함수들을 다시 호출하고 새로운 상탯값을 전달하는 것이다.

상탯값이 변경될 때마다 사용자 인터페이스의 전체 컴포저블 트리를 재구성하면 사용자 인터페이스의 렌더링과 업데이트에 있어 비효율적이다. 컴포즈는 해당 상태 변화에 직접 영향을 받는 함수들만 재구성하는 지능적 재구성 기법을 이용해 이 오버헤드를 피한다. 즉, 해당 상탯값을 읽는 함수들만 재구성하는 것이다.

컴포저블에서 상태 선언하기

  • MutableState 인스턴스는 mutableStateOf() 런타임 함수를 호출하고, 초기 상탯값을 전달해서 만든다. 예를 들어, 다음은 빈 String 값으로 초기화된 MutableState 인스턴스를 생성하는 코드이다.
var textState = { mutableStateOf("") }
  • 포함한 값이 변경되면 해당 값을 구독하는 모든 함수의 재구성을 트리거하는 옵저버블 상태를 제공한다.
  • 그러나 위의 선언에는 한 가지 핵심 요소가 누락되었다. 상태는 재구성 과정에서 기억되어야 한다. 위의 코드에서 해당 상태는 선언된 함수가 재구성될 때마다 빈 문자열로 다시 초기화된다. 현재 상탯값을 유지하게 하려면 remember 키워드를 이용한다.
var myState = remember { mutableStateOf("") }
  • 상태를 선언하는 좀 더 일반적이고 간결한 접근 방식은 by 키워드를 통해 코틀린 프로퍼티를 위임하는 것이다.
@Composable
fun MyTextField() {
    var textState = remember { mutableStateOf("") }

    val onTextChange = {text: String ->
        textState.value = text
    }

    TextField(
        value = textState.value,
        onValueChange = onTextChange
    )
}
  • 위의 코드를 by 키워드를 사용하여 아래와 같이 수정할 수 있다.
    • 이를 수행하기 위해서는 2개의 라이브러리를 임포트해야 한다
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue

@Composable
fun MyTextField() {
    var textState by remember { mutableStateOf("") }

    val onTextChange = {text: String ->
        textState = text
    }

    TextField(
        value = textState,
        onValueChange = onTextChange
    )
}
  • 대부분의 경우 by 키워드 및 프로퍼티 위임을 이용하여 간결하게 코드를 작성하여 이용한다

단방향 데이터 흐름

  • 단방향 데이터 흐름 접근 방식: 한 컴포저블에 저장된 상태는 자식 컴포저블 함수들에 의해 직접 변경되어서는 안된다는 개념
  • 예를 들어서 bool 상탯값을 가지는 funtionA, switch 컴포넌트를 가지며 functionA의 자식 컴포저블인 functionB가 있다고 하자. 단방향 데이터 흐름을 따른다면 B의 switch가 A의 bool 상태를 변경하는 것을 금지한다
  • 단방향 데이터 흐름에 적합한 접근 방식은 A가 B에게 자신의 상탯값과 이벤트 핸들러 함수를 파라미터로 전달하고 B에서 switch에 의해 A의 bool 상탯값이 변경되어야 한다면 이를 파라미터로 전달받은 이벤트 핸들러를 통해서 A에 전달한다
@Composable
fun FunctionA() {
	var switchState by remember {mutableStateOf(true)}
    
    val onSwitchChange = {value: Boolean -> 
    	switchState = value
    }
    
    FunctionB(switchState, onSwitchChange)
}

@Composable
fun FunctionB(switchState: Boolean, onSwitchChange: (Boolean) -> Unit) {
	Switch(
    	checked = switchState,
        onCheckedChange = onSwitchChange
    )
}

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
	StateExampleTheme {
    	Column{
        	DemoScreen()
            FunctionA()
        }
    }
}

상태 호이스팅

  • 앞서 다뤘던 FunctionA와 FunctionB 예시처럼 특정 상탯값을 이벤트 핸들러를 통해 자식 컴포저블에서 부모 컴포저블로 전달하는 것을 상태 호이스팅이라 한다.
  • 상태 호이스팅 방식의 장점은 자식 컴포저블을 비상태 컴포저블로 만들 수 있고 이를 통해 재사용성이 높아진다.
    • 이후의 앱 개발 프로세스에서 상태를 다른 하위 컴포저블에도 전달할 수 있게 된다
@Composable
fun DemoScreen() {
	var textState by remember { mutableStateOf("") }
    
    val onTextChange = { text: String ->
    	textState = text
    }
    
    MyTextField(text: String, onTextChange: (String -> Unit))
}

@Composable
fun MyTextField(text: String, onTextChange: (String) -> Unit) {
	TextField(
    	value = text,
        onValueChange = onTextChange
    )
}
  • 위의 코드가 대표적인 상태 호이스팅의 예시이다. TextField의 상탯값을 자기 자신이 직접 가지고 있는 것이 아니라 부모 컴포저블이 소유하고 있으며 value의 변화가 발생하면 콜백 함수인 onTextChange를 통해 부모 컴포저블이 가지고 있는 상탯값을 변경하게 된다. 이를 통해 자식 컴포저블은 비상태 컴포저블을 유지할 수 있게 된다.

환경 설정 변경을 통한 상태 저장하기

  • 일반적으로 상태는 환경 설정 변경(ex) 가로 모드 전환) 시 유지되지 않으며, 이를 유지하려면 rememberSaveable 키워드를 이용해야 한다.
profile
안녕하세요.

0개의 댓글