Rust TUI 개발에서의 Crossterm와 Ratatui의 경계

위 이미지는 Neovim에서 crossterm의 Command trait의 Implementation을 조회할 경우 보이는 lsp preview이다.

Rust TUI 개발에서의 Crossterm와 Ratatui의 경계

Rust로 TUI(Terminal User Interface) 프로그램을 만들 때 가장 자주 추천받는 조합은 Ratatui + Crossterm이다.
하지만 실제로 개발을 시작하면, “어디까지가 Crossterm의 역할이고, 어디서부터 Ratatui가 하는 일인가?”가 명확하지 않다.

이 글은 그 경계를 코드 수준에서 탐색한 기록이다.


Ratatui와 Crossterm의 역할

요약하면 다음과 같다.

영역역할
Crossterm터미널 제어를 위한 추상화 계층. OS별 Escape Code를 감싸고, 커서 이동·색상·입력 이벤트 등 저수준 기능 제공
RatatuiCrossterm 위에서 동작하는 고수준 UI 프레임워크. 위젯, 레이아웃, 렌더링을 제공

즉, Crossterm은 터미널과의 직접 통신 담당,
Ratatui는 그 위에서 UI 요소를 배치하고 그리기 쉽게 만들어주는 레이어라고 한다.

감으로는 위와 같이 알고 있어도, 실무에서는 정확히 어떤 부분에서 Ratatui를 사용해야 할지, 어떤 부분이 Crossterm의 영역인지 헷갈렸다.

그래서 하루를 투자해서 crossterm crate를 받아서 cargo run --example 들의 소스들을 직접 보고 난 후에야, 그 경계를 명확히 이해할 수 있었다.

뿐만 아니라, crossterm에서 가장 중요한 struct인 Command trait에 대해 그 대략 43가지의 구현체들을 보고 나니, crossterm 의 역할을 이해할 수 있었다.


불친절(?)한 공식 문서

crossterm은

https://www.youtube.com/watch?v=vc5UPu76XOw

Tsoding 의 영상을 봐도 알 수 있듯이 그렇게 문서화가 잘 되어 있지는 않은 편이다.

Ratatui, Tokio 등과 같은 다른 유명한 러스트 라이브러리들이 따로 튜토리얼과 문서 사이트가 있는 것을 생각할 때, crossterm은 불친절하다.

하지만 덕분에 그런 친절한 문서 사이트 없이도, docs.rs 문서만으로도 crate와 친숙해지는 방법을 체득했다.


Crossterm의 구조 이해

Crossterm을 이해하려면 Command 트레이트와 execute!(), queue!() 매크로를 알아야 한다.

execute!(stdout, MoveTo(0, 0), Print("Hello"))?;
queue!(stdout, MoveTo(0, 0))?;
stdout.flush()?;
  • execute! : 명령을 즉시 실행 (버퍼에 기록 후 flush)
  • queue! : 명령을 버퍼에 쌓아두고 나중에 flush

이 매크로들은 내부적으로 QueueableCommand::queue()를 호출하며,
여기에 들어가는 인자들이 바로 Command 트레이트의 구현체들이다.


Command 트레이트 구현체들

Crossterm의 핵심 빌딩 블록은 약 40여 개의 Command 구현체다.

각 구현체는 특정 터미널 제어 명령에 대응하는 Escape Sequence를 버퍼에 작성한다. 직접 세어보지는 않았지만,

이렇게 crossterm의 소스코드에 전체 grep을 해보니 43개의 케이스가 나왔다.

예를 들어:

  • MoveTo, Hide, Show 등은 커서 제어
  • SetForegroundColor, Print 등은 출력과 색상
  • EnableMouseCapture, DisableMouseCapture 등은 마우스 입력 제어
  • EnterAlternateScreen, LeaveAlternateScreen은 대체 화면 전환

이러한 구현체 덕분에 개발자는 Escape 코드를 직접 작성하지 않아도 된다.
Crossterm은 이를 OS 독립적으로 처리해준다.

그런데 뭔가 허전하지 않나요?

