해당 내용은 Compose Internals 1장 Composable 함수들 내용을 공부하며 기록한 내용입니다.

@Composable은 컴파일러에게 이 함수가 Compose 트리의 일부 노드를 생성한다는 의도를 전달합니다.
@Composable
fun Greeting(name: String) {
Text("Hello, $name!")
}
이 함수는 "Hello, $name!"이라는 UI 요소를 생성하고,
이를 Compose 트리에 하나의 노드로 방출(emit)합니다.
Compose는 UI를 메모리 상에서 트리(Tree) 구조로 표현합니다. 이 구조는 HTML의 DOM처럼, 각 Composable 함수가 하나의 노드가 되는 계층적 구조입니다.

Composable 함수는 실행되면 UI를 리턴하는 것이 아니라, 트리에 노드를 추가(emit) 합니다. 이 동작을 방출 이라고 부릅니다.
@Composable 어노테이션을 사용함으로써, 컴파일러에게 함수가 데이터를 하나의 노드(node)로 변환하여 Composable 트리(tree)에 기재하겠다는 의도를 전달합니다.
즉, Composable 함수를 @Composable (Input) → Unit 와 같은 형태로 볼 때, 입력값은 데이터가 되고, 출력은 일반적으로 생각하는 함수의 반환 값이 아니라, 트리에 요소를 삽입하기 위해 기재된 일련의 동작(action)이라고 볼 수 있습니다. 이 동작을 함수 실행의 부수 효과(side effect)로 발생한다고 말할 수 있습니다.
즉, Composable 함수는 “화면을 그리는 설계도”로서, 실행 시 트리에 UI 요소를 배치하는 역할을 합니다.
이때 함수의 리턴값은 보통 Unit이며, UI를 그리는 부수 효과(Side Effect)가 발생합니다.
트리의 인메모리 표현(in-memory representation)을 만들거나 업데이트하는 것
Composable 함수는 화면을 그리기 위한 트리 구조를 메모리에서 만들거나 업데이트 하는 것이 목적입니다.
Jetpack Compose는 UI를 그릴 때 실제 화면(UI)을 바로 만드는 게 아니라, 메모리 안에 UI 구조(트리 형태)를 먼저 만들게 됩니다.
Composable 함수는 읽은 데이터가 바뀌면 자동으로 다시 실행되며, 메모리상의 트리를 갱신(업데이트)합니다.
@Composable이 붙은 함수는 단지 UI를 그리는 게 아니라, Compose 런타임이 해당 함수에 대해 여러 최적화 기법을 적용할 수 있도록 설계된 특별한 타입이라는 뜻입니다.
일반 함수와 다르게 @Composable 함수는 다음과 같은 정보를 런타임에게 제공합니다:
Compose는 다음과 같은 최적화를 하려고 합니다:
이런 최적화를 하기 위해서는 이 Composable 함수가 어디서 어떻게 호출되며, 서로 영향을 주는지 아닌지에 대한 확실한 정보(확실성)이 필요합니다.
Compose가 “이 함수는 다른 함수와 완전히 독립적이구나” 혹은 “이 함수는 저 데이터가 바뀌면 꼭 다시 실행되어야 하겠네!” 같은 걸 미리 알고 있어야 최적화할 수 있습니다.
@Composable
fun ProfileImage() {
// 이 함수는 독립적입니다. 이름이 바뀌어도 다시 실행 안 해도 됩니다.
}
@Composable
fun Profile(name: String) {
Text(name) // 이름이 바뀌면 여긴 다시 실행되어야 합니다.
}
즉, @Composable 함수는 런타임에게 약속된 계약서로 비유할 수 있습니다.
계약을 믿고 런타임은 함수를 병렬로 돌리거나, 순서를 바꾸거나, 다시 실행하지 않도록 최적화할 수 있습니다.
Composable 함수의 속성은 대부분 Compose Compiler에 의해 활성화 됩니다.
@Composable 어노테이션이 붙은 함수는 Kotlin 컴파일러 플러그인인 Compose Compiler에 의해 변환됩니다. 이 변환 과정에서 컴파일러는 함수의 매개변수 목록 끝에 Composer 객체를 암묵적으로 추가합니다. 이 객체는 런타임에 주입되며, 모든 하위 Composable 호출로 전달되어 트리의 모든 수준에서 접근할 수 있게 됩니다.

