Compose Internals - Composable 함수들

동키·2025년 6월 2일

안드로이드

목록 보기
12/14

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

1.1 Composable 함수의 의미

@Composable은 컴파일러에게 이 함수가 Compose 트리의 일부 노드를 생성한다는 의도를 전달합니다.

@Composable
fun Greeting(name: String) {
    Text("Hello, $name!")
}

이 함수는 "Hello, $name!"이라는 UI 요소를 생성하고,
이를 Compose 트리에 하나의 노드로 방출(emit)합니다.

🌳 Composable Tree란?

Compose는 UI를 메모리 상에서 트리(Tree) 구조로 표현합니다. 이 구조는 HTML의 DOM처럼, 각 Composable 함수가 하나의 노드가 되는 계층적 구조입니다.

Emit: UI를 방출 한다는 의미

Composable 함수는 실행되면 UI를 리턴하는 것이 아니라, 트리에 노드를 추가(emit) 합니다. 이 동작을 방출 이라고 부릅니다.

@Composable 어노테이션을 사용함으로써, 컴파일러에게 함수가 데이터를 하나의 노드(node)로 변환하여 Composable 트리(tree)에 기재하겠다는 의도를 전달합니다.

즉, Composable 함수를 @Composable (Input) → Unit 와 같은 형태로 볼 때, 입력값은 데이터가 되고, 출력은 일반적으로 생각하는 함수의 반환 값이 아니라, 트리에 요소를 삽입하기 위해 기재된 일련의 동작(action)이라고 볼 수 있습니다. 이 동작을 함수 실행의 부수 효과(side effect)로 발생한다고 말할 수 있습니다.

즉, Composable 함수는 “화면을 그리는 설계도”로서, 실행 시 트리에 UI 요소를 배치하는 역할을 합니다.

이때 함수의 리턴값은 보통 Unit이며, UI를 그리는 부수 효과(Side Effect)가 발생합니다.

Composable 함수를 실행하는 목적

트리의 인메모리 표현(in-memory representation)을 만들거나 업데이트하는 것

Composable 함수는 화면을 그리기 위한 트리 구조를 메모리에서 만들거나 업데이트 하는 것이 목적입니다.

Jetpack Compose는 UI를 그릴 때 실제 화면(UI)을 바로 만드는 게 아니라, 메모리 안에 UI 구조(트리 형태)를 먼저 만들게 됩니다.

Composable 함수는 읽은 데이터가 바뀌면 자동으로 다시 실행되며, 메모리상의 트리를 갱신(업데이트)합니다.

  • 삽입: 새 데이터가 생기면 새로운 노드를 추가
  • 제거: 데이터가 사라지면 해당 노드를 삭제
  • 교체: 값이 바뀌면 노드를 바꿈
  • 위치 이동: 순서가 바뀌면 재배치

1.2 Composable 함수의 속성

@Composable이 붙은 함수는 단지 UI를 그리는 게 아니라, Compose 런타임이 해당 함수에 대해 여러 최적화 기법을 적용할 수 있도록 설계된 특별한 타입이라는 뜻입니다.

일반 함수와 다르게 @Composable 함수는 다음과 같은 정보를 런타임에게 제공합니다:

  • 이 함수는 Compose 트리의 노드다
  • 다시 실행(Recomposition)될 수 있다
  • 상태를 기억하거나 읽을 수 있다

Compose Runtime 최적화

Compose는 다음과 같은 최적화를 하려고 합니다:

  • 병렬 Composition: 독립적인 UI는 동시에 그릴 수 있음
  • 스마트 Recomposition: 바뀐 부분만 다시 그림
  • 위치 기억법(positional memoization): UI 위치를 기준으로 상태 기억
  • 선택적 실행 순서 변경: 덜 중요한 UI는 나중에 그리기 등

이런 최적화를 하기 위해서는 이 Composable 함수가 어디서 어떻게 호출되며, 서로 영향을 주는지 아닌지에 대한 확실한 정보(확실성)이 필요합니다.

확실성

Compose가 “이 함수는 다른 함수와 완전히 독립적이구나” 혹은 “이 함수는 저 데이터가 바뀌면 꼭 다시 실행되어야 하겠네!” 같은 걸 미리 알고 있어야 최적화할 수 있습니다.

@Composable
fun ProfileImage() {
    // 이 함수는 독립적입니다. 이름이 바뀌어도 다시 실행 안 해도 됩니다.
}

