Composable 함수 이해하기: Jetpack Compose의 핵심 개념 정리
Composable 함수 이해하기: Jetpack Compose의 핵심 개념 정리
Jetpack Compose에서 @Composable 함수는 UI를 직접 그리는 함수가 아니다. 대신 화면이 어떤 구조와 상태를 가져야 하는지를 설명하는 역할을 한다. 이 설명들은 Compose 런타임에 의해 해석되어 메모리 안의 UI 트리, 즉 Composition을 구성하거나 업데이트하는 데 사용된다.
이번 글에서는 Composable 함수가 어떤 방식으로 동작하는지, 그리고 왜 특정 규칙들이 중요한지 런타임 관점에서 정리해본다.
Composable 함수의 본질: UI를 “그리는 것”이 아닌 “설명하는 것”
일반적인 Kotlin 함수는 입력값을 받아 결과를 반환한다. 반면 Composable 함수는 보통 값을 반환하지 않고(Unit), 함수 실행 과정에서 UI 구조에 대한 정보를 방출한다.
이 과정은 Compose에서 흔히 emit(방출) 이라고 부르며, “여기에는 텍스트가 필요하고, 그 아래에는 버튼이 있다”와 같은 UI 설계도를 제출하는 것에 가깝다. Compose 런타임은 이 설계도들을 모아 인메모리 UI 트리를 구성한다.
즉, Composable 함수의 유일한 목적은 Composition을 생성하거나 최신 상태로 유지하는 것이다.
@Composable 어노테이션이 하는 일
@Composable 어노테이션은 단순한 표식이 아니다. 이 어노테이션이 붙은 순간, 컴파일러는 해당 함수를 특별한 호출 규칙과 실행 방식을 가지는 함수로 변환한다.
대표적인 변화 중 하나는 컴파일 시점에 보이지 않는 매개변수인 Composer가 함수에 추가된다는 점이다. 이 Composer는 Composable 함수와 Compose 런타임 사이의 중재자 역할을 하며, 부모 Composable에서 자식 Composable로 계속 전달된다.
이 구조 때문에 Composable 함수는 오직 다른 Composable 함수 안에서만 호출될 수 있다. Composer가 일관되게 전달되어야 전체 UI 트리를 안정적으로 관리할 수 있기 때문이다.
런타임 최적화를 위한 중요한 계약들
Compose 런타임은 매우 공격적인 최적화를 수행한다. 하지만 이를 가능하게 하려면 개발자가 반드시 지켜야 할 규칙들이 있다.
멱등성(Idempotency)
Composable 함수는 같은 입력값으로 여러 번 실행되더라도 항상 동일한 UI 설명을 만들어야 한다. 이 성질을 멱등성이라고 한다.
Recomposition 과정에서 런타임은 “입력이 변하지 않았으면 결과도 같을 것”이라고 가정하고, 해당 함수의 실행 자체를 생략한다. 멱등성이 깨지면 이런 최적화는 불가능해진다.
통제되지 않은 Side Effect 금지
Composable 함수 내부에서 네트워크 요청, 데이터베이스 접근, 전역 상태 변경과 같은 부수 효과를 직접 실행하는 것은 위험하다. Composable은 언제, 얼마나 자주, 어떤 순서로 실행될지 예측할 수 없기 때문이다.
이 문제를 해결하기 위해 Compose는 LaunchedEffect, SideEffect와 같은 이펙트 핸들러를 제공한다. 이들은 Composable의 생명주기를 인식하고, 부수 효과가 필요한 시점에만 안전하게 실행되도록 돕는다.
Composable 함수 자체는 최대한 단순하게 UI 설명에만 집중하는 것이 이상적이다.
Composable 함수의 실행 특징
재시작 가능 (Restartable)
Composable 함수는 상태 변화에 따라 여러 번 다시 실행될 수 있다. Compose 컴파일러는 어떤 Composable이 상태를 읽는지 분석하고, 런타임은 이 정보를 기반으로 필요한 부분만 선택적으로 재실행한다.
이 덕분에 전체 UI를 다시 그리지 않고도 효율적인 업데이트가 가능하다.
빠른 실행 (Fast Execution)
Composable 함수는 무거운 UI 객체를 생성하지 않는다. 대신 가볍고 빠른 UI 구조 정보를 방출한다. 이 방식 덕분에 애니메이션처럼 매 프레임 UI가 변하는 상황에서도 성능 부담이 크지 않다.
단, Composable 내부에 비용이 큰 연산이나 IO 작업을 넣으면 이 장점은 쉽게 무너진다. 이런 작업은 코루틴이나 이펙트 핸들러로 분리해야 한다.
위치 기억법과 key의 중요성
Compose는 Composable의 결과를 기억할 때, 입력값뿐만 아니라 소스 코드 상 호출 위치도 함께 고려한다. 이를 위치 기억법(Positional Memoization)이라고 한다.
하지만 반복문 안에서 Composable을 나열하면 문제가 생길 수 있다. 이 경우 런타임은 호출 순서(index)에 의존해 각 항목을 식별하는데, 리스트 중간에 항목이 추가되면 이후 항목들이 불필요하게 재구성될 수 있다.
이를 방지하려면 key(id)를 사용해 각 항목에 안정적인 고유 식별자를 제공해야 한다. 그러면 순서가 바뀌어도 Compose는 각 항목의 정체성을 정확히 유지할 수 있다.
suspend 함수와의 유사성, 그리고 함수 컬러링
Composable 함수는 Kotlin의 suspend 함수와 구조적으로 닮아 있다. 두 함수 모두 컴파일러가 숨은 매개변수를 추가하고, 특정 컨텍스트에서만 호출할 수 있도록 제한한다.
이런 특성은 흔히 함수 컬러링(function coloring) 이라고 불린다. 서로 다른 색의 함수는 직접 섞을 수 없고, setContent 같은 명확한 진입점을 통해서만 연결된다.
흥미롭게도 forEach 같은 inline 함수는 람다 코드가 호출 지점에 그대로 펼쳐지기 때문에, Composable 컨텍스트 안에서 자연스럽게 사용할 수 있다.
댓글
댓글 쓰기