아래와 같은 Composable 함수를
@Composable
fun NamePlate(name: String, lastname: String) {
Column(modifier = Modifier.padding(16.dp)) {
Text(text = name)
Text(text = lastname, style = MaterialTheme.typography.titleMedium)
}
}
Compose Compiler가 아래와 같이 변환합니다.
fun NamePlate(name: String, lastname: String, $composer: Composer<*>) {
Column(modifier = Modifier.padding(16.dp), $composer) {
Text(text = name, $composer)
Text(text = lastname, style = ..., $composer)
}
}
위 코드에서 확인할 수 있듯이 Composer는 트리 내에서 모든 Composable 호출로 전달됩니다.
Compose Compiler는 Composable 함수가 오로지 다른 Composable 함수에서만 호출될 수 있게 합니다.
즉,
Composer는 개발자가 작성하는 Composable 코드와 Compose Runtime 간의 중재자 역할을 합니다.
Composable 함수는 트리에 대한 변경 사항을 전달하고, 런타임 시에 트리의 형태를 빌드하거나 업데이트하기 위해 Composer를 사용합니다.
특정 작업이나 연산을 여러 번 반복하더라도 결과가 처음 수행한 결과와 동일하게 유지되는 성질
Recomposition 최적화의 기반
Composable 함수는 생성하는 노드 트리에 대해 멱등성을 가져야 합니다. 동일한 입력 매개변수를 사용하여 Composable 함수를 여러 번 다시 실행하더라도 동일한 트리가 생성되어야 합니다.
Compose Runtime은 recomposition과 같은 작업을 위해 이러한 멱등성이 제공하는 가정에 의존합니다.
입력값이 변경될 때 마다 Composable 함수를 다시 실행하여 업데이트된 정보를 방출시키고 트리를 업데이트 하는 작업.
Compose Runtime이 역할을 수행함
UI를 구성하는 데이터가 변경되었을 때,
Compose는 이 과정에서 UI 전체를 다시 그리지 않고, 변경된 부분만 정확하게 다시 그립니다. 이걸 가능하게 하는 조건 중 하나가 바로 멱등성입니다.
Recomposition의 과정은 트리를 아래로 순회하면서 어떤 노드를 재구성 해야 하는지 확인합니다. 이 과정에서 입력값이 변경된 노드만 recomposition을 수행하고 나머지는 생략합니다. 특정 노드를 생략하는 것은 해당 노드를 대표하는 Composable 함수가 멱등성의 성질을 가질 때만 가능합니다. 그 이유는 런타임은 동일한 입력값을 제공할 경우 동일한 결과를 생성한다고 가정할 수 있기 때문입니다.
동일한 입력값에 대한 결과는 이미 메모리에 적재되어 있으므로 Compose는 다시 실행할 필요가 없고, 결과적으로 생략할 수 있습니다.
호출된 함수의 제어를 벗어나서 발생할 수 있는 예상치 못한 모든 동작을 의미합니다.
이런 작업은 입력값에만 의존하지 않고, 외부 요인에 따라 달라질 수 있기 때문에 사이드 이펙트로 간주됩니다.
Compose Runtime은 Composable 함수가 예측 가능하도록(결정론적인) 기대하기 때문에 사이드 이펙트가 포함된 Composable 함수는 예측이 어려워지고, 결과적으로 Compose에게 좋지 않습니다.
이 말인 즉, 사이드 이펙트는 Compose 내에서 Compose 내에서 아무 통제를 받지 않고 여러 번 실행될 수 있습니다. Composeable 함수가 사이드 이펙트를 실행한다면 매 함수 호출 시마다 새로운 프로그램 상태를 생성할 수 있으므로, Compose Runtime에게 필수적인 멱등성을 따르지 않게 됩니다.
@Composable
fun FetchData() {
val data = fetchFromNetwork() // 사이드 이펙트 발생
Text(data)
}
Composable 함수는 근본적으로 Runtime에 의해 짧은 시간 내에 여러 번 다시 실행될 수 있으며, 이로 인해 네트워크 요청이 여러 번 수행되어 제어를 벗어날 수 있습니다.
최악의 상황은 이러한 사이드 이펙트가 아무 조건 없이 다른 스레드에서 실행될 수 있다는 것입니다.
Compose Runtime은 Composable 함수에 대한 실행 전략을 선택할 권한을 보유하고 있습니다.
이는 하드웨어의 멀티 코어의 이점을 활용하기 위해 recomposition을 다른 스레드로 이전시킬 수 있거나,
필요성이나 우선순위에 따라 임의의 순서로 실행할 수 있습니다.
@Composable
fun MainScreen() {
Header() // 외부 상태를 변경
ProfileDetail() // 그 외부 상태를 읽음 ← ⚠ 위험한 패턴
EventList()
}
Header()에서 어떤 전역 변수를 설정하고, ProfileDetail()에서 전역 변수를 사용한다고 가정하겠습니다.
하지만 Compose는 이 함수들을 아무 순서로나 혹은 병렬로 실행할 수 있기 때문에, 예상한 동작을 보장할 수 없습니다.
Composable 함수를 stateless(무상태, 상태를 보존하지 않음)하게 만들려고 노력해야 합니다.
Composable 함수는 모든 입력값은 매개변수로서 받고, 결과를 생성하기 위해 주어진 입력값만을 사용합니다.
stateful(상태유지, 상태를 보유하는) 프로그램을 개발하기 위해서는
사이드 이펙트가 필요하므로, 특정 단계에서는 우리가 사이트 이펙트를 실행해야만 합니다 (주로 Composable 트리의 루트에서 빈번하게 실행됩니다).
이를 위해 Compose는 Effect Handlers (이펙트 핸들러)라는 안전한 도구를 제공합니다.
Composable 함수 내에서 아무런 제어를 받지 못하고 직접적으로 이펙트가 호출되는 것을 방지하는 역할 수행
이펙트 핸들러는 사이드 이펙트가 Composable의 라이프사이클을 인식하도록 하여, 해당 라이프사이클에 의해 제한되거나 실행될 수 있게 합니다.
Composable 노드가 트리를 떠날 때 자동으로 이펙트를 해제하거나 취소할 수 있게 하고, 이펙트에 주어진 입력값이 변경되면 재실행시키거나, 심지어 동일한 이펙트를 recomposition 과정에서 유지시키고 한 번만 호출되게 할 수 있습니다.
일반 함수는 한 번 호출되면, 호출된 위치로부터 순차적으로 실행됩니다.
다른 함수를 호출할 수는 있지만, 재호출(re-run) 되지는 않습니다.

