compose-rust 가이드

UI 작성하기

composable, 상태, 이벤트 계획

이 페이지는 2026-10-20의 compose-rust 1.0.0에 들어오는 작성 API를 설명합니다. Compose를 따릅니다. composable은 함수이고, 상태는 remember 에 두며, 상태가 바뀌면 그것을 읽은 코드만 다시 실행됩니다.

계획이며, 아직 크레이트에 없습니다

이 페이지의 코드는 0.0.x에서 컴파일되지 않습니다. 의도한 모양을 보이는 것이고, 이름도 확정되지 않았습니다. 작업은 #23 이 추적합니다. 다만 이벤트, 텍스트 입력, UI 로컬 상태에 관한 규칙은 지금도 지켜집니다. 렌더러와 경계가 그것을 강제하기 때문입니다.

composable은 함수입니다

함수에 #[composable] 을 붙이고 그 안에서 위젯을 부릅니다. 위젯도 함수이고 이름은 Compose의 것입니다. Column, Text, Button 이 그 예입니다. 자식을 갖는 위젯은 자식을 클로저로 받습니다.

rust, 계획한 모양(0.0.x에서 컴파일되지 않음)
#[composable]
fn counter() {
    let count = remember(|| mutable_state_of(0));
    Column(Modifier::new().fill_max_width(), || {
        Text(format!("{}", count.get()));
        Button(|| count.set(count.get() + 1), || Text("+1"));
    });
}

fn main() {
    compose_rust::launch(counter);
}

#[composable] 은 런타임이 호출 자리를 서로 구별하는 데 쓰는 그룹을 넣습니다. 그룹이나 키를 손으로 쓰지 않습니다. 그룹은 if, match, 반복문, 조기 return, continue, break, ?, 되감기를 지나도 맞게 닫힙니다. 그룹을 사용자가 아니라 매크로가 갖기 때문입니다.

위젯 어휘는 스키마의 것이고, 위젯 레퍼런스에 있습니다. 작성 API는 자기 위젯이나 속성을 따로 정의하지 않으므로, 쓸 수 있는 것과 렌더러가 그릴 수 있는 것이 같은 목록입니다.

상태와 recomposition

상태 객체는 자신을 읽은 스코프를 기록합니다. 상태에 쓰면 정확히 그 스코프들이 무효가 되고, 다음 프레임에 다시 실행됩니다. 바뀐 것을 찾는 방법은 이것이 전부이며, 트리 비교는 없습니다.

위치 기반 기억과 키

remember 는 호출 자리, 곧 그룹 경로와 그 안의 순서에 묶입니다. 분기가 사라졌다가 다시 나타나거나 반복이 늘고 줄어도, 각 호출 자리는 자기 값을 유지하고 남의 값을 받지 않습니다.

반복문에서는 순서가 맞는 정체성이 아닐 때가 있습니다. 항목의 순서가 바뀔 수 있다면 Compose의 key 처럼 항목마다 키를 줍니다. 키가 있는 항목이 자리를 바꾸면 지우고 다시 만드는 대신 이동으로 나가고, 그 항목이 기억한 값도 따라갑니다.

effect

effect는 Compose의 SideEffect, LaunchedEffect, DisposableEffect 를 따르는 세 가지입니다.

이벤트 핸들러 안의 상태 쓰기는 recomposition을 부릅니다. recomposition 도중의 상태 쓰기는 다음 프레임으로 미뤄지므로, composition이 자기 자신을 먹이지 않습니다.

이벤트 핸들러

핸들러는 위 Button 의 첫 인자처럼 평범한 Rust 클로저입니다. 핸들러는 동기로 실행됩니다. 렌더러가 자기 UI 스레드에서 Host를 부르고, 핸들러가 실행되고, 런타임이 recomposition을 하고, 바뀐 것의 배치가 같은 호출로 돌아갑니다. 큐에 쌓이는 것은 없습니다.

핸들러는 UI 스레드에서 돌므로 가벼워야 합니다. PTY 읽기, 네트워크 호출, 파일 I/O처럼 멈추는 일은 워커 스레드의 몫입니다. 워커는 같은 상태 객체에 쓰고, 프레임은 Host가 스스로 요청합니다. 사용자 코드는 경계 함수를 부르지 않습니다.

키 소비: Enter와 Shift+Enter