@Composable
fun Profile(name: String) {
    Text(name) // 이름이 바뀌면 여긴 다시 실행되어야 합니다.
}

즉, @Composable 함수는 런타임에게 약속된 계약서로 비유할 수 있습니다.

계약을 믿고 런타임은 함수를 병렬로 돌리거나, 순서를 바꾸거나, 다시 실행하지 않도록 최적화할 수 있습니다.


1.3 호출 컨텍스트 (Calling context)

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

  • Composer는 UI 트리를 만들고 추적하고 갱신하는 관리자입니다.
  • 우리가 @Composable 함수를 호출하면, 자동으로 이 Composer 객체가 전달되어 트리를 구성합니다.
  • 개발자는 직접 사용할 필요 없고, 컴파일러가 자동으로 처리합니다.

위 코드에서 확인할 수 있듯이 Composer는 트리 내에서 모든 Composable 호출로 전달됩니다.

Compose Compiler는 Composable 함수가 오로지 다른 Composable 함수에서만 호출될 수 있게 합니다.

  • 그래야만 Composer가 항상 유효한 트리 구조를 유지할 수 있기 때문입니다.
  • 이 규칙이 없다면 Compose 트리가 깨지고, 재구성이 불가능해질 수 있습니다.

즉, Composer는 개발자가 작성하는 Composable 코드와 Compose Runtime 간의 중재자 역할을 합니다.

Composable 함수는 트리에 대한 변경 사항을 전달하고, 런타임 시에 트리의 형태를 빌드하거나 업데이트하기 위해 Composer를 사용합니다.


1.4 멱등성 (Idempotent)

특정 작업이나 연산을 여러 번 반복하더라도 결과가 처음 수행한 결과와 동일하게 유지되는 성질
Recomposition 최적화의 기반

Composable 함수는 생성하는 노드 트리에 대해 멱등성을 가져야 합니다. 동일한 입력 매개변수를 사용하여 Composable 함수를 여러 번 다시 실행하더라도 동일한 트리가 생성되어야 합니다.

Compose Runtime은 recomposition과 같은 작업을 위해 이러한 멱등성이 제공하는 가정에 의존합니다.

Recomposition

입력값이 변경될 때 마다 Composable 함수를 다시 실행하여 업데이트된 정보를 방출시키고 트리를 업데이트 하는 작업. Compose Runtime이 역할을 수행함

UI를 구성하는 데이터가 변경되었을 때,

  • 해당 데이터를 사용하는 Composable 함수만 다시 실행해서
  • UI 트리를 업데이트하는 작업입니다.

Compose는 이 과정에서 UI 전체를 다시 그리지 않고, 변경된 부분만 정확하게 다시 그립니다. 이걸 가능하게 하는 조건 중 하나가 바로 멱등성입니다.

Recomposition의 과정은 트리를 아래로 순회하면서 어떤 노드를 재구성 해야 하는지 확인합니다. 이 과정에서 입력값이 변경된 노드만 recomposition을 수행하고 나머지는 생략합니다. 특정 노드를 생략하는 것은 해당 노드를 대표하는 Composable 함수가 멱등성의 성질을 가질 때만 가능합니다. 그 이유는 런타임은 동일한 입력값을 제공할 경우 동일한 결과를 생성한다고 가정할 수 있기 때문입니다.

동일한 입력값에 대한 결과는 이미 메모리에 적재되어 있으므로 Compose는 다시 실행할 필요가 없고, 결과적으로 생략할 수 있습니다.


1.5 통제되지 않은 사이드 이펙트 방지

사이드 이펙트(side effect)

호출된 함수의 제어를 벗어나서 발생할 수 있는 예상치 못한 모든 동작을 의미합니다.

  • 로컬 캐시에서 데이터 읽기
  • 네트워크 요청
  • 전역 변수 설정
  • 파일 시스템 접근
  • SharedPreferences 수정
  • Toast 띄우기, Navigation 이동

이런 작업은 입력값에만 의존하지 않고, 외부 요인에 따라 달라질 수 있기 때문에 사이드 이펙트로 간주됩니다.

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 함수는 모든 입력값은 매개변수로서 받고, 결과를 생성하기 위해 주어진 입력값만을 사용합니다.

  • Composable 함수는 입력만 받도록 설계하세요.
  • 상태나 외부 의존성 없이, 주어진 값만으로 UI를 단순하게 그리는 역할만 합니다.
  • 이런 함수는 단순하고 높은 재사용성을 갖게 합니다.

