Jetpack Compose 에서는 State 가 변경되는 경우 이를 SnapshotSystem 이 알아차리고 Invalidate 된 Composition 과 그 정보를 Composer 에게 전달, Recomposer 에 의해 recompose 된다.
Jetpack Compose 에서 가장 중요하다고 볼 수 있는, State 들은 내부적으로 다음과 같은 코드를 가진다.
@Stable
interface State<out T> {
val value: T
}
@Stable
interface MutableState<T> : State<T> {
override var value: T
operator fun component1(): T
operator fun component2(): (T) -> Unit
}
이곳에 @Stable Annotation 은 대체 무엇일까?
이것을 알기 전에 우리는 Compose 의 Smart Recomposition 에 대해 알아보아야 한다.
Compose Compiler 은 함수 재호출 과정에서 이전 값과의 비교를 통해 Recomposition 할지, Skipping 할 지 코드를 작성한다.
이러한 기능을 Smart Recomposition 이라고 부르는데, Jetpack Compose 의 가장 큰 기능 중 하나이다.
Smart Recomposition 의 실행 여부는 다음과 같다
대상이 안정적이고 값이 변경되지 않은 경우 (equals = true) Skipping대상이 안정적이고 값이 변경 된 경우 (equals = false) Recomposition대상이 불안정한경우 (구조적 동일성. '===' 체크하여 false 면) Recomposition
여기서 안정적이라는 것은 @Stable, @Immutable 이 붙은 class 이거나 내부적으로 안정적인 프로퍼티를 가진 class, String 을 포함한 primitive type, unstable 을 캡처하지 않은 람다 표현식 이 존재한다.
List<T> 나 Set<T> 같은 inteface 나 Any 와 같은 추상 타입들은 컴파일 시 구현을 예측할 수 없다.
또한 가변적인 public property 을 포함하는 클래스도 불안정하다.
그렇기에 Compose Compiler 는 이들을 Unstable 하다고 간주하고, 이러한 매개변수가 존재하는 Composable 함수는 일단 Recomposition 하도록 코드를 구현하여 Compose Runtime 이 Recomposition 하도록 한다.
테스트 코드를 작성하기에 앞서 Strong Skipping Mode 가 활성화되어 있다면, 테스트가 불가능하므로 이를 종료시켜주자.
예전에는 실험적 기능이었기에 직접 켜줘야했으나, 요즘은 구글 권장 기능이기 때문에 이것이 자동적으로 켜져있다.
Jetpack Compose 를 사용하는 Module 의 build.gradle.kts 로 이동해주자.

그리고 위처럼 StrongSkippingMode 를 비활성화해준다.
테스트를 진행하기 위해 다음 코드를 작성해주자.
ListComposable 은 변화없는 List<Int> 파라미터를 받고, count 변화를 통해 restartableGroup 인 Scaffold Lambda 를 재호출 시킬 것이다.
class MainActivity() : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
Scaffold {
val list = remember { mutableListOf(1, 2, 3) }
var count by remember { mutableStateOf(0) }
Column(modifier = Modifier.padding(it)) {
ListComposable(list)
Button({ count++ }) { }
Text(count.toString())
}
}
}
}
}
@Composable
fun ListComposable(list: List<Int>){
Text(list[0].toString())
Text(list[1].toString())
Text(list[2].toString())
}
이렇게 실행하는 경우 리컴포지션은 어느곳에서 발생할까?
- Button + Text
- Button + Text + ListComposable

정답은 2번이다.
ListComposable 로 전달된 List<Int> 는 Unstable 하다.
Compose Compiler 는 $changed 비트마스크 매개변수를 활용하여 매개변수가 안정적인지, 불안정한지를 전달하고 이를 통해 Recomposition 하는데, Unstable 인 경우 항상 변경됨으로 세팅하여 dirty 상태로 만들기 때문이다.
전달되는 Bitmask 값은 Runtime 때만 알 수 있으므로 확인이 어렵다.
Unstable 한 data type 이 안정적이라고 Compose Compiler 에게 알려주는 것이 Stable Marker 인데, 이는 Annotation 을 활용하여 적용한다.
@Stable
interface CustomResult<T> {
val value: T
}
@Stable
class GyuResult<T>(result: T) : CustomResult<T> {
override var value by mutableStateOf(result)
}
@AndroidEntryPoint
class MainActivity() : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
Scaffold {
val result by remember { mutableStateOf(GyuResult(1))}
Column(modifier = Modifier.padding(it)) {
Button({ result.value++ } ) { }
GetResult(result)
}
}
}
}
}
@Composable
fun GetResult(result: CustomResult<Int>){
Text(text = result.value.toString())
}
// 변경하려면 무조건 새 인스턴스를 생성해야함
@Immutable
public data class User(
public val id: String,
public val nickname: String,
public val profileImages: List<String>,
)
// 가변 프로퍼티가 존재하여 불안정
data class Unstable(var name: Int)
// 불변 프로퍼티만 존재하기 때문에 안정적
data class Stable(val name: Int)
// interface List 가 존재하기에 불안정
data class Unstable2(
val name: Int,
val list: List<Int>
)
// Stable Annotation 을 사용하여 안정적
@Stable
data class Stable2(
val name: Int,
val list: List<Int>
)
// Immutable Annotation 을 사용하여 안정적
@Immutable
data class Immutable(
val name: Int,
val list: ImmutableList<Int>
)
// 내부가 불변이고 ImmutableList 로 구현하여 안정적
data class Immutable(
val name: Int,
val list: ImmutableList<Int>
)