채팅 입력창은 Enter 로 보내고 Shift+Enter 로 줄을 바꿔야 합니다. 렌더러는 그 키가 처리되었는지를 같은 호출 안에서 알아야 합니다. 웹의 preventDefault(), Compose에서 키 이벤트를 소비하는 것과 같은 생각입니다.

경계는 지금도 이것을 싣습니다. 키 핸들러의 결과가 호출의 반환값으로 렌더러에 돌아가고, 렌더러는 그것을 Compose에 처리됨으로 알립니다. 작성 API는 이것을, 키를 소비했는지 말하는 키 핸들러로 드러냅니다.

IME 조합 중의 Enter

IME가 조합하는 동안 렌더러는 Host에 키 이벤트를 아예 보내지 않습니다. 조합 중에 누른 Enter 는 "보내기"가 아니라 "이 음절 확정"이라는 뜻입니다. 이 규칙이 깨지면 한글을 치다 Enter 를 눌렀을 때 조합 중인 음절이 빠진 채 메시지가 전송됩니다. 핸들러는 그런 키를 보지 않으므로, "Shift 없는 Enter"라는 단순한 조건이 맞습니다.

비제어 TextField

TextField 에는 값 속성이 없습니다. 편집 버퍼, 선택 영역, 조합 상태는 렌더러의 것입니다. Rust는 이벤트로 변화를 압니다. 디바운스된 현재 텍스트 알림, 제출된 값, 포커스를 잃은 사실입니다.

이유는 한글입니다. 한글 음절은 치는 동안 자모로 조립됩니다. 조합 중인 텍스트가 Host를 왕복하면, 키를 칠 때마다 텍스트가 렌더러를 떠났다 돌아오면서 조합이 초기화되고 음절이 부서집니다. 일본어와 중국어 입력도 같습니다. 버퍼를 Kotlin에 두면 이 실패는 드문 일이 아니라 일어날 수 없는 일이 됩니다.

Host가 텍스트를 바꿔야 할 때, 예를 들어 보낸 뒤 입력창을 비울 때는 텍스트와 선택 영역을 담은 SetText 레코드를 명시적으로 보냅니다. 렌더러는 진행 중인 조합이 끝난 뒤에 이것을 적용합니다. 0.0.1에서는 Host::set_text 입니다.

애니메이션

작성 API는 렌더러가 이미 재생하는 애니메이션 레코드를 Compose의 이름으로 씁니다.

rust, 계획한 모양(0.0.x에서 컴파일되지 않음)
#[composable]
fn banner(visible: bool) {
    let alpha = animate_float_as_state(if visible { 1.0 } else { 0.0 });
    Box(Modifier::new().graphics_layer(|layer| layer.alpha = alpha), || {
        Text("Saved");
    });
}

애니메이션이 재생되는 동안 Host는 recomposition을 한 번도 하지 않습니다. 중간값은 Compose 프레임 시계 위에서 렌더러에 있고, Host는 보지 않습니다. 길이와 곡선을 주지 않으면 디자인 시스템의 MotionRole::Standard 로 재생되므로 움직임이 플랫폼을 따릅니다. 길이와 곡선을 직접 주는 것은 탈출구입니다.

상태가 사는 곳

애플리케이션 상태는 Rust에 있는 사용자의 것입니다. UI 로컬 상태는 Kotlin에 있는 렌더러의 것이고, recomposition 런타임이 생겨도 Rust로 옮겨 오지 않습니다. 이것은 지금도 그렇습니다.

상태주인이유
애플리케이션과 도메인 상태Rust사용자의 프로그램입니다.
편집 중인 텍스트, IME 조합Kotlin조합은 왕복하면 안 됩니다(위 참고).
스크롤 위치KotlinRust가 가지면 한 프레임 늦습니다.
포커스Kotlin포커스는 이미 플랫폼이 갖고 있습니다.
애니메이션 진행KotlinCompose 프레임 시계 위에서 돕니다.
다이얼로그, 메뉴, 드롭다운, 시트가 열려 있는지Kotlin보낸 값은 시작값이고, 열리고 닫히는 동작은 렌더러가 가지므로 애니메이션이 왕복하지 않습니다.
슬라이더를 끄는 동안 지나는 위치Kotlin손가락을 따라가는 데 경계 호출이 들면 안 됩니다.
앱 확대 수준Kotlin렌더러가 앱마다 저장합니다. 아키텍처를 보십시오.