이전 글 에서 Compose 컴파일러 플러그인은 컴포저블 함수 내부에서 어떤 데이터가 읽혔는지를 추적하고, 데이터의 변화가 감지될 때만 해당 함수를 재구성하도록 한다고 포스팅 했습니다.
그에 따라 최적화를 위해 Stable과 Immutable 개념에 대해 배웠고 이를 예제로 보여주기 위해 NowinAndroid 예제를 통해 실험을 해봤습니다.
하지만 배웠던 내용과 다르게 Stable하게 만들어줬음에도 리컴포지션이 일어났습니다.
일단 전반적인 구성 부터 알려드리겠습니다.
nowinandroid에서는 애플리케이션의 전반적인 상태들을 정의 해놓은 NiaAppState 객체가 있습니다. 구체적으로 네비게이션, 코루틴 작업 관리, UI 크기 조정, 네트워크 상태 모니터링, 사용자 뉴스 리소스 관리, 시간대 모니터링 등이 포함됩니다.
여기서 showSettingsDialog 상태가 변했다고 가정해봅시다.
showSettingsDialog 상태 변경에 맞춰 SettingsDialog가 표시되거나 사라지거나 하겠죠
만약 여기서 AppState에 @Stable을 명시하지 않았다면 어떻게 될까요?
SettingsDialog만 변화하였고 AppState에는 아무런 값 변화가 없습니다.
그렇다면 같이 NiaGradientBackground 컴포저블에 있는 NiaNavHost는 아래처럼 appState에만 영향을 받는데 재사용되는게 맞는 동작이겠죠
appState에 @Stable을 명시하고 리컴포지션 횟수를 추적해봤습니다.
근데.. 왜 전부 리컴포지션이 일어나는 것이죠?
음... 예상과 달라서 당황했지만 디버깅을 해봤습니다. 이번에 알았던 사실인데 디버깅 할때 Recomposition State를 친절히 알려줍니다.
onTopicClick이 변경되었다고 나옵니다.
디버깅으로 실제로 주소값을 비교해보니 새로 생성되는게 맞습니다.
onTopicClick은 네비게이션 람다함수입니다.
navController 객체가 계속 새로 생성되는 것도 아니고 왜 자꾸 onTopicClick 람다 함수가 왜 자꾸 새로 생성될까요?
해당 부분을 좀 더 자세히 찾아봤습니다.
저와 같이 이런 상황을 겪은 분이 있더라구요
그리고 알게된 것은 현재 람다 함수는 Stable과 상관 없이 무조건 재생성되고 Compose Compiler는 이를 skip하지 못하는 상태라는 것을 발견했습니다
또한 Compiler 1.5.4+ 버전부터 실험적 기능으로 이를 해결하기 위해 strong skipping mode option이 추가 되었습니다.
현재 1.7.0-alpha의 Compose 라이브러리에 있는 코드에서 이 기능을 활성화했으며 Compose 1.7의 안정적인 릴리스를 목표로 코드에서 기본적으로 언제 이 기능을 활성화할지 평가하고 있다고 하네요
즉, 1.7.0 버전이 정식 출시되기전에 현재에서는 이를 실험적 기능으로 선택적으로 키고 끌 수 있다고 합니다.
방법으로는 루트 수준 Gradle 파일에 아래처럼 명시하면 됩니다.
task.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>() {
compilerOptions.freeCompilerArgs.addAll(
"-P" ,
"plugin:androidx.compose.compiler.plugins.kotlin:experimentalStrongSkipping=true" ,
)
}
공식적인 자료가 나와있지 않아 저도 자세히 파악하진 못했습니다.
어느정도 있는 자료를 긁어 유추를 포함해서 작성하겠습니다.
이전에는 기본적으로, Compose 컴파일러는 안정적인(stable) 값만을 매개변수로 받는 컴포저블 함수를 리컴포지션에서 건너뛸 수 있도록 표시합니다. "Strong skipping" 모드는 이러한 조건을 완화하여 불안정한 매개변수를 가진 컴포저블도 건너뛸 수 있도록 합니다.
특히 이 모드를 활성화하면, 모든 람다 표현식이 자동으로 remember 함수에 의해 기억됩니다. 따라서, 람다를 사용하는 컴포저블이 리컴포지션을 건너뛸 수 있도록 람다를 직접 remember로 감쌀 필요가 없어집니다.
예를 들어 아래와 같이 불안정한 객체와 안정적인 객체를 포착하는 람다가 있을 때:
@Composable
fun MyComposable(unstableObject: Unstable, stableObject: Stable) {
val lambda = {
use(unstableObject)
use(stableObject)
}
}
"Strong skipping" 모드가 활성화되면, Compose 컴파일러는 자동으로 다음과 같이 변환합니다
@Composable
fun MyComposable(unstableObject: Unstable, stableObject: Stable) {
val lambda = remember(unstableObject, stableObject) {
{
use(unstableObject)
use(stableObject)
}
}
}
이제 다시 돌아와 NowInAndroid 예제로 돌아와 remember로 람다를 메모제이션 해보겠습니다.
before
after
skip이 되는 것을 볼 수 있네요.
정리하자면
1. 1.7.0에서는 Strong Skip mode가 자동으로 적용된다.
2. 현재 1.5.4에는 실험적으로 사용가능하다.
3. 또한 람다의 경우 현재 실험적인 기능을 사용하지 않고 remember을 통해 수동으로 가능하다.
저는 nowinandroid에 Issue 등록하기로 결심했고 자총지총 설명해서 Issue를 올렸다.
아직 실험적인 기능을 도입하기엔 검증이 필요할 것 같아 해당 부분만 remember을 통해 해결하는 코드로 ISSUE 등록하고 PullRequest 하는데 CLA 서명(컴퓨터 바꾼거 때문에 author email이 달라져 있었음 ㅠㅠ;)때문에 화가 나고 있던 와중에 답변이 빠르게 왔다.
해석하자면 곧 Strong Skipping Mode를 도입할 것이고 만약 Strong Skipping Mode가 도입되면 remember가 자동으로 적용되는데 현재 이를 또 remember로 하게 되면 나중에 중복적으로 선언된다는 것이다.
즉, 실험적으로 현재 버전에서 쓸건지, 1.7.0이 곧 도입되는지는 모르겠지만
곧 Strong Skipping Mode를 도입할 것이라 현재 remember을 쓰는건 후에 중복으로 남을 수 있어 당장은 이로 인한 자원 낭비는 감수하겠다는 것이다.
그랬구만.. 그랬어... 맞왜틀 해결