전에 포스팅에서 CompositionLocal을 간단히 다룬 적이 있지만, 이번에는 staticCompositionLocalOf와 compositionLocalOf의 차이를 중심으로 다시 정리해보려고 합니다.
상태 호이스팅(State Hoisting)과 전달의 문제
컴포저블 함수는 트리(tree) 구조로 이루어져 있습니다. 그리고 상태는 일반적으로 트리의 가능한 한 상위 노드에서 선언한 뒤 하위 노드로 전달하는 것이 권장됩니다(= 상태 호이스팅).

따라서, 트리의 깊이가 깊어질수록(예: depth = n) 같은 상태를 최하위까지 전달하려면 n개의 함수 매개변수를 추가해야 하는 문제가 있습니다.
예를 들어 트리 깊이가 100이라면, 단순히 상태 하나를 전달하기 위해 함수 100개에 전부 매개변수를 추가해야 하죠. 이는 매우 번거롭고, 유지보수에도 큰 부담을 주게 됩니다.
CompositionLocal의 등장
이 문제를 해결하기 위해 CompositionLocal이 존재합니다.
CompositionLocal은 상위에서 선언된 상태를 하위 트리에서 직접 접근할 수 있도록 해줍니다. 즉, 일일이 매개변수로 전달할 필요가 없다는 뜻입니다.
예시로, 다음과 같이 특정 범위를 지정해서 CompositionLocalProvider를 Composable4에서만 감쌌다고 가정해봅시다.

해당 그림처럼 Composable4 한정으로 CompositionLocal의 범위를 지정했다면, Composable4, Composable5, Composable6까지만 해당 상태에 접근할 수 있습니다.
이번에는 앞서 설명한 트리 구조에서 특정 상태(예: 빨간색 값)를 하위 컴포저블 함수들에게 전달하는 상황을 가정해보겠습니다.
즉, 상위에서 정의한 빨간색 값을 하위 컴포저블로 내려주고 싶다면, 다음과 같이 코드를 작성할 수 있습니다.
val ColorCompositionLocal = staticCompositionLocalOf {
Color.Blue // 기본값을 정의
}
@Composable
fun Composable1() {
...
CompositionLocalProvider(ColorCompositionLocal.provides(Color.Red)) {
Composable4()
}
}
@Composable
fun Composable4() {
...
}
코드에서는 상위 컴포저블 함수에서 하위 컴포저블 함수까지 상태를 전달하기 위해 CompositionLocalProvider를 사용했습니다.
CompositionLocalProvider는 하나의 CompositionLocal만 제공하는 것이 아니라, 여러 개를 동시에 제공할 수도 있습니다. 함수 시그니처를 보면 다음과 같이 정의되어 있습니다.
fun CompositionLocalProvider(
vararg values: ProvidedValue<*>,
content: @Composable () -> Unit)
그리고 이렇게 CompositionLocalProvider로 감싸진 하위 컴포저블 함수들은 CompositionLocal.current를 통해 다음과 같이 현재 제공된 값을 손쉽게 읽을 수 있습니다.
@Composable
fun Composable3() {
Text(
modifier = Modifier.background(color = ColorCompositionLocal.current), // 빨간색
text = "Composable3"
)
...
}
덕분에 매개변수로 일일이 값을 전달하지 않아도 동일한 범위 안에 있는 모든 하위 컴포저블에서 공통된 상태를 공유할 수 있게 됩니다.
CompositionLocal 전체 예제코드
위에서 언급한 트리 및 다음의 전체 예제코드를 통해 CompositionLocal에 대한 통찰을 얻을 수 있습니다.
val ColorCompositionLocal = staticCompositionLocalOf {
Color.Blue // 기본값을 정의 한다
}
@Composable
fun Composable1() {
Column {
Text(
modifier = Modifier.background(color = ColorCompositionLocal.current),
text = "Composable1"
)
CompositionLocalProvider(ColorCompositionLocal.provides(Color.Cyan)) {
Composable2()
}
CompositionLocalProvider(ColorCompositionLocal.provides(Color.Red)) {
Composable4()
}
}
}
@Composable
fun Composable2() {
Text(
modifier = Modifier.background(color = ColorCompositionLocal.current),
text = "Composable2"
)
Composable3()
}
@Composable
fun Composable3() {
Text(
modifier = Modifier.background(color = ColorCompositionLocal.current),
text = "Composable3"
)
}
@Composable
fun Composable4() {
Text(
modifier = Modifier.background(color = ColorCompositionLocal.current),
text = "Composable4"
)
CompositionLocalProvider(ColorCompositionLocal.provides(Color.Green)) {
Composable5()
}
Composable6()
}
@Composable
fun Composable5() {
Text(
modifier = Modifier.background(color = ColorCompositionLocal.current),
text = "Composable5"
)
}
@Composable
fun Composable6() {
Text(
modifier = Modifier.background(color = ColorCompositionLocal.current),
text = "Composable6"
)
}
@Preview(showBackground = true)
@Composable
private fun DefaultPreview() {
Composable1()
}
[Preview]