여기까지만 crossterm 이 해주는 것이라면 crossterm은 단순 escape code mapping 라이브러리라고 할 수 있다. 그러나 여기서 끝나지 않는다. 그게 crossterm을 사용하면서 가장 헷갈린 부분이다.


Extra Features

crossterm은 그 밖에 events, windows와 같은 부가적인 기능 또한 제공하는 것이다. 이들은 Terminal Escape Code와 관련이 없지만, TUI 프로그램을 만들기 위해서 꼭 필요한 기능 중 하나이니 포함되어 있는 것이다.


Event 모듈

https://docs.rs/crossterm/latest/crossterm/event/index.html

Crossterm은 단순히 화면 제어만 하는 것이 아니다.
event 모듈을 통해 키보드, 마우스, 윈도우 이벤트도 지원한다.

주요 함수:

crossterm::event::poll(timeout)?;
crossterm::event::read()?;
  • poll() : 지정된 시간 내 입력 대기. 타임아웃 가능
  • read() : 블로킹 방식으로 이벤트 읽기

사용할 수 있는 이 두 함수들의 차이를 살펴보면 좋다.

나는 이해가 가질 않아 소스코드도 살펴보았는데, crossterm 소스코드의 examples 디렉토리의

  • event-poll-read
  • event-read

두 cargo examples 들의 차이를 찾아보면 좋다.
cargo run --example <이름> 으로 실행할 수 있다.

전자는 poll() + read()를 통해 타임아웃을 걸 수 있고,
후자는 read() 만을 사용한다.

둘 다 블로킹이지만, 전자의 read()는 poll()과 함께 사용할 경우 논리적으로는 논블로킹인 것이 보장되어 있다고 한다.
자세한 내용은 모듈 공식 문서에도 적혀 있다.
event-poll-read 의 간단한 소스코드를 보는 것이 큰 도움이 될 것이다.


(misc.) Ratatui의 역할

Ratatui는 Crossterm의 저수준 기능을 직접 다루지 않는다.
대신 다음과 같은 고수준 기능을 제공한다.

  • Widget: Paragraph, Table, Block 등 재사용 가능한 UI 요소
  • Layout: 터미널 크기에 따른 동적 분할
  • Rendering: 버퍼 기반의 최소 업데이트

즉, Ratatui는 “무엇을 보여줄지”를 다루고,
Crossterm은 “어떻게 터미널에 표현할지”를 담당한다.


Crossterm의 한계와 확장

poll, read 등의 블로킹 함수들만 있는 것을 보고 깨달은 점이 있다.

Crossterm 자체는 epoll, io_uring 같은 커널 수준 I/O를 직접 지원하지 않는다.
비동기 이벤트 처리를 원한다면 tokio, async-std, mio 같은 런타임과 함께 사용해야 한다.
이 조합을 통해 반응성 높은 TUI를 구성할 수 있다.


정리하며

Crossterm은 터미널 제어의 저수준 빌딩 블록이고,
Ratatui는 그 위의 UI 계층이다.

구분CrosstermRatatui
주 기능터미널 제어, 이벤트 처리위젯, 레이아웃, 렌더링
목적Escape 코드 추상화시각적 구성요소 관리
수준저수준고수준
비동기 처리외부 런타임 필요내부적으로 비동기 아님

UI 구성의 복잡도가 높아질수록 Ratatui와의 조합이 실용적이겠지만,
Crossterm만으로도 완전한 TUI를 만들 수 있다!

crossterm crate의 interactive-demo 를 직접 실행해보면, crossterm만으로도 간단하게 만든 예시 데모도 매우 실용적이고 멋질 수 있음을 알게 된다.


그 밖에 배운 점

  • 문서가 친절하지 않은 크레이트라도,
    docs.rs 문서와 examples 디렉토리만으로 충분히 구조를 파악할 수 있다.
  • Rust 소스코드는 trait 중심으로 명확하게 구성되어 있어,
    실제 구현을 탐색해보는 것이 학습 효율이 높다.

0개의 댓글