본문 바로가기
프로그래밍/Jetpack Compose

[Jetpack Compose] rememberUpdatedState 가 무엇인지 알아보자

by dev_gyu 2024. 9. 24.
728x90

rememberUpdatedState 가 무엇일까?

remember 는 value 를 캐싱한다.

하지만 value 가 바뀔때마다 새롭게 재계산을 하기 위해서는 key 를 설정해주어야한다.

즉 아래와 같이 설정을 해줘야 참조한 상태에 따라 새로운 값으로 바뀐다는 의미이다.

 

val test = remember(value) { mutableStateOf(value) }

 

근데 이렇게 구현하기에는 코드가 너무 길다.

그래서 우리는 rememberUpdatedState 를 사용하여 이 코드를 많이 축소화할 수 있다.

val rememberUpdateNumber by rememberUpdatedState(state)

// 위처럼 축소가 가능한 이유는 내부적으로 아래와 같은 코드를 띄고 있기 때문이다.
@Composable
fun <T> rememberUpdatedState(newValue: T): State<T> = remember {
    mutableStateOf(newValue)
}.apply { value = newValue }

 

rememberUpdatedState 사용 시 주의점

우리가 Compose 를 사용할때는 필연적으로 Recomposition 이 일어나게 된다.

이 리컴포지션이 일어나는 경우 Composable 함수는 다시 한 번 코드를 재실행하게 되는데 우리가 remember 를 사용하여 값을 캐싱하는 가장 큰 이유가 바로 이 문제 때문이다 (코드가 재실행되면서 초기값으로 돌아가기 때문)

 

자, 그럼 다시 한 번 rememberUpdatedState 의 코드를 살펴보자. 무언가 이상한점이 있지 않은가?

내부적으로 remember(value) 가 아닌, apply 를 통해서 value 를 재설정하고 있다.

이것이 의미하는 것는 rememberUpdateNumber 가 선언되어있는 Composable 에서 리컴포지션이 실행될 때마다 rememberUpdateNumber 는 value 를 newValue 로 초기화한다는 것이다.

 

만약 newValue 의 값이 바뀌지 않는다면 문제가 없겠으나 newValue 가 System.currentTimeTomillis 와 같이 매번 바뀌는 값을 참조하고 있다면 한 가지 문제가 발생하는데, 아래 예시를 봐보자.

  1. RememberNumber Text 를 변경하고 싶어 버튼을 클릭한다
  2. 버튼이 클릭되면서 newNumber 가 증가하고 상태가 바뀌었으니 stateful 인 TestComposable, stateless RememberNumber 가 리컴포지션된다.
  3. 이 경우 RememberUpdateStatedNumber 는 어떻게 될까? 참조하는 상태를 직접 변경하지 않았으니 리컴포지션이 Skip 되지 않을까?
@Composable
fun TestComposable(){
    var newNumber by remember { mutableIntStateOf (1000) }

    val rememberNumber by remember(newNumber) { mutableStateOf (newNumber.toLong() + System.currentTimeMillis()) }
    val rememberUpdateTime by rememberUpdatedState(System.currentTimeMillis())

    Button(onClick = { newNumber++ }){}

    RememberNumber(number = rememberNumber)
    RememberUpdateStatedNumber(number = rememberUpdateTime)
}


@Composable
fun RememberNumber(number: Long){
    Text(text = number.toString())
}


@Composable
fun RememberUpdateStatedNumber(number: Long){
    Text(text = number.toString())
}

 

 답은 [리컴포지션이 작동한다.] 이다

rememberUpdateState 로 선언되었기에, TestComposable 이 리컴포지션되면서 apply 를 통해 새로운 값이 들어가게 되고 이로 인해 rememberUpdateTime 에 현재 시간이 새롭게 업데이트 되기 때문이다.

 

아래는 위의 코드를 직접 실행하여 테스트 한 결과물이다.

이 문제로 인해 리컴포지션이 무한히 발생하고 있으며, 이것을 처리하지 못한다면 UI 렌더링 문제로 인한 렉, OOM 등의 버그가 발생할 수 있으므로 주의하자.

엄청난 수의 리컴포지션 ..

 

 

이러한 문제가 있다면 RememberUpdatedState 는 왜 사용하는 것일까?