사이드 이펙트가 필요한 순간

stateful(상태유지, 상태를 보유하는) 프로그램을 개발하기 위해서는 사이드 이펙트 가 필요하므로, 특정 단계에서는 우리가 사이트 이펙트를 실행해야만 합니다 (주로 Composable 트리의 루트에서 빈번하게 실행됩니다).

  • 네트워크 호출
  • DB 저장
  • 파일 접근
  • 상태 관찰 (예: LiveData, Flow 등)

이를 위해 Compose는 Effect Handlers (이펙트 핸들러)라는 안전한 도구를 제공합니다.

Effect Handlers

Composable 함수 내에서 아무런 제어를 받지 못하고 직접적으로 이펙트가 호출되는 것을 방지하는 역할 수행

이펙트 핸들러는 사이드 이펙트가 Composable의 라이프사이클을 인식하도록 하여, 해당 라이프사이클에 의해 제한되거나 실행될 수 있게 합니다.

Composable 노드가 트리를 떠날 때 자동으로 이펙트를 해제하거나 취소할 수 있게 하고, 이펙트에 주어진 입력값이 변경되면 재실행시키거나, 심지어 동일한 이펙트를 recomposition 과정에서 유지시키고 한 번만 호출되게 할 수 있습니다.

1.6 재시작 가능 (Restartable)

일반함수

일반 함수는 한 번 호출되면, 호출된 위치로부터 순차적으로 실행됩니다.
다른 함수를 호출할 수는 있지만, 재호출(re-run) 되지는 않습니다.

Composable 함수

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

Composable 함수는 관찰하는 상태(state)의 변화에 기반하여 반작용적으로 재실행되도록 설계되었습니다.

Compose Compiler는 일부 상태(state)를 읽는 모든 Composable 함수를 찾아 Compose Runtime에게 재시작하는 방법을 가르치는데 필요한 코드를 생성합니다.
상태를 읽지 않는 Composable은 재시작할 필요가 없으므로, Compose Runtime에게 해당 방법을 가르칠 이유가 없습니다.


1. 7 빠른 실행 (fast execution)

Composable 함수들은 UI를 구축하거나 반환하지 않습니다.

Composable에 대한 오해와 진실

  • 오해: Composable 함수는 UI를 반환한다.
  • 진실: Composable 함수는 UI의 설명(description) 을 메모리에 담는 노드 트리를 생성합니다

Composable은 단순히 인메모리 구조를 구축 및 업데이트하기 위한 데이터를 방출할 뿐입니다.
이는 Composable을 더욱 빠르게 만들며, 런타임이 아무 문제없이 해당 함수를 여러번 실행할 수 있도록 합니다.


1.8 위치 기억법 (Positional memoization)

메모이제이션(memoization)

함수가 입력값에 기반하여 결과를 캐싱하는 기법
즉, 입력값이 같으면 결과를 캐싱해서 다시 계산하지 않도록 하는 기법

순수 함수

항상 같은 입력에는 같은 출력을 주고, 외부 상태에 영향을 주지 않는 함수
메모이제이션이 가능한 함수 조건입니다.

Composable의 위치 기반 기억법

함수 메모이제이션에서, 함수 호출은 그 이름, 타입 및 매개변수 값의 조합을 통해 식별됩니다.
Compose의 경우는 추가적인 요소가 고려됩니다.

Composable 함수는 소스 코드 내 호출 위치에 대한 불변의 정보를 가지고 있습니다.
Compose Runtime은 동일한 함수가 동일한 매개변수 값으로 다른 위치에서 호출될 때, 동일한 Composable 부모 트리 내에서 고유한 다른 ID를 생성합니다.

@Composable
fun MyComposable() {
    Text("Hello") // 위치 A
    Text("Hello") // 위치 B
    Text("Hello") // 위치 C
}
  • 인메모리 트리는 해당 함수들을 세 개의 다른 인스턴스로 저장하게 되고, 각각은 고유한 정체성을 가지게 됩니다.
  • 여기서 Text("Hello")는 3번 호출되지만, 각 호출은 서로 다른 위치에 있기 때문에 Compose는 이것들을 세 개의 다른 UI 요소로 인식합니다.
  • 즉, 동일한 Composable이라도 호출 위치가 다르면 전혀 다른 것으로 간주합니다.