recomposition으로 여러 번 다시 시작될 수 있습니다.
그래서 런타임은 함수가 재실행될 수 있도록 해당 함수들에 대한 참조를 유지합니다.

Composable 함수는 관찰하는 상태(state)의 변화에 기반하여 반작용적으로 재실행되도록 설계되었습니다.
Compose Compiler는 일부 상태(state)를 읽는 모든 Composable 함수를 찾아 Compose Runtime에게 재시작하는 방법을 가르치는데 필요한 코드를 생성합니다.
상태를 읽지 않는 Composable은 재시작할 필요가 없으므로, Compose Runtime에게 해당 방법을 가르칠 이유가 없습니다.
Composable 함수들은 UI를 구축하거나 반환하지 않습니다.
Composable은 단순히 인메모리 구조를 구축 및 업데이트하기 위한 데이터를 방출할 뿐입니다.
이는 Composable을 더욱 빠르게 만들며, 런타임이 아무 문제없이 해당 함수를 여러번 실행할 수 있도록 합니다.
함수가 입력값에 기반하여 결과를 캐싱하는 기법
즉, 입력값이 같으면 결과를 캐싱해서 다시 계산하지 않도록 하는 기법
항상 같은 입력에는 같은 출력을 주고, 외부 상태에 영향을 주지 않는 함수
메모이제이션이 가능한 함수 조건입니다.
함수 메모이제이션에서, 함수 호출은 그 이름, 타입 및 매개변수 값의 조합을 통해 식별됩니다.
Compose의 경우는 추가적인 요소가 고려됩니다.
Composable 함수는 소스 코드 내 호출 위치에 대한 불변의 정보를 가지고 있습니다.
Compose Runtime은 동일한 함수가 동일한 매개변수 값으로 다른 위치에서 호출될 때, 동일한 Composable 부모 트리 내에서 고유한 다른 ID를 생성합니다.
@Composable
fun MyComposable() {
Text("Hello") // 위치 A
Text("Hello") // 위치 B
Text("Hello") // 위치 C
}

