Rust로 쓰는 네이티브 UI
UI는 Dioxus로 쓰고, 그리는 일은 Compose에게 맡깁니다.
dioxus-compose는 Rust 코드가 rsx!와 컴포넌트, 훅으로 화면을 기술하고,
미리 컴파일해 둔 Compose Multiplatform 렌더러가 그것을 그리는 GUI 스택입니다.
웹뷰를 품지 않고, JVM을 함께 배포하지도 않습니다.
왜 만드는가
이 스택이 겨냥하는 것은 하루 종일 켜 두는 애플리케이션입니다. 늘 켜 두는 도구에서는 무게가
매시간 체감됩니다. 그런데 웹 기반 셸은 가볍지 않습니다. 웹뷰는 브라우저 엔진을 통째로
데려오고, JRE는 jlink로 줄여도 80~120 MB가 붙습니다.
그렇다고 순수 Rust UI 프레임워크로 가면, 텍스트를 다루는 앱에서 가장 중요한 부분인 텍스트 자체에서 막힙니다. 한국어·일본어·중국어 입력은 플랫폼 IME를 거치고, 90%만 맞는 IME는 결국 쓸 수 없는 앱입니다. Compose Multiplatform은 텍스트와 IME, 위젯 품질이 이미 성숙해 있습니다. 그래서 이 프로젝트는 딱 그 부분만 빌려 옵니다. 애플리케이션 코드는 Rust에 남고, Compose는 Rust가 보내는 UI 트리를 해석하는 인터프리터가 됩니다.
| 협상 불가 조건 | 그래서 생기는 결과 |
|---|---|
| 웹뷰 금지 | WKWebView, WebView2, WebKitGTK를 쓰지 않습니다. Tauri와 wry도 마찬가지입니다. |
| JVM 동봉 금지 | 데스크톱은 GraalVM native-image 공유 라이브러리로 배포하고, iOS는 Kotlin/Native로 만들 계획입니다. |
| 수동 JNI·cinterop 금지 | 경계 심(shim)은 전부 Rust 스키마에서 생성합니다. |
| UI 코드는 Rust에 | Kotlin은 렌더러의 구현 세부일 뿐, 기능을 작성하는 자리가 아닙니다. |
| Compose 수준의 텍스트·IME | Compose의 플랫폼 텍스트 입력을 우회하는 경로를 만들지 않습니다. |
코드는 이렇게 생겼습니다
저장소의 desktop_demo 예제를 줄인 것입니다. 위젯 이름은 Compose 그대로이고,
속성은 그것을 snake_case로 옮긴 Rust 표기입니다.
use dioxus_compose::prelude::*;
fn app() -> Element {
let mut messages = use_signal(Vec::<String>::new);
let mut draft = use_signal(String::new);
rsx! {
Column {
fill_max_width: true,
Text { text: "dioxus-compose chat" }
for message in messages() {
Text { text: message }
}
TextField {
placeholder: "Write a message",
multiline: true,
on_value_change: move |value| draft.set(value),
on_key_down: move |event: KeyEvent| {
if event.key() == Key::Enter && !event.shift_key() {
// … draft를 messages에 넣습니다 …
event.consume();
}
}
}
}
}
}
fn main() {
dioxus_compose::launch(app);
}
현재 상태
로드맵을 기능 목록처럼 포장하지 않고 있는 그대로 적습니다. 지금 끝에서 끝까지 도는 것은 macOS 데스크톱뿐입니다. 계획으로 표시한 항목은 SPEC.md에 설계만 되어 있고 구현은 없습니다.
| 플랫폼 | 상태 | 방식 |
|---|---|---|
| macOS 데스크톱 (arm64) | 동작 | Liberica NIK 25 Full로 native-image --shared 빌드한 렌더러를 Rust 바이너리가 링크합니다. 2026-09-20 확인. |
| Windows / Linux 데스크톱 | 계획 | 같은 native-image 경로입니다. 빌드 스크립트는 아직 macOS 외에서는 바로 멈춥니다. |
| iOS | 계획 | Kotlin/Native -produce static으로 같은 C 심볼을 내보냅니다. |
| Android | 계획 | Kotlin이 프로세스와 루프를 소유하고, Rust cdylib을 생성된 JNI 심으로 호출합니다. |
| Web (wasm) | 계획 | Kotlin이 wasm linear memory를 소유하고 Rust가 그것을 import합니다. 배치 arena를 제자리에서 공유하므로 복사가 없고, 경계 호출은 코드젠으로 만든 JS forwarder를 거칩니다(실측 약 12 ns). |
기능별 상태
| 기능 | 상태 | 메모 |
|---|---|---|
위젯: Column, Row, Box, Text, TextField, Button, Spacer, LazyColumn | 동작 | 스키마 전체입니다. 이 밖의 것은 스키마부터 넓혀야 합니다. |
| 이벤트와 동기 소비 | 동작 | on_click, on_value_change, on_submit, on_focus_lost, on_key_down과 event.consume(). |
비제어 TextField | 동작 | 편집 값과 조합 상태를 렌더러가 소유합니다. |
| Rust 스키마 → Kotlin 프로토콜 코드젠 | 동작 | 생성물이 낡으면 테스트가 실패합니다. |
fill_max_width·fill_max_height 외의 Modifier | 일부 | 패딩, 크기, 배경, clickable은 프로토콜에는 있지만 rsx! 속성이 아직 없습니다. |
LazyColumn 윈도잉 (FR-8) | Host 쪽 | 컴포넌트·프로토콜·Host 테스트는 통과합니다. Kotlin 인터프리터는 아직 Column으로 그리고 RangeRequested를 보내지 않습니다. |
AppendText 스트리밍 (FR-9) | Host 쪽 | Host가 프레임당 한 건으로 모아 flush합니다. rsx! 수준 API는 아직 없고 Host::append_text는 프로토콜 node id를 받습니다. |
| 디자인 시스템과 테마 (FR-13, FR-14) | 계획 | 명세는 와이어 태그까지 Agreed지만 구현된 것은 없습니다. 크레이트에 Paint, TypeRole, DesignSystem, SetTheme 모두 아직 없습니다. 디자인 시스템을 보세요. |