Jetpack Compose의 핵심 개념 중 하나는 리컴포지션(Recomposition)입니다. Compose는 상태(State)가 변경될 때, 변경된 부분만 다시 그리는 "스마트 리컴포지션"을 지원합니다. 여기서 중요한 역할을 하는 것이 바로 스코프(scope)입니다.
스코프(scope)는 Compose에서 상태 변경을 감지하고, 재구성(recomposition) 범위를 제한하는 경계를 나타냅니다.
(Compose가 “이 구역만 다시 그리겠다”고 잡아두는 최소 단위..?)
Compose에서 스코프를 생성하는 기준은 다음과 같습니다.
non-inline Composable 함수: inline으로 표시되지 않고 Unit을 반환하는 함수는 스코프를 생성합니다.
inline Composable 함수: Column, Row, Box 등은 inline 함수로, 자체적인 스코프를 생성하지 않습니다.
즉, non-inline Composable 함수는 별도의 스코프 경계를 만들어 상태가 변경될 때 리컴포지션의 영향을 최소화할 수 있습니다.
Column은 inline 함수로 스코프를 생성하지 않습니다. (inline 함수에 관한 글 : https://velog.io/@minlove2013/KotlinJava-%EC%A0%9C%EB%84%A4%EB%A6%AD%EA%B3%BC-reified-%ED%82%A4%EC%9B%8C%EB%93%9C-%ED%8C%8C%ED%95%B4%EC%B9%98%EA%B8%B0-with-inline)
아래 예시 코드를 봅시다.
Column(
modifier = Modifier.background(getRandomColor())
) {
println("Bottom Column")
Text(
text = "Update1: $update1",
textAlign = TextAlign.Center,
color = getRandomColor()
)
}
Column이 inline 함수이기 때문에, $update1 상태가 변경될 때, 가장 가까운 상위 스코프 전체가 리컴포지션 됩니다.
결과적으로 getRandomColor()가 불필요하게 다시 호출되어 배경색이 변경되는 현상이 발생합니다.
또 다른 예시를 봅시다.
@Composable
private fun Sample5() {
Column(
modifier = Modifier
.background(getRandomColor())
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
var update by remember { mutableStateOf(0) }
Button(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.fillMaxWidth()
.padding(vertical = 4.dp),
colors = ButtonDefaults.buttonColors(containerColor = getRandomColor()),
onClick = {
update++
}) {
Text("update: $update", color = getRandomColor())
}
}
}
이 코드의 실행 결과는 다음과 같습니다.

@Composable
private fun Sample1() {
Column(
modifier = Modifier.fillMaxSize().background(getRandomColor()),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
var update1 by remember { mutableStateOf(0) }
Button(
onClick = { update1++ },
colors = ButtonDefaults.buttonColors(containerColor = getRandomColor())
) {
Text("Update1: $update1")
}
Text(
text = "update: $update1",
textAlign = TextAlign.Center,
color = getRandomColor()
)
}
}
이 코드의 실행 결과는 다음과 같습니다.

왜 이런 결과가 나오는지 생각해봅시다.
Sample5() 함수는 Button 의 @Composable인 RowScope 람다 함수 내부에서만 update 상태를 읽습니다. 이 말은 즉 새로운 스코프 내에서 값을 읽기 때문에 해당 스코프에서만 Recomposition이 발생합니다.
하지만 Sample1() 함수는 Button의 RowScope 람다 함수 내부에서 update 상태값을 읽지만, 그 아래 있는 Text에서 바로 상태를 읽습니다.
위에서 설명했듯 Column은 inline 함수이기 때문에 Text의 자체 Scope가 존재하지 않기 때문에 Text에서 상태값을 읽는다 -> Text의 Scope가 존재하지 않아 그 자체만 Recomposition 되지 않는다 -> 부모 스코프 (Column)가 Recomposition 됩니다.
커스텀 컴포저블로 스코프를 만들어서 Column이 상위 스코프가 되지 않도록 non-inline Composable을 사용하여 별도의 스코프를 생성함으로써 해결할 수 있습니다.
@Composable
fun RandomColorColumn(content: @Composable () -> Unit) {
Column(
modifier = Modifier
.padding(4.dp)
.shadow(1.dp, shape = CutCornerShape(topEnd = 8.dp))
.background(getRandomColor()) // ← 이곳에서 배경색을 결정
.padding(4.dp)
) {
content() // ← 자식 Composable이 여기에 들어감
}
}
사용 예시:
RandomColorColumn {
println("Bottom Column")
Text(
text = "Update1: $update1", // ← 상태 변경이 일어나도
textAlign = TextAlign.Center,
color = getRandomColor()
)
}
이렇게 하면 상태가 변경될 때 RandomColorColumn 내부만 리컴포지션되며, 외부에서 정의된 배경색은 바뀌지 않습니다.
Before (문제 상황):
ParentComposable { // 전체 스코프가 리컴포지션됨
Column { // inline이므로 별도의 스코프 아님
Text("Update1: $update1") // 상태 읽기 → 상위 스코프 전체 리컴포지션
}
}
After (해결됨):
ParentComposable {
RandomColorColumn { // 새로운 스코프 생성
Text("Update1: $update1") // 상태 읽기 → 이 스코프만 리컴포지션
}
}
▶︎ RandomColorColumn 호출부 (non‑inline) ←─ 스코프 A
└─ Column (inline, 스코프 미생성)
└─ content() 호출 (non‑inline 람다) ←─ 스코프 B
inline 키워드와 scope를 이해하여 Recomposition을 최대한 줄여봅시다.
잘못된 내용이 있을 수 있습니다. 댓글 언제나 환영입니다.