현대 TUI 프레임워크 비교 — 무엇을 얼마나 추상화하는가
Bubble Tea, Ratatui, Textual, OpenTUI를 기능표가 아니라 이벤트 루프, 상태, 레이아웃, 렌더러, 컴포넌트의 추상화 수준으로 비교한다.
현대 TUI 프레임워크를 단순히 언어별 목록으로 외우면 차이가 잘 안 보인다.
1
2
3
4
Go → Bubble Tea
Rust → Ratatui
Python → Textual
TypeScript → OpenTUI
더 유용한 질문은 이것이다.
각 프레임워크가 TUI 엔진의 어느 부분까지 대신해주는가?
앞 글에서 본 공통 구조를 다시 놓고 비교해보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
입력
↓
이벤트 루프
↓
상태
↓
레이아웃
↓
셀 버퍼
↓
렌더러 / 차이 계산
↓
터미널
한눈에 보기
| 프레임워크 | 핵심 철학 | 상태 모델 | 레이아웃 | 렌더러 | 컴포넌트 수준 |
|---|---|---|---|---|---|
| Bubble Tea | Elm Architecture | 강하게 제시 | 별도 조합 | 내장 셀 렌더러 | 중간 |
| Ratatui | Immediate Mode 렌더링 | 앱이 직접 소유 | Constraint/Layout | 이중 버퍼 차이 계산 | 낮음~중간 |
| Textual | 애플리케이션 프레임워크 | 반응형 상태 | CSS 계열 | 프레임워크가 관리 | 높음 |
| OpenTUI | native renderer + component tree | Core/React/Solid 선택 | Flexbox/Yoga | Zig native diff renderer | 높음 |
이 표에서 중요한 것은 “누가 더 좋다”가 아니다.
애플리케이션 개발자가 직접 책임질 영역이 얼마나 남아 있는가가 다르다.
Bubble Tea — 아키텍처를 강하게 잡아준다
Bubble Tea는 The Elm Architecture를 기반으로 한다.
핵심 구조는:
1
2
3
4
5
6
7
8
9
Model
↓
Msg
↓
Update
↓
Model
↓
View
이다.
공식적으로 프로그램은 상태를 가진 Model과 Init, Update, View를 중심으로 구성된다.
1
2
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd)
func (m model) View() tea.View
여기서:
1
2
3
4
Model = 애플리케이션 상태
Msg = 이벤트
Update = 상태 전이
View = 상태 → UI
라고 볼 수 있다.
즉 Bubble Tea가 가장 강하게 추상화하는 것은 애플리케이션의 흐름 자체다.
개발자가 이벤트 루프를 직접 for {}로 만들기보다 프레임워크 런타임이 메시지를 받아 Update로 흘려준다.
비동기 I/O도 Cmd → Msg 패턴으로 다시 이벤트 루프에 합류시킨다.
1
2
3
4
5
HTTP 요청
↓ Cmd
결과
↓ Msg
Update
그래서 복잡한 대화형 CLI/TUI에서도 상태 전이의 경계가 명확해진다.
Ratatui — 렌더링은 강하지만 앱 구조는 자유롭다
Ratatui는 Bubble Tea보다 애플리케이션 아키텍처를 덜 강제한다.
전형적인 구조는:
1
2
3
4
5
앱이 이벤트를 대기
↓
앱 상태 수정
↓
terminal.draw(|frame| ...)
에 가깝다.
즉 이벤트 루프와 상태 소유권은 애플리케이션이 직접 설계하는 경우가 많다.
대신 Ratatui는 다음 부분을 잘 추상화한다.
1
2
3
4
5
6
Frame
Widget
Layout
Style
Buffer
차이 렌더링(Diff Rendering)
특히 렌더링은 Immediate Mode 사고방식이 강하다.
매 프레임마다 현재 상태를 기준으로 원하는 UI 전체를 다시 기술한다.
1
2
3
4
terminal.draw(|frame| {
frame.render_widget(...);
frame.render_widget(...);
});
하지만 실제 터미널에는 모든 셀을 재출력하지 않는다.
Ratatui는 이전/현재 버퍼의 차이를 계산해 필요한 부분만 backend를 통해 출력한다.
즉:
1
2
애플리케이션 구조의 자유도 ↑
렌더링 추상화 강도 ↑
라는 조합이다.
Rust 개발자가 이벤트 루프나 async runtime을 직접 조합하고 싶은 경우 잘 맞는다.
Textual — TUI보다 애플리케이션 프레임워크에 가깝다
Textual은 추상화 수준이 훨씬 높다.
단순히:
1
터미널에 Widget 그리기
를 넘어:
1
2
3
4
5
6
Widget 트리
메시지/이벤트
반응형 상태(Reactive State)
Focus
CSS형 스타일링/레이아웃
애플리케이션 생명주기
까지 하나의 프레임워크 안에서 제공한다.
그래서 Python GUI/Web 프레임워크와 비슷한 감각으로 접근할 수 있다.
1
2
3
4
5
6
App
├─ Header
├─ Sidebar
├─ DataTable
├─ Input
└─ Footer
UI 트리와 반응형 속성이 살아 있고 상태 변경이 다시 레이아웃과 렌더링으로 이어진다.
Harlequin 같은 SQL IDE가 Textual과 잘 맞는 이유도 여기에 있다.
단순 선택 메뉴가 아니라 editor, tree, table, dialog, focus 이동 등 복잡한 애플리케이션 UI가 필요하기 때문이다.
OpenTUI — 웹 UI 추상화를 터미널까지 끌고 온다
OpenTUI는 조금 특이하다.
낮은 층에는 Zig로 작성된 native terminal renderer가 있고, 위에는 TypeScript API가 있다.
1
2
3
4
5
6
7
TypeScript
↓
OpenTUI Core
↓ ABI
Zig 렌더러
↓
터미널
Core만 써도:
1
2
3
4
5
6
Renderable 트리
입력 파싱
Flexbox 레이아웃
프레임 스케줄링
셀 버퍼
차이 렌더링
을 제공한다.
그리고 그 위에 React/Solid binding까지 있다.
1
2
3
4
5
6
7
React / Solid
↓
컴포넌트 트리
↓
OpenTUI Core
↓
native renderer
즉 개발자는:
1
2
3
4
<box flexDirection="row">
<Sidebar />
<Main />
</box>
처럼 터미널 좌표를 거의 생각하지 않고 컴포넌트를 먼저 설계할 수 있다.
OpenTUI 공식 문서 기준으로 렌더러는 컴포넌트 트리를 배치하고 변경된 터미널 셀만 업데이트한다.
이건 TUI 추상화가 웹 프론트엔드의 추상화 수준에 거의 도달한 사례다.
같은 Todo 앱을 만든다고 생각해보자
Bubble Tea
주로 생각할 것:
1
2
3
4
Model은 무엇인가?
어떤 Msg가 있는가?
Update에서 상태를 어떻게 바꿀까?
View가 무엇을 반환할까?
Ratatui
주로 생각할 것:
1
2
3
4
앱 상태는 어떻게 둘까?
이벤트 루프를 어떻게 돌릴까?
레이아웃을 어떻게 나눌까?
Frame에 어떤 Widget을 렌더링할까?
Textual
주로 생각할 것:
1
2
3
4
Widget 트리는 어떻게 구성할까?
메시지/이벤트 handler는 어디에 둘까?
반응형 상태는 무엇인가?
CSS 레이아웃/스타일을 어떻게 줄까?
OpenTUI
Core라면:
1
2
3
Renderable 트리
이벤트
Flex 레이아웃
React/Solid라면:
1
2
3
4
컴포넌트
상태/Signal
Hook/Effect
Flex 레이아웃
을 먼저 생각한다.
왜 프레임워크는 언어별로 갈리는가
여기까지 보면 한 가지 의문이 생긴다.
1
2
3
4
Go → Bubble Tea
Rust → Ratatui
Python → Textual
TypeScript → OpenTUI
TUI의 원리는 같은데 왜 프레임워크는 언어별로 나뉘는가?
이유는 프레임워크가 독립 실행 프로그램이 아니라 애플리케이션 코드와 같은 프로세스에서 함수, 객체, 타입을 직접 공유하는 라이브러리이기 때문이다.
예를 들어 Ratatui의 Frame, Widget, trait와 closure는 Rust의 타입 시스템과 호출 모델 안에서 동작한다. Python이나 C++ 코드가 이를 그대로 이해하는 것은 아니다.
1
2
3
4
5
Rust 애플리케이션
↓ 함수 / 타입 / trait
Ratatui
↓
터미널 backend
다른 언어에서 사용하려면 보통 중간에 FFI나 binding이 필요하다.
1
2
3
4
5
다른 언어
↓ binding / FFI
공통 ABI
↓
native library
따라서 TUI의 아키텍처 자체가 언어에 종속된 것은 아니다.
1
입력 → 이벤트 루프 → 상태 → 레이아웃 → 셀 버퍼 → diff renderer
이 원리는 어느 언어에서도 구현할 수 있다.
다만 각 언어 생태계가 이 원리를 자기 언어의 타입 시스템, 비동기 모델, 패키지 생태계에 맞게 다시 추상화한 결과가 Bubble Tea, Ratatui, Textual, OpenTUI 같은 프레임워크다.
즉 다음처럼 이해하는 편이 정확하다.
1
2
TUI 원리 → 언어 독립적
프레임워크의 구현/API → 대체로 언어 종속적
프레임워크는 아래의 어느 터미널 계층을 바라보는가
앞에서 터미널 추상화의 흐름을 다음처럼 확장해왔다.
1
2
3
4
5
6
7
8
9
10
11
12
13
Terminal Emulator
↑
TTY / PTY
↑
termios
↑
ANSI / VT
↑
terminfo
↑
curses
↑
현대 TUI 프레임워크
이 그림은 역사적으로 추상화가 추가되어 온 흐름을 이해하기에는 좋다.
하지만 이것을 모든 프로그램이 반드시 아래에서 위로 한 층씩 통과하는 실제 호출 스택으로 이해하면 안 된다.
현대 TUI 프레임워크는 curses → terminfo를 반드시 거치지 않는다. 많은 프레임워크가 자체 renderer, driver 또는 backend를 두고 ANSI/VT 계열 터미널 프로토콜까지 직접 내려간다.
대략적인 구조는 다음과 같다.
| 프레임워크 | 터미널 쪽 하위 계층 | ANSI/VT | terminfo | curses |
|---|---|---|---|---|
| Bubble Tea | 자체 renderer / terminal I/O | 사용 | 필수 아님 | 사용하지 않음 |
| Ratatui | Backend → Crossterm/Termion/Termwiz 등 | backend를 통해 사용 | backend에 따라 다름 | 필수 아님 |
| Textual | 자체 Driver / renderer | 사용 | 필수 아님 | 사용하지 않음 |
| OpenTUI | Zig native renderer | 사용 | 필수 아님 | 사용하지 않음 |
즉 공통적인 바닥은 대략 다음과 같다.
1
2
3
4
5
6
7
8
9
현대 TUI 프레임워크
↓
renderer / backend / driver
↓
ANSI / VT 계열 터미널 제어
↓
TTY / PTY
↓
Terminal Emulator
Bubble Tea
Bubble Tea는 curses 위에서 화면을 그리는 라이브러리가 아니다.
1
2
3
4
5
6
7
Bubble Tea
↓
자체 renderer / terminal I/O
↓
ANSI / VT
↓
TTY / PTY
애플리케이션 수준에서는 Model/Update/View를 제공하면서, 아래쪽에서는 터미널 제어 시퀀스를 만들어 화면을 갱신한다.
Ratatui
Ratatui는 터미널 출력을 Backend라는 별도 추상화 뒤에 둔다는 점이 특징적이다.
1
2
3
4
5
6
7
Ratatui
↓
Backend
↓
Crossterm / Termion / Termwiz / ...
↓
ANSI / VT 및 OS terminal I/O
그래서 Ratatui 자체와 실제 터미널 I/O 구현을 분리해 생각할 수 있다.
terminfo 같은 capability 정보의 사용 여부도 선택한 backend와 구현에 따라 달라질 수 있다.
Textual
Textual 역시 curses를 필수 기반으로 삼지 않는다.
1
2
3
4
5
6
7
Textual
↓
Driver / renderer
↓
ANSI / VT
↓
TTY / PTY
위에서는 Widget, 메시지, Reactive State 같은 높은 수준의 애플리케이션 모델을 제공하지만 아래에서는 자체 터미널 드라이버 계층을 통해 터미널과 통신한다.
OpenTUI
OpenTUI는 이 구조가 특히 명확하다.
1
2
3
4
5
6
7
8
9
TypeScript / React / Solid
↓
OpenTUI Core
↓ FFI
Zig native renderer
↓
ANSI / VT
↓
TTY / PTY
고수준 컴포넌트 모델과 저수준 native renderer를 명확히 분리하고 있다.
왜 terminfo를 반드시 쓰지 않는가
terminfo가 등장한 이유는 터미널마다 같은 기능을 수행하는 제어 시퀀스가 달랐기 때문이다.
과거에는 프로그램이 다음과 같은 차이를 직접 알기 어려웠다.
1
2
3
터미널 A → cursor_up = 시퀀스 A
터미널 B → cursor_up = 시퀀스 B
터미널 C → 색상 기능 자체가 다름
그래서 terminfo가 터미널의 capability를 데이터베이스로 제공했다.
1
2
3
4
5
6
7
애플리케이션
↓
terminfo
↓
"현재 터미널에서 cursor_up은 무엇인가?"
↓
해당 터미널의 제어 시퀀스
하지만 현대 terminal emulator들은 VT/xterm 계열의 ANSI escape sequence를 광범위하게 호환한다.
따라서 현대 TUI 프레임워크는 흔히 다음과 같은 전제를 둔다.
지원 대상은 현대적인 ANSI/VT 호환 터미널이다.
그러면 커서 이동, 화면 지우기, 색상, alternate screen, mouse reporting, bracketed paste 같은 기본 기능은 프레임워크나 backend가 직접 시퀀스를 생성해도 된다.
1
2
3
4
5
renderer / backend
↓
ANSI / VT sequence 직접 생성
↓
Terminal Emulator
또 현대 터미널 기능은 전통적인 terminfo capability만으로 설명하기 어려운 경우도 있다.
1
2
3
4
5
6
7
24-bit true color
OSC 8 hyperlink
Kitty keyboard protocol
Kitty graphics
Sixel
focus event
bracketed paste
그래서 현대 라이브러리들은 필요에 따라 다음 방법들을 함께 사용한다.
1
2
3
4
5
$TERM / 환경변수
터미널별 capability table
runtime probing
특정 터미널 프로토콜 감지
terminfo
즉 terminfo가 사라진 것이 아니다.
터미널을 제어하기 위해 반드시 통과해야 하는 중간 계층에서, capability를 알아내기 위한 여러 수단 중 하나로 역할이 바뀐 것에 가깝다.
이를 로드맵 관점에서 구분하면 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
역사적 추상화 흐름
ANSI/VT → terminfo → curses → 현대 TUI
실제 현대 TUI의 런타임 경로
현대 TUI
↓
renderer / backend / driver
↓
ANSI / VT
↓
TTY / PTY
따라서:
1
2
3
ANSI / VT → 현대 TUI의 사실상 공통 기반
terminfo → 선택적 capability 계층
curses → 전통적 화면 추상화, 현대 프레임워크는 자주 우회
라고 이해하면 된다.
왜 현대 TUI에서 Go와 Rust가 자주 보이는가
현대 TUI는 개발자 도구, 시스템 도구, CLI와 영역이 많이 겹친다.
이런 프로그램은 보통 다음 특성을 선호한다.
1
2
3
4
5
빠른 실행
낮은 런타임 부담
SSH/서버 환경에서의 사용
단일 실행 파일 배포
터미널과 OS 기능에 대한 접근
이 조건에 Go와 Rust가 잘 맞는다.
Go는 단일 바이너리 배포가 쉽고 동시성 모델이 단순해서 네트워크·CLI 도구를 빠르게 만드는 데 강하다. Bubble Tea는 그 위에 메시지와 상태 중심의 TUI 아키텍처를 제공한다.
Rust는 런타임 부담이 작고 시스템 수준 제어가 강해서 터미널 렌더링이나 파일 시스템, 비동기 I/O를 세밀하게 다루는 도구와 잘 맞는다. Ratatui는 렌더링 계층을 강하게 추상화하면서 애플리케이션 구조는 비교적 자유롭게 남겨둔다.
그래서 현대 TUI 생태계에서는 대략 이런 성격 차이가 보인다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Go
→ 개발 생산성 + 쉬운 배포
→ CLI/TUI 개발 도구에 강함
Rust
→ 성능 + 시스템 제어
→ 렌더러/시스템 도구에 강함
Python
→ 높은 추상화 + 빠른 UI 개발
→ 복잡한 애플리케이션형 TUI
TypeScript
→ 웹 컴포넌트 모델 재사용
→ React/Solid 스타일의 TUI
즉 Go와 Rust가 TUI의 원리를 독점하는 것이 아니라, 현대 TUI가 많이 사용되는 문제 영역과 두 언어의 장점이 잘 맞아서 존재감이 큰 것에 가깝다.
추상화가 높다고 무조건 좋은 것은 아니다
고수준 프레임워크는 많은 것을 대신해주지만 그만큼 런타임과 프레임워크 규칙도 커진다.
반대로 낮은 수준은 boilerplate가 늘지만 제어권이 크다.
1
2
3
4
5
6
7
8
9
10
11
낮은 추상화
Ratatui 계열
↑ 제어권 큼
↑ 앱 구조 직접 설계
Bubble Tea
↑ 상태 흐름 구조화
Textual / OpenTUI 고수준 API
↑ 컴포넌트/런타임 제공
↑ 복잡한 UI를 빠르게 구성
실제로 어떤 앱은 프레임워크가 제공하는 구조보다 특수 요구사항이 더 중요해서 자체 TUI 엔진을 유지한다.
이게 다음 글의 주제다.
중요한 흐름 — 렌더러보다 애플리케이션 모델이 위로 올라왔다
역사를 길게 놓으면 관심사의 변화가 보인다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
1970년대
"어떤 escape sequence를 보내지?"
1980년대
"어떤 터미널 capability를 쓰지?"
curses
"어떤 화면/창을 갱신하지?"
현대 TUI
"어떤 상태와 이벤트가 있지?"
컴포넌트 TUI
"어떤 컴포넌트 트리를 만들지?"
터미널은 그대로 셀 격자(Cell Grid)인데 개발자가 생각하는 추상화 수준만 계속 올라간 것이다.
프레임워크 선택 기준
언어가 이미 정해졌다면 생태계 선택이 상당 부분 자동으로 된다.
하지만 구조 관점에서는 다음 질문이 더 중요하다.
1
2
3
4
5
6
작은 단일 화면 도구인가?
복잡한 IDE형 앱인가?
비동기 I/O가 많은가?
상태 전이를 강하게 구조화하고 싶은가?
렌더링 제어권이 중요한가?
웹 프론트엔드 경험을 재사용하고 싶은가?
대략:
1
2
3
4
Go + 메시지/상태 아키텍처 → Bubble Tea
Rust + 렌더러/레이아웃 중심 → Ratatui
Python + 복잡한 앱 UI → Textual
TypeScript + 컴포넌트 UI → OpenTUI
로 출발점을 잡을 수 있다.
다음 단계 — 실제 앱은 무엇을 선택했나
이제 프레임워크 이론을 실제 도구에 꽂아본다.
1
2
3
4
5
6
7
fzf
btop
lazygit
Yazi
Harlequin
OpenCode
Neovim
흥미로운 점은 유명 TUI 앱이 모두 최신 프레임워크를 쓰는 것이 아니라는 것이다.
일부는 자체 렌더러를 만들고, 일부는 중간 수준 라이브러리를 쓰고, 일부는 고수준 프레임워크를 사용한다.
그 차이를 보면 언제 추상화를 가져다 쓰고 언제 직접 구현하는지가 보인다.
참고
- Bubble Tea — The Elm Architecture 기반 Model/Update/View
- Ratatui — Immediate Mode 프레임 렌더링과 이중 버퍼 차이 계산
- Textual — Python 반응형 TUI 애플리케이션 프레임워크
- OpenTUI — Zig native core + TypeScript, React/Solid bindings, Flexbox 레이아웃