Compose는 매 프레임마다 UI를 다시 그리는 방식이 아니라, 필요한 부분만 재구성(recomposition)합니다.
종종 Compose Runtime 입장에서 Composable 함수에게 고유한 정체성을 할당하는 것이 어려운 경우가 있습니다. 반복문에서 생성된 Composable의 리스트 형태 입니다.
@Composable
fun MoviesScreen(movies: List<Movie>) {
Column {
for (movie in movies) {
MovieOverview(movie)
}
}
}
이 코드에서 MovieOverview 는 매번 같은 위치에서 호출되지만, 각각의 talk 요소는 리스트 내에서 다른 항목으로 간주되고, 결과적으로 트리에서는 서로 다른 노드로 구성됩니다. 이와 같은 경우에 Compose Runtime은 고유한 ID를 생성하고 여전히 서로 다른 Composable 함수를 구별할 수 있도록 호출 순서에 의존합니다.
이러한 방식은 리스트 끝에 새로운 요소를 추가할 때는 잘 동작합니다. 리스트 내의 기존 Composable 함수들이 여전히 같은 위치에 있기 때문입니다. (색상이 동일 = 컴포저블 재구성되지 않았음을 의미)

하지만, 리스트 상단이나 중간에 요소를 추가한다면 Compose Runtime은 요소 삽입이 발생하는 지점 아래의 모든 MovieOverview Composable 함수에 대해서 recomposition을 발생시키게 됩니다.

이유는 Composable 함수들의 위치가 변경되었기 때문인데, 심지어 해당 함수들의 입력값이 변경되지 않았더라도 해당합니다. 이런 방식은 업데이트가 생략되었어야 할 Composable 함수들에게 recomposition이 발생했기 때문에 매우 비효율적입니다.
이 문제를 해결하기 위해 Compose는 key 라는 Composable 함수를 제공하는데, 이 함수를 이용하여 Composable 함수에게 명시적인 키 값을 지정할 수 있습니다.
@Composable
fun MoviesScreenWithKey(movies: List<Movie>) {
Column {
for (movie in movies) {
key(movie.id) { // Unique ID for this movie
MovieOverview(movie)
}
}
}
}
각 MoviewOverview Composable에 대한 키 값으로 movie.id(고유한)를 사용하고 있으며, 이것은 Composable Runtime이 Composable 함수의 위치에 상관없이 리스트에 속한 모든 항목의 정체성을 유지하도록 합니다.

