Jetpack Compose Internals를 참고하여 작성하였습니다
Compose Runtime vs Compose UI
Compose Runtime
- Compose의 기본적인 기계 장치와 제공
- 상태 관리와 composition 관련 원시 요소(primitives)를 제공
- UI에 제한되지 않고 다양한 트리 구조를 관리할 수 있음
- Kotlin이 실행되는 모든 플랫폼에서 사용 가능
- Android나 UI에 직접적인 의존성이 거의 없음
Compose UI
- 새로운 안드로이드 UI 툴킷, 캔버스 위에 LayoutNode 트리가 표현하는 내용을 그림
- 안드로이드 UI 개발에 특화됨
멀티플랫폼 지원
Compose Runtime은 UI 외에 다양한 트리 구조를 만드는데 사용할 수 있다
- Compose for Desktop
- Android 구현과 매우 유사하게 유지
- 포팅된 Skia 래퍼를 통해 Compose UI의 전체 렌더링 계층 재사용
- 마우스/키보드 지원을 위해 이벤트 시스템 확장
- Compose for iOS
- 렌더링 계층으로 Skia 사용
- JVM에서 Kotlin/Native로 이식 가능한 기존 로직의 상당 부분 재사용
- Compose for Web
- 브라우저 DOM에 의존하여 요소 표시
- 컴파일러와 런타임만 재사용
- HTML/CSS 위에 구성요소가 정의됨
- 결과적으로 Compose UI와는 상당히 다른 시스템 구축
런타임과 컴파일러는 베이스 플랫폼이 달라도 거의 동일한 방식으로 사용된다
Composition 재소개 (Re-Introducing composition)
- Recomposer는 Composition을 구동하고 관련된 상태가 변경될 때마다 recomposition 발생
- Composition은 모든 Composable 함수에 대한 Context 제공
- Composition은 SlotTable 기반의 캐시를 제공하고 Applier를 통해 트리를 생성하는 인터페이스 제공
Composition을 직접적으로 생성하는 방법
/**
* applier -> Composition에 의해 생성된 트리를 생성하고 연결
* parent -> 부모 Context로 모든 Composable 함수 내에서 사용 가능 (일반적으로 rememberCompositionContext())
*/
fun Composition(
applier: Applier<*>,
parent: CompositionContext
): Composition = CompositionImpl(parent, applier)
벡터 그래픽의 구성 (Composition of vector graphics)
Compose에서 벡터 렌더링은 전통적인 Android 시스템의 Drawable과 유사한 Painter 추상화를 통해 구현
벡터에 특화된 Node를 생성하고 벡터 트리에서 조합되어 나중에 Canvas에 그려지게 된다
// Image는 LayoutNode 사용
Image(
painter = rememberVectorPainter { width, height ->
Group(
scaleX = 0.75f,
scaleY = 0.75f
) {
val pathData = PathData { ... }
Path(pathData = pathData)
}
}
)
@Composable
@VectorComposable
fun Group(
...
clipPathData: List<PathNode> = EmptyPath,
content: @Composable @VectorComposable () -> Unit
) {
ComposeNode<GroupComponent, VectorApplier>(
factory = { GroupComponent() },
...
) {
content()
}
// PathNode 사용
fun Path(
pathData: List<PathNode>,
...
) {
ComposeNode<PathComponent, VectorApplier>( ... ) { ... }
}
- Group, Path는 일반적으로 사용하는 UI와 다른 Composition에 존재 (VectorPainter에 포함)
벡터 이미지 트리 구축 (Building vector image tree)
벡터 이미지는 벡터 그래픽의 요구사항에 더 잘 맞게 LayoutNode보다 간단한 요소로 생성
sealed class VNode {
...
// 노드와 그 자식의 내용을 그리는 방법 제공
abstract fun DrawScope.draw()
}
internal class VectorComponent(
val root: GroupComponent
) : VNode() {
...
override fun DrawScope.draw() {
// set up viewport size and cache drawing
draw(1.0f, null)
}
...
}
// 자식이 없는 리프 노드로 pathData를 그린다
internal class PathComponent : VNode() {
...
override fun DrawScope.draw() {
// draw path
}
...
}
// 자식을 조합하고 공유 변형을 적용
internal class GroupComponent : VNode() {
...
override fun DrawScope.draw() {
// draw children with transform
}
...
}
자식 노드를 부모에 연결하기 위해 VNode는 VectorApplier를 통해 결합
class VectorApplier(root: VNode) : AbstractApplier<VNode>(root) {
override fun insertTopDown(index: Int, instance: VNode) {
// Ignored as the tree is built bottom-up.
}
override fun insertBottomUp(index: Int, instance: VNode) {
current.asGroup().insertAt(index, instance)
}
override fun remove(index: Int, count: Int) {
current.asGroup().remove(index, count)
}
override fun onClear() {
root.asGroup().let { it.remove(0, it.numChildren) }
}
override fun move(from: Int, to: Int, count: Int) {
current.asGroup().move(from, to, count)
}
private fun VNode.asGroup(): GroupComponent {
return when (this) {
is GroupComponent -> this
else -> error("Cannot only insert VNode into Group")
}
}
}
Applier 인터페이스 대부분의 메소드는 리스트 연산(insert/move/remove)을 한다
이를 반복해서 재구현하는 것을 방지하기 위해 AbstractApplier는 MutableList에 대한 확장을 제공
VectorApplier는 GroupComponent에서 이러한 연산이 구현되어 있다
Compose를 이용한 DOM 관리 (Managing DOM with Compose)
Compose 멀티플랫폼은 아직 새로운 개념으로, JVM 생태계 외부에서는 런타임과 컴파일러만 사용 가능하다
하지만, 이 두 모듈만으로도 composition을 만들고 그 안에서 작업을 수행할 수 있어 다양한 실험이 가능하다
Google의 Compose 컴파일러는 모든 Kotlin 플랫폼을 지원하지만, 런타임은 Android 전용으로 배포된다
JetBrains는 JS를 포함한 멀티플랫폼 아티팩트로 자체 버전의 Compose를 출시
브라우저와 DOM
- 브라우저는 이미 HTML/CSS 기반의 뷰 시스템을 갖추고 있음
- Kotlin/JS 표준 라이브러리를 통해 DOM(Document Object Model) API 조작 가능
예제) 브라우저 내 HTML 표현
<div>
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
</div>
(3개의 항목을 가진 순서 없는 리스트 표시)
DOM 구조와 요소
- 트리 형태의 구조, Kotlin/JS에서 org.w3c.dom.Node로 표현
- 주요 요소
- HTML 요소 -> div, li 같은 태그
- Text -> 태그 사이의 텍스트
DOM 요소들을 사용한 JS의 트리 표현
Compose와 DOM
- DOM 요소들은 벡터 이미지 composition에서 사용된 VNode와 유사
- Compose로 관리되는 트리를 구현하기 위한 기반으로 사용
예시) Compose를 사용하여 DOM을 관리하는 코드
// tag -> 태그 이름
// content -> 내부 콘텐츠를 정의할 함수
@Composable
fun Tag(tag: String, content: @Composable () -> Unit) {
// DOM 요소 생성
ComposeNode<HTMLElement, DomApplier>(
// 지정된 태그 이름의 HTML 요소 생성
factory = { document.createElement(tag) as HTMLElement },
update = {},
content = content // 자식 요소가 추가됨
)
}
// value -> 텍스트 값
@Composable
fun Text(value: String) {
// 재사용 가능한 텍스트 노드 생성
ReusableComposeNode<Text, DomApplier>(
factory = { document.createTextElement(””) },
update = {
set(value) { this.data = it }
}
)
}
- 태그 변경 불가능
- 태그 이름 변경 시 재생성 되어야 함
- Compose는 자동으로 처리하지 않아 같은 Composable에 다른 태그 이름을 전달하는 것을 피해야 함
- Text 요소 재사용
- Text 요소는 구조적으로 동일하므로 ReusableComposeNode로 표시
- 다른 그룹 내에서도 노드 인스턴스를 재사용
- 정확성을 위해 텍스트 노드는 빈 내용으로 생성되고, 값은 update 매개변수로 설정
- Applier
- DOM 요소를 조작하기 위해 Compose는 DomApplier 인스턴스 필요
브라우저에서의 독립 구성 (Standalone composition in the browser)
브라우저에서 Compose를 사용하기 위해서 활성화된 composition이 필요
Android의 ComposeView와 달리, 브라우저 환경에서는 초기화를 직접 구현해야 한다
renderComposable
(composition 시작과 관련된 모든 구현 세부 사항을 숨기고
DOM 요소에 composable 요소를 렌더링할 수 있는 방법 제공)
fun renderComposable(root: HTMLElement, content: @Composable () -> Unit) {
GlobalSnapshotManager.ensureStarted()
val recomposerContext = DefaultMonotonicFrameClock + Dispatchers.Main
val recomposer = Recomposer(recomposerContext)
val composition = ControlledComposition(
applier = DomApplier(root),
parent = recomposer
)
composition.setContent(content)
CoroutineScope(recomposerContext).launch(start = UNDISPATCHED) {
recomposer.runRecomposeAndApplyChanges()
}
}
- 스냅샷 시스템 초기화
- GlobalSnapshotManager.ensureStarted 함수로 상태 업데이트 시스템 초기화
- 코루틴 Context 설정
- 브라우저에 맞는 Clock과 Dispatcher 설정
- Composition 생성
- DomApplier를 사용하여 DOM 조작 연결
- Content 설정
- 제공된 composable 함수로 composition content 설정
- Recomposition 과정 시작
- 코루틴을 시작하고 Recomposer.runRecomposeAndApplyChanges를 호출해 recomposition 시작
위 기본 요소들(Primitives)을 조합해 HTML 페이지 내용을 렌더링 할 수 있다
예시) HTML 페이지에 간단한 버튼을 렌더링
fun main() {
renderComposable(document.body!!) {
// <button>Click me!</button>와 동일
Tag("button") {
Text("Click me!")
}
}
}
예시) 상호작용 추가
(정적 콘텐츠만으로는 Compose의 강점을 활용할 수 없으므로, 상호작용을 추가)
@Composable
fun Tag(
tag: String,
// 클릭 이벤트 발생 시 호출될 콜백
onClick: () -> Unit = {},
content: @Composable () -> Unit
) {
ComposeNode<HTMLElement, DomApplier>(
factory = { createTagElement(tag) },
update = {
// 리스너가 변경되면 DOM 노드의 리스너도 재설정
set(onClick) {
this.onclick = { _ -> onClick() }
}
},
content = content
)
}
- 각 태그는 람다 매개변수로 클릭 리스너를 정의
- 모든 HTMLElement에 정의된 onclick 속성을 사용해 DOM 노드로 전파
예시) 상태를 관리하는 간단한 카운터
fun main() {
renderComposable(document.body!!) {
// 클릭 시 업데이트되는 카운터 상태
var counterState by remember { mutableStateOf(0) }
Tag("h1") {
Text("Counter value: $counterState")
}
Tag("button", onClick = { counterState++ }) {
Text("Increment!")
}
}
}
'Compose' 카테고리의 다른 글
[ComposeInternals] 이펙트와 이펙트 핸들러 (Effects and Effect handlers) (0) | 2025.04.23 |
---|---|
[ComposeInternals] 상태 스냅샷 시스템 (State snapshot system) (0) | 2025.04.20 |
[ComposeInternals] 컴포즈 UI (Compose UI) - (4) Modifier (0) | 2025.04.13 |
[ComposeInternals] 컴포즈 UI (Compose UI) - (3) Layout Constraints (제약 조건) (0) | 2025.04.13 |
[ComposeInternals] 컴포즈 UI (Compose UI) - (2) Measure(측정) (0) | 2025.04.04 |