staticCompositionLocalOf과 compositionLocalOf
CompositionLocal은 상태를 제공하는 일종의 컨테이너와 같습니다.
실제로 CompositionLocal을 만드는 2가지 방법이 있습니다.
// StaticProvidableCompositionLocal로 만들기
val staticCompositionLocal = staticCompositionLocalOf {
${상태}
}
// DynamicProvidableCompositionLocal로 만들기
val dynamicCompositionLocal = compositionLocalOf {
${상태}
}
staticCompositionLocalOf는 말 그대로 정적인 CompositionLocal을 만드는 방식입니다. 이 경우 값이 변경되면, 해당 값을 제공하는 람다 전체가 재구성됩니다.
즉, 그 범위에 포함된 모든 자식 컴포저블이 다시 그려지기 때문에 변경이 드문 값에 사용하는 것이 적합합니다.
예를 들어 앱 전역에서 사용하는 테마 색상이나 폰트 스타일처럼 거의 바뀌지 않는 값들을 관리할 때 유용합니다.
반면 compositionLocalOf는 동적인 CompositionLocal을 만드는 방식입니다.
이 경우 값이 바뀌더라도 해당 값을 실제로 읽는 컴포저블만 재구성됩니다. 따라서 자주 바뀌는 상태를 다룰 때 훨씬 효율적입니다.
예를 들어 로그인된 사용자 정보, 리스트에서 현재 선택된 아이템, 혹은 UI 내에서 자주 변경되는 상태 값 같은 경우에 적합합니다.
정리하자면, 두 방식의 가장 큰 차이는 재구성(Recomposition) 범위에 있습니다. staticCompositionLocalOf를, 변경이 잦은 상태라면 compositionLocalOf를 사용하시는 것이 바람직합니다.
다음 예제는 컴포저블 함수의 계층별로 재구성이 발생하는 시점에 카운트를 증가시키고 해당 카운트를 보여줍니다. 이 예제를 통해 Static CompositionLocal과 Dynamic CompositionLocal의 차이를 명확히 알 수 있습니다.
var color by mutableStateOf(Color.Red)
private var outsideStatic = 0
private var centerStatic = 0
private var insideStatic = 0
private var outsideDynamic = 0
private var centerDynamic = 0
private var insideDynamic = 0
private val ColorComposableLocalStatic = staticCompositionLocalOf<Color> { error("기본값 없음") }
private val ColorComposableLocalDynamic = compositionLocalOf<Color> { error("기본값 없음") }
@Composable
fun CompositionLocals() {
Column {
Text("staticCompositionLocalOf")
CompositionLocalProvider(ColorComposableLocalStatic provides color) {
outsideStatic++
MyBox(color = Color.Yellow, outsideStatic, centerStatic, insideStatic) {
centerStatic++
MyBox(color = ColorComposableLocalStatic.current, outsideStatic, centerStatic, insideStatic) {
insideStatic++
MyBox(color = Color.Yellow, outsideStatic, centerStatic, insideStatic) {
}
}
}
}
Text("compositionLocalOf")
CompositionLocalProvider(ColorComposableLocalDynamic provides color) {
outsideDynamic++
MyBox(color = Color.Yellow, outsideDynamic, centerDynamic, insideDynamic) {
centerDynamic++
MyBox(color = ColorComposableLocalDynamic.current, outsideDynamic, centerDynamic, insideDynamic) {
insideDynamic++
MyBox(color = Color.Yellow, outsideDynamic, centerDynamic, insideDynamic) {
}
}
}
}
Button(onClick = {
color = if (color == Color.Green) {
Color.Red
} else {
Color.Green
}
}, modifier = Modifier.fillMaxWidth()) {
Text("Click Me")
}
}
}
@Composable
fun MyBox(color: Color,
outside: Int,
center: Int,
inside: Int,
content: @Composable BoxScope.() -> Unit) {
Column (Modifier.background(color)) {
Text("outside = $outside, center = $center, inside = $inside")
Box(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
content = content
)
}
}
@Preview(showBackground = true)
@Composable
private fun CompositionLocalsPreview() {
CompositionLocals()
}
위 코드의 실행 결과는 아래 영상을 통해 확인하실 수 있습니다.
영상을 보시면 compositionLocalOf의 경우 current를 실제로 사용하는 부분만 재구성되어 카운트가 증가하는 것을 확인할 수 있습니다. 반면 staticCompositionLocalOf는 값이 변경될 때 해당 범위 전체가 재구성되기 때문에, 하위에 포함된 모든 컴포저블의 카운트가 함께 증가하는 모습을 볼 수 있습니다.
그렇다면 왜 이런 차이가 발생하는 것일까요?
핵심은 바로 remember의 유무에 있습니다.
먼저 staticCompositionLocalOf의 내부 구현을 살펴보면 다음과 같습니다.
fun <T> staticCompositionLocalOf(defaultFactory: () -> T): ProvidableCompositionLocal<T> =
StaticProvidableCompositionLocal(defaultFactory)
internal class StaticProvidableCompositionLocal<T>(defaultFactory: () -> T) :
ProvidableCompositionLocal<T>(defaultFactory) {
@Composable
override fun provided(value: T): State<T> = StaticValueHolder(value)
}
internal data class StaticValueHolder<T>(override val value: T) : State<T>
즉, staticCompositionLocalOf는 단순히 State 역할을 하는 StaticValueHolder를 반환할 뿐이고, 이 값은 remember로 기억되지 않습니다. 그렇기 때문에 값이 변경될 경우 해당 CompositionLocal을 제공한 범위 전체가 다시 그려지게 됩니다.
반면 compositionLocalOf는 내부 구현에서 remember와 mutableStateOf를 활용합니다.
fun <T> compositionLocalOf(
policy: SnapshotMutationPolicy<T> =
structuralEqualityPolicy(),
defaultFactory: () -> T
): ProvidableCompositionLocal<T> = DynamicProvidableCompositionLocal(policy, defaultFactory)
internal class DynamicProvidableCompositionLocal<T>(
private val policy: SnapshotMutationPolicy<T>,
defaultFactory: () -> T
) : ProvidableCompositionLocal<T>(defaultFactory) {
@Composable
override fun provided(value: T): State<T> =
remember { mutableStateOf(value, policy) }.apply {
this.value = value
}
}
여기서는 remember를 사용하여 상태를 저장하고 있으며, 내부적으로는 currentComposer를 통해 값의 변경을 추적합니다. 이 방식은 mutableStateOf가 Compose의 상태 시스템과 연결되어 있기 때문에, 값이 바뀔 때 실제로 해당 값을 소비하고 있는 컴포저블 함수만 재구성하게 됩니다.
정리하자면, staticCompositionLocalOf는 단순히 값만 들고 있는 정적 구조라서 변경 시 전체 범위가 재구성되는 반면, compositionLocalOf는 remember와 mutableStateOf를 기반으로 동작하기 때문에, 값이 변했을 때 current를 읽는 특정 컴포저블만 부분적으로 재구성되는 것입니다.
끝.
참고자료
https://charlezz.com/?p=46403#:~:text=staticCompositionLocalOf와%20compositionLocalOf의%20차이
Compose의 CompositionLocal 이해하기 | 찰스의 안드로이드
컴포저블 함수는 트리(tree)로 구성된다. 이 때 상태는 일반적으로 트리에서 가능한 한 가장 높은 노드에 선언되어야 한다(상태 호이스팅). 그리고 일반적으로 이렇게 선언된 상태는 하위 트리로
charlezz.com
https://jgeun97.tistory.com/365
[Android Compose] CompositionLocalOf는 언제 쓰는거지?
Android Compose CompositionLocalCompositionLocalCompose에서 각 Composable 함수의 매개변수는 UI 트리를 통해 아래로 흐르는 구조입니다.이런 구조는 색상이나 유형 스타일과 같이 매우 자주 널리 사용되는 데이
jgeun97.tistory.com
https://developer.android.com/develop/ui/compose/compositionlocal?hl=ko
CompositionLocal을 사용한 로컬 범위 지정 데이터 | Jetpack Compose | Android Developers
이 페이지는 Cloud Translation API를 통해 번역되었습니다. CompositionLocal을 사용한 로컬 범위 지정 데이터 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Composition
developer.android.com
'Android > Compose' 카테고리의 다른 글
| [Android] Compose의 중첩 스크롤(Nested Scroll) 완벽하게 이해하기 (0) | 2025.10.12 |
|---|---|
| [Android] Compose에서 이미지를 자유자재로 커스터마이징 해보기 (0) | 2025.09.17 |
| [Android] Compose에서 자연스러운 텍스트 표시하기(Font Padding, Baseline, LineHeight) (8) | 2025.07.30 |
| [Android] Compose의 ConstraintLayout (1) | 2025.06.05 |
| [Android] Compose 레이아웃에서 Intrinsic 측정 (0) | 2025.06.02 |