위치 기반 기억법은 Compose Runtime이 설계에 따라 Composable 함수를 기억할 수 있게 해 줍니다.
Compose Compiler에 의해 재시작(Restartable)이 가능하다고 추론된 모든 Composable 함수는 recomposition을 생략할 수 있어야 하며, 인메모리에 자동적으로 기억됨을 의미합니다.
remember는 트리의 상태를 유지하는 인메모리 구조에서 값을 메모리에 읽고 쓰는 역할을 수행하는 Composable 함수입니다.
Compose는 매번 UI를 그릴 때마다 전체 함수를 실행하지만, 기존 결과를 재활용해서 불필요한 연산을 피합니다. 이를 위해 상태를 기억하고 있어야 하고, 이때 사용되는 것이 remember입니다.
@Composable
fun FilteredImage(path: String) {
val filters = remember { computeFilters(path) }
ImageWithFiltersApplied(filters)
}
@Composable
fun ImageWithFiltersApplied(filters: List<Filter>) {
TODO()
}
Compose는 내부적으로 Slot Table이라는 구조를 이용합니다. 쉽게 말해:
Compose에서 메모이제이션은 애플리케이션 전체에 걸쳐서 적용되지 않습니다.
무언가가 메모리에 기록될 때, 메모이제이션을 호출하는 Composable의 컨텍스트 내에서만 수행됩니다.
suspend 함수는 비동기 처리 또는 중단 가능한 작업을 Kotlin 코루틴에서 다루기 위해 사용됩니다.
suspend fun publishTweet(tweet: Tweet): Post
위의 코드는 Kotlin 컴파일러에 의해 아래와 같이 변경됩니다.
fun publishTweet(tweet: Tweet, continuation: Continuation<Post>): Any
Continuation은 콜백처럼 작동하는 객체이며, 실행이 어디까지 왔고 어떤 데이터를 가지고 있는지를 기억합니다.
즉, 다양한 중단점에서 실행을 일시 중단하고 재개하는 데 필요한 모든 정보를 담고 있습니다.
재시작(restartable)이 가능하고, 상태(state) 등으로부터 반응할 수 있도록 만듭니다.
이 개념은 동기(synchronous) 함수와 비동기(asynchronous) 함수가 서로 완전히 다른 특성을 가지며, 쉽게 섞이지 않는다는 사실에서 나왔습니다.
Bob Nystrom은 동기 함수는 파란색, 비동기 함수는 빨간색처럼 생각할 수 있다고 했습니다.
즉, 함수의 색이 다르면 섞을 수 없고, 중간에 통합 지점(entry point)이 필요하다는 의미입니다.
suspend fun fetchUser(): User
이 제한 덕분에 Kotlin은비동기 실행을 안전하고 명확하게 제어할 수 있습니다.
@Composable
fun Greeting(name: String) {
Text("Hello, $name")
}
노드 트리의 변경사항을 기술하기 위한 것입니다.inline 함수란 함수를 호출할 때, 실제 코드가 그 자리에 복사되는 함수입니다.
inline fun doSomething(action: () -> Unit) {
println("Start")
action()
println("End")
}
컴파일러가 doSomething을 호출하면
doSomething {
println("Doing work")
}
요래 됩니다
println("Start")
println("Doing work")
println("End")
@Composable
fun SpeakerList(speakers: List<Speaker>) {
Column {
speakers.forEach {
Speaker(it) // 어떻게 이게 되는지?
}
}
}
forEach는 사실 inline 함수이기 때문에, 이 안에서 Speaker(it) 같은 Composable 호출을 허용할 수 있습니다.
인라인 덕분에 람다 함수 안의 Composable 호출이SpeakerList 함수 본문에 복사되어 들어가기 때문에Composable → Composable 관계가 유지됩니다.
@Composable 어노테이션은 컴파일 시점에 함수의 타입을 효과적으로 변경합니다.
함수의 구문(syntax)적 관점에서 Composable 함수의 타입은 @Composable (T) → A 입니다.
여기서 A는 Unit일 수도 있고, 함수가 값을 반환하는 경우(예를 들어 remember) 다른 타입일 수도 있습니다.
일반적인 람다를 선언하는 것처럼 Composable 람다를 선언할 수 있습니다.
val textComposable: @Composable (String) -> Unit = {
Text(
text = it,
style = MaterialTheme.typography.subtitle1
)
}
@Composable
fun NamePlate(name: String, lastname: String) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = name,
style = MaterialTheme.typography.h6
)
textComposable(lastname)
}
}
또한 Composable 함수는 @Composable Scope.() → A와 같은 형태의 타입을 가질 수 있는데, 이는 특정 Composable로만 정보 범위를 지정하는 데 자주 사용됩니다.
inline fun Box(
...,
content: @Composable BoxScope.() -> Unit
) {
// ...
Layout(
content = { BoxScopeInstance.content() },
measurePolicy = measurePolicy,
modifier = modifier
)
}
@Composable 어노테이션은 런 타임 시 Composable 함수의 유효성을 검사하고 사용하는 방법을 변경하는데, 이것이 바로 Composable 함수가 Kotlin의 표준 함수와 다른 타입으로 간주되는 이유입니다.