왜 위치 기반으로 기억하나요?

Compose는 매 프레임마다 UI를 다시 그리는 방식이 아니라, 필요한 부분만 재구성(recomposition)합니다.

  • 이때 어떤 Composable을 다시 그릴지 판단하기 위해 위치를 기억합니다.
  • 그래서 Compose는 위치 + 입력값의 조합을 기준으로 캐싱된 결과를 재사용할지 결정합니다.

반복문에서 문제가 생긴다? (중요)

종종 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와 메모이제이션

remember는 트리의 상태를 유지하는 인메모리 구조에서 값을 메모리에 읽고 쓰는 역할을 수행하는 Composable 함수입니다.

Compose는 매번 UI를 그릴 때마다 전체 함수를 실행하지만, 기존 결과를 재활용해서 불필요한 연산을 피합니다. 이를 위해 상태를 기억하고 있어야 하고, 이때 사용되는 것이 remember입니다.

@Composable
fun FilteredImage(path: String) {
    val filters = remember { computeFilters(path) }
    ImageWithFiltersApplied(filters)
}

@Composable
fun ImageWithFiltersApplied(filters: List<Filter>) {
		TODO()
}
  • remember는 컴포저블 함수 내부에서 호출된 정확한 위치를 기준으로 값을 기억합니다.
  • computeFilters(path)의 결과는 메모리에 해당 위치와 연결된 슬롯에 저장됩니다.
  • 다음번 recomposition에서도 해당 위치에 도달하면 캐싱된 값이 그대로 사용됩니다.
  • 이때 path 값이 바뀌지 않았다면 재계산은 생략됩니다.

Compose는 내부적으로 Slot Table이라는 구조를 이용합니다. 쉽게 말해:

  • 각 Composable 함수의 호출 위치는 슬롯(slot)이라는 단위로 기록됩니다.
  • 이 슬롯은 함수 호출 순서와 위치에 기반해 고유하게 관리됩니다.
  • remember는 해당 슬롯에 값을 저장하고 다음에 동일한 위치에 도달하면 이를 조회합니다.

remember의 작동 범위는 로컬(context-scoped)이다

Compose에서 메모이제이션은 애플리케이션 전체에 걸쳐서 적용되지 않습니다.
무언가가 메모리에 기록될 때, 메모이제이션을 호출하는 Composable의 컨텍스트 내에서만 수행됩니다.

  • remember의 값은 해당 Composable 함수 호출 컨텍스트 내에서만 유효합니다.
  • 전역적으로 캐싱되지 않으며, 재사용 범위도 슬롯 테이블 내의 위치로 한정됩니다.
  • 이 말은: 항상 remember는 그 위치의 값만 기억한다는 것을 의미합니다.

1.9 Suspend 함수와의 유사성

suspend 함수란

suspend 함수는 비동기 처리 또는 중단 가능한 작업을 Kotlin 코루틴에서 다루기 위해 사용됩니다.

  • 호출 컨텍스트(Continuation)를 컴파일러가 자동으로 추가합니다.
  • Continuation은 현재 작업을 멈췄다가, 나중에 다시 이어서 실행할 수 있도록 해줍니다.
  • 즉, 중단(suspension)과 재개(resumption)를 관리합니다.
suspend fun publishTweet(tweet: Tweet): Post

위의 코드는 Kotlin 컴파일러에 의해 아래와 같이 변경됩니다.

fun publishTweet(tweet: Tweet, continuation: Continuation<Post>): Any

Continuation은 콜백처럼 작동하는 객체이며, 실행이 어디까지 왔고 어떤 데이터를 가지고 있는지를 기억합니다.

즉, 다양한 중단점에서 실행을 일시 중단하고 재개하는 데 필요한 모든 정보를 담고 있습니다.

Composable 함수

재시작(restartable)이 가능하고, 상태(state) 등으로부터 반응할 수 있도록 만듭니다.

  • 실행을 중단(suspend)하는 것과는 다르게, 재시작(restart) 가능한 구조입니다.
  • Compose 컴파일러는 @Composable 함수에 숨겨진 Slot Table과 Remember 관리 컨텍스트를 자동으로 추가합니다.
  • 이 구조는 UI 트리를 인메모리에 저장하고, 상태가 바뀔 때 필요한 부분만 다시 실행(recompose)할 수 있도록 합니다.