아래는 rememberUpdatedState 에 대한 JavaDoc 설명이다

remember a mutableStateOf and update its value to newValue on each recomposition of the rememberUpdatedState call.

rememberUpdatedState should be used when parameters or values computed during composition are referenced by a long-lived lambda or object expression. Recomposition will update the resulting State without recreating the long-lived lambda or object, allowing that object to persist without cancelling and resubscribing, or relaunching a long-lived operation that may be expensive or prohibitive to recreate and restart. This may be common when working with DisposableEffect or LaunchedEffect

 

쉽게 설명하자면 구성 중에 계산된 파라미터나 값이 장기간 실행되는 람다 또는 객체 표현식에 의해 참조될 때 사용하라는 뜻이고 일반적으로 DisposableEffect  LaunchedEffect 에서 사용된다고 한다.

 

테스트를 위해 다음과 같이 구현해보자

  1. RememberUpdatedStateTest Composable 에 LaunchedEffect 를 선언
  2. 내부에서는 number 를 capture 하고 Log 를 찍도록 반복 설정해준다.
  3. 버튼을 눌러 newNumber 를 증가시키고 로그를 확인해본다.
@Composable
fun TestComposable(){
    var newNumber by remember { mutableIntStateOf (1000) }
    Button(onClick = { newNumber++ }){}

    RememberUpdatedStateTest(number = newNumber)
}

@Composable
fun RememberUpdatedStateTest(number: Int){
    LaunchedEffect(Unit) {
        while (true){
            delay(300L)
            Log.e("숫자는 - ", number.toString())
        }
    }
}

 

위와 같이 코드를 구현했다면 버튼을 아무리 눌러도 number 는 초기 컴포지션 단계에서 설정되어있던 1000 을 내뱉을 것이다.

이유는 Lambda 의 경우 Composable 이 아니기에 Lambda 가 사용되는 Composable 함수가 Restart 되는 것이 아니라면 재실행되지 않는다.

그렇기에 Lambda 내부에서 캡쳐한 값은 초기에 설정한 값 1000 이며, number 가 바뀌더라도 값이 업데이트 되지 않는 것이다.

 

만약 업데이트 된 값을 Lambda 에서 처리하고 싶다면 rememberUpdatedState 를 사용하면 된다.

 

@Composable
fun TestComposable(){
    var newNumber by remember { mutableIntStateOf (1000) }
    Button(onClick = { newNumber++ }){}

    RememberUpdatedStateTest(number = newNumber)
}

@Composable
fun RememberUpdatedStateTest(number: Int){
    val rememberUpdatedStateValue by rememberUpdatedState(number)

    LaunchedEffect(Unit) {
        while (true){
            delay(300L)
            Log.e("숫자는 - ", rememberUpdatedStateValue.toString())
        }
    }
}

 

위와 같이 rememberUpdateState 를 사용한다면 Lambda 가 캡쳐한 값의 value 가 업데이트 되므로 Lambda 에서도 상태 업데이트를 확인할 수 있다.

 

rememberUpdatedState 말고 remember(key) 를 사용하면 안되는 건가요?

사실 나도 처음에는 remember(key){ mutableStateOf(value) } 로 선언하면 되는게 아닌가 싶었지만 직접 실험해보니 이것은 Lambda 에 제대로 값 전달이 안되었다.

 

이유는 다음과 같다.

  1. Lambda 의 변수 캡쳐는 final 로 전달되기에 상태 객체의 값이 변하여 새로운 객체로 갱신된다고 하더라도 Lambda 는 기존 객체 (주소) 를 바라보고 있다
  2. rememberUpdatedState 의 경우 apply { value = newValue } 를 사용하므로 새로운 객체로 변경하는 것이 아닌, 값만을 갱신하기에 객체 자체는 변함이 없다
  3. remember(key){ mutableStateOf(value) } 의 경우 내부 값이 달라진다면 새로운 State 객체를 생성하기 때문에 Lambda 에서 캡쳐한 객체와 아예 다른 객체가 되어버리기 때문에 Lambda 내부에서는 업데이트를 알 수 없게 된다.

 

이러한 기능을 가지고 있으니 RememberUpdatedState 는 람다를 사용할때 이용하면 좋다.

728x90