왜 Compose는 suspend를 쓰지 않았을까?

  • suspend는 실행 흐름 제어에 집중된 개념이므로, UI 상태 재조합과는 맞지 않음
  • Compose는 UI 트리 상태를 메모리에 저장하고 최적화하는 데 초점을 맞춤
  • 슬롯 테이블, remember 시스템, recomposition 컨트롤 등 Compose 전용 런타임이 필요
  • suspend를 사용하면 오히려 Compose가 원하는 재시작 기반 구조를 표현하기 어렵고, 런타임 오버헤드가 발생할 수 있음

1.10 Composable 함수의 색깔

함수 컬러링 개념

이 개념은 동기(synchronous) 함수비동기(asynchronous) 함수가 서로 완전히 다른 특성을 가지며, 쉽게 섞이지 않는다는 사실에서 나왔습니다.

Bob Nystrom은 동기 함수는 파란색, 비동기 함수는 빨간색처럼 생각할 수 있다고 했습니다.

  • suspend 함수는 다른 suspend 함수에서만 호출 가능
  • @Composable 함수도 다른 @Composable 함수에서만 호출 가능

즉, 함수의 색이 다르면 섞을 수 없고, 중간에 통합 지점(entry point)이 필요하다는 의미입니다.

Kotlin의 suspend함수와 채색 함수

suspend fun fetchUser(): User
  • 이 함수는 시간이 걸릴 수 있는 네트워크 작업 등을 처리할 수 있습니다.
  • 하지만 이 함수는 다른 suspend 함수에서만 호출할 수 있습니다.
  • 일반 함수에서 이걸 쓰고 싶다면 launch나 runBlocking 같은 통합점이 필요합니다.

이 제한 덕분에 Kotlin은비동기 실행을 안전하고 명확하게 제어할 수 있습니다.

Jetpack Compose의 @Composable함수도 채색 함수

@Composable
fun Greeting(name: String) {
    Text("Hello, $name")
}
  • Composable 함수는 프로그램 로직을 작성하기 위해 설계된 것이 아니라 노드 트리의 변경사항을 기술하기 위한 것입니다.
  • 마찬가지로, 이 함수는 다른 @Composable 함수에서만 호출할 수 있습니다.
  • 일반 함수에서 호출하려면 setContent { ... } 같은 통합 진입점이 필요합니다.

inline 함수

inline 함수란 함수를 호출할 때, 실제 코드가 그 자리에 복사되는 함수입니다.

inline fun doSomething(action: () -> Unit) {
    println("Start")
    action()
    println("End")
}
컴파일러가 doSomething을 호출하면 
doSomething {
    println("Doing work")
}

요래 됩니다
println("Start")
println("Doing work")
println("End")

Composable 함수 안에서 forEach 같은 일반 함수를 사용할 수 있는 이유?

@Composable
fun SpeakerList(speakers: List<Speaker>) {
    Column {
        speakers.forEach {
            Speaker(it) // 어떻게 이게 되는지?
        }
    }
}

forEach는 사실 inline 함수이기 때문에, 이 안에서 Speaker(it) 같은 Composable 호출을 허용할 수 있습니다.

인라인 덕분에 람다 함수 안의 Composable 호출이SpeakerList 함수 본문에 복사되어 들어가기 때문에Composable → Composable 관계가 유지됩니다.

함수 컬러링은 정말 문제일까?

  • Composable과 일반 함수, 혹은 suspend 함수 사이를 넘나드는 작업이 번거롭고 헷갈릴 수 있습니다
  • 하지만 Compose나 Coroutine은 이를 통합 지점으로 명확히 나눔으로써 개발자와 컴파일러 모두에게 안전성과 명확성을 제공합니다.

1.11Composable 함수 타입

@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의 표준 함수와 다른 타입으로 간주되는 이유입니다.

왜 이렇게 타입이 다를까?

  1. @Composable 함수는 특별한 호출 환경을 필요로 합니다. (remember, recomposition, SlotTable 등)
  2. 그래서 컴파일러는 @Composable을 보고 특수한 호출 트랜스폼(transform)을 적용합니다.
  3. 따라서 @Composable () -> Unit은 () -> Unit과 다른 함수 타입입니다.
profile
오늘 하루도 화이팅

0개의 댓글