Skip to content

필사 모드: TUI 르네상스 2026 — Ratatui·Bubble Tea·Textual·Ink로 다시 아름다워진 터미널 (심층 가이드)

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

프롤로그 — 검은 화면 위의 르네상스

2010년대 중반, 누군가에게 "터미널 UI를 만든다"고 하면 두 가지 반응이 돌아왔다. 첫째, "왜?" 둘째, "ncurses?" 그리고 대화는 거기서 끝났다.

2026년의 풍경은 완전히 다르다.

- `lazygit`을 처음 본 사람이 "이거 Electron 앱이에요?"라고 묻는다. 아니다. Go로 짠 TUI다.

- `k9s`는 쿠버네티스 운영자의 표준 도구가 되었다. 웹 대시보드보다 빠르다.

- `atuin`은 셸 히스토리를 SQLite로 저장하고, 동기화하고, TUI로 검색한다. 한 번 쓰면 `Ctrl+R`로 못 돌아간다.

- `gum`은 셸 스크립트에 둥근 모서리 입력 폼을 입힌다. `gum choose`, `gum confirm`만으로 install.sh가 예뻐진다.

- `posting`은 Postman을 터미널로 옮겨놓았다. JSON 트리, 마크다운 응답, 변수 환경까지.

- `slumber`는 HTTP 클라이언트를 TOML 설정 파일 + 키바인딩으로 다시 짰다.

이게 가능했던 건 네 개의 toolkit이 거의 같은 시기에 성숙했기 때문이다.

- **Ratatui** (Rust, immediate-mode) — `tui-rs`에서 포크되어 2023년 정식 출범, 2026년 v0.31. lazygit-ish 도구의 Rust 진영을 사실상 점령.

- **Bubble Tea** (Go, Elm Architecture) — Charm사가 만든 The Elm Architecture(TEA)의 Go 포팅. lazygit, gh, gum, soft-serve의 기반.

- **Textual** (Python, declarative + CSS-like) — Will McGugan(Rich 저자)이 만든 Python 전용 toolkit. 2026년 5.x, posting의 기반.

- **Ink** (Node.js, JSX/React) — Vadim Demedes가 만든 React for CLIs. Claude Code, GitHub Copilot CLI, Gatsby CLI의 기반.

여기에 C++의 FTXUI, Rust의 cursive, C의 notcurses, Node의 blessed/blessed-contrib까지 더하면 "터미널은 죽은 platform이 아니라 부활한 platform"이라는 게 분명해진다.

이 글은 그 르네상스를 정리한다. 왜 다시 터미널인지(1장), 어떤 demo 앱들이 흐름을 바꿨는지(2장), 네 toolkit의 철학 차이(3~6장), 같은 카운터 앱을 네 언어로 짜는 비교(7장), 30분 첫 TUI 경로(8장), 정직한 trade-off(9장).

1장 · 왜 TUI가 다시 떴는가 — 6가지 이유

먼저 정직하게. TUI는 "감성"이나 "레트로"가 아니다. 그 환상으로는 lazygit이 GitHub Desktop을 대체하지 못한다. 진짜 이유는 따로 있다.

**1. SSH·컨테이너·CI 환경이 일상이 되었다.** 클라우드·쿠버네티스·원격 개발이 보편화되면서, 우리는 자주 "GUI가 없는 곳"에서 일한다. SSH 한 줄로 k9s를 띄울 수 있다는 건 X11 forwarding으로 GUI 앱 띄우는 것과 비교가 안 된다.

**2. 키보드 우선 워크플로의 재발견.** Vim·Neovim·Emacs 사용자에게 마우스 왕복은 인지 비용이다. lazygit이 `Ctrl+e`로 fetch, `s`로 stage, `c`로 commit으로 끝나는 흐름은 SourceTree보다 빠르다. 측정 가능하게.

**3. 터미널 에뮬레이터가 강해졌다.** Alacritty·WezTerm·Kitty·Ghostty가 GPU 가속·트루컬러·이미지 프로토콜·undercurl·합자(ligature)·마우스 이벤트까지 지원한다. 1990년대 ncurses 시절의 256색 한계는 옛말이다.

**4. Unicode와 폰트가 따라왔다.** Nerd Fonts(아이콘), Powerline 글리프, 박스 그리기 문자, 둥근 모서리(`╭─╮`)가 보편화되면서 "터미널은 텍스트만 그릴 수 있다"는 한계가 사실상 사라졌다.

**5. AI 코딩 도구의 기본 UI가 TUI로 갔다.** Claude Code, Codex CLI, GitHub Copilot CLI, Aider — 코딩 에이전트의 대표주자들이 모두 터미널 안에 산다. 채팅 UI가 웹이 아닌 곳에 있다는 건 이상한 일이 아니라 정상이 되었다.

**6. Charm·Ratatui라는 "마케팅 잘 하는 라이브러리"가 생겼다.** Charm사는 lipgloss·glow·gum·soft-serve·VHS 같은 도구로 "TUI도 예쁠 수 있다"는 걸 데모로 보여줬다. Ratatui는 책(2024년 Manning)·튜토리얼·전시(`ratatui.rs`)로 Rust TUI 신을 견인했다. 도구는 항상 있었다. 마케팅이 없었을 뿐.

기억할 한 줄: **"TUI는 GUI보다 못한 게 아니라, 다른 제약 위에서 다른 최적해를 찾는 형식이다."**

2장 · TUI를 다시 멋지게 만든 demo 앱들

도구의 성공은 도구 자신이 아니라 그 도구로 만들어진 결과물이 결정한다. 2020년대 TUI 부흥을 끌고 온 대표주자들을 본다.

lazygit (Go · Bubble Tea 이전 세대의 jesseduffield/gocui)

Git의 모든 일상 작업을 키 한두 번에 끝낸다. 패널 전환은 1·2·3·4, stage는 `s`, commit은 `c`, push는 `P`. 깃 GUI의 사실상 대안. 2026년 5월 기준 GitHub star 약 53k.

gh (Go · Bubble Tea 부분 사용)

GitHub의 공식 CLI. `gh pr create`, `gh pr view`, `gh repo clone` 같은 명령어가 익숙하지만, `gh pr checkout`이나 인터랙티브 모드에서는 Bubble Tea 패턴이 보인다.

gum (Go · Bubble Tea + Lipgloss)

Charm사의 "셸 스크립트용 위젯 라이브러리." `gum input`, `gum confirm`, `gum choose`, `gum spin` 같은 명령어를 bash에서 직접 호출한다. install 스크립트·dotfiles bootstrap에 사실상 표준.

k9s (Go · tcell + tview)

쿠버네티스 클러스터를 TUI로 운영. 네임스페이스 전환, pod logs, port-forward, edit deployment까지 키보드만으로. 운영팀의 표준 도구.

atuin (Rust · Ratatui)

Magical shell history. SQLite에 명령어 기록을 저장하고, 디바이스 간 동기화하고, `Ctrl+R`로 fuzzy 검색한다. 한 번 쓰면 기본 `Ctrl+R`로 못 돌아간다.

posting (Python · Textual)

"터미널의 Postman." HTTP 요청·환경 변수·컬렉션·JSON 트리 보기·마크다운 응답 렌더링까지. Darren Burns가 Textual의 limit를 보여주려고 만든 demo 앱이 정식 도구로 성장.

slumber (Rust · Ratatui)

YAML 설정 + 키바인딩으로 짠 HTTP 클라이언트. Postman 컬렉션을 텍스트로 다루고 싶은 사람을 위한 도구.

gitui (Rust · Ratatui)

lazygit의 Rust 버전. 더 빠르고 메모리 덜 먹는다는 게 셀링 포인트. 2026년 v0.27.

btop (C++ · 자체 렌더링)

`top`/`htop`의 후계자. 둥근 모서리 박스, 그래프, 마우스 지원. C++로 짰지만 UI 디자인이 TUI 르네상스의 시각적 기준을 만들었다.

glow (Go · Bubble Tea)

마크다운 뷰어. `glow README.md`로 터미널에서 마크다운을 예쁘게 본다. Charm 생태계의 출발점 중 하나.

vhs (Go · Bubble Tea + Headless 터미널)

`.tape` 스크립트로 터미널 GIF/MP4를 자동 생성. README의 데모 GIF가 일관되게 보이는 비결.

nushell + tabiew + others

CSV·Parquet 뷰어 `tabiew` (Rust/Ratatui), 파일 매니저 `yazi` (Rust/Ratatui), 검색 도구 `television` (Rust/Ratatui) 등 Ratatui 기반 도구가 폭증.

여기서 한 가지 패턴이 보인다. **Go = Bubble Tea, Rust = Ratatui, Python = Textual, Node = Ink.** 각 언어의 사실상 표준 toolkit이 정해졌다.

3장 · Ratatui — Rust의 immediate-mode TUI

출발점

Florian Dehau가 2016년에 시작한 `tui-rs`가 2023년 메인테이너 부재로 멈췄고, 커뮤니티가 `ratatui`로 포크하면서 정식 출범했다. 2026년 5월 기준 v0.31.x, GitHub star 약 14k, 의존하는 crate 수가 1,000개를 넘어섰다. Rust 생태계에서 atuin·gitui·yazi·bottom·television 같은 사실상 모든 메이저 TUI가 Ratatui 위에 산다.

핵심 철학 — Immediate-mode

대부분 GUI 프레임워크는 **retained-mode**다. 위젯 트리를 만들고, 상태가 바뀌면 트리를 업데이트하고, 프레임워크가 다시 그린다. Ratatui는 **immediate-mode**다. 매 프레임 사용자가 "이번 프레임의 UI는 이거다"를 직접 그린다.

// 매 프레임 호출되는 draw 클로저

terminal.draw(|frame| {

let area = frame.area();

let block = Block::default().borders(Borders::ALL).title("Counter");

let text = Paragraph::new(format!("count: {}", count)).block(block);

frame.render_widget(text, area);

})?;

이게 왜 좋은가? 상태 동기화 버그가 없다. 트리에 stale node가 남을 일이 없다. 게임 엔진(`bevy_egui`, `egui`)이 즐겨 쓰는 패턴이 그대로 터미널에 왔다.

대신 비용도 있다. 매 프레임 다 그리는 책임은 개발자에게 있다. 60fps로 돌리면 60번 draw 클로저가 호출된다. 비싼 계산은 직접 캐시해야 한다. (실제로는 input event가 있을 때만 다시 그리는 패턴이 일반적.)

위젯과 layout

use ratatui::{

layout::{Constraint, Direction, Layout},

widgets::{Block, Borders, List, ListItem, Paragraph},

};

let chunks = Layout::default()

.direction(Direction::Horizontal)

.constraints([Constraint::Percentage(30), Constraint::Percentage(70)])

.split(frame.area());

frame.render_widget(sidebar, chunks[0]);

frame.render_widget(main_area, chunks[1]);

기본 위젯: `Block`, `Paragraph`, `List`, `Table`, `Tabs`, `Gauge`, `Sparkline`, `BarChart`, `Chart`, `Canvas`. `Canvas`로는 점·선·심지어 세계지도까지 그릴 수 있다.

이벤트 — crossterm

Ratatui 자체는 그리기만 하고, 이벤트는 `crossterm`(또는 `termion`)이 담당한다.

use crossterm::event::{self, Event, KeyCode};

if event::poll(std::time::Duration::from_millis(100))? {

if let Event::Key(key) = event::read()? {

match key.code {

KeyCode::Char('q') => break,

KeyCode::Up => count += 1,

KeyCode::Down => count -= 1,

_ => {}

}

}

}

누가 쓰는가

- **atuin** — 셸 히스토리

- **gitui** — Git TUI

- **yazi** — 파일 매니저

- **bottom** — 시스템 모니터(htop 대안)

- **television** — fuzzy finder, fzf의 후계자 후보

- **tabiew** — CSV/Parquet 뷰어

- **slumber** — HTTP 클라이언트

강점·약점

- 강점 — 성능(컴파일된 Rust), 메모리 효율, 안정성, 풍부한 위젯, 활발한 커뮤니티, 책(Manning 2024)·`ratatui.rs` 공식 사이트.

- 약점 — immediate-mode 학습 곡선, 보일러플레이트(crossterm 초기화·alternate screen·raw mode), 디버그 어렵다(터미널 그 자체가 stdout이라 println 디버그 불가).

4장 · Bubble Tea — Go의 The Elm Architecture

출발점

Charm사(Toby Padilla·Christian Rocha)가 2020년에 발표. The Elm Architecture(TEA)를 Go에 직접 옮겼다. 2026년 5월 기준 v1.3, GitHub star 약 32k. lazygit이 기반을 옮기는 중, gh CLI의 일부, gum 전체, glow 전체, soft-serve 전체가 Bubble Tea 위에 있다.

핵심 철학 — Elm Architecture(MVU)

`Model` → `Update` → `View`. 상태는 `Model`에 있다. 메시지(이벤트)가 들어오면 `Update(msg, model) -> (newModel, cmd)`. `View(model) -> string`이 화면을 그린다. 부수효과는 `Cmd`(고차 함수)로 격리한다.

type Model struct {

count int

}

func (m Model) Init() tea.Cmd { return nil }

func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {

switch msg := msg.(type) {

case tea.KeyMsg:

switch msg.String() {

case "q", "ctrl+c":

return m, tea.Quit

case "up":

m.count++

case "down":

m.count--

}

}

return m, nil

}

func (m Model) View() string {

return fmt.Sprintf("count: %d\n\nq to quit", m.count)

}

이 함수형 패턴이 주는 이점은 immediate-mode와 다르다. 상태 변화의 **흐름이 한 곳**에 있다. `Update`만 보면 무엇이 가능한 변화인지 안다. 테스트하기 쉽다(순수 함수).

Lipgloss — 스타일링

Bubble Tea 단독으로는 색·박스·정렬만 그린다. 그 위에 **Lipgloss**가 CSS-like 스타일 API를 얹는다.

var titleStyle = lipgloss.NewStyle().

Bold(true).

Foreground(lipgloss.Color("#FAFAFA")).

Background(lipgloss.Color("#7D56F4")).

PaddingTop(1).

PaddingBottom(1).

PaddingLeft(2).

PaddingRight(2)

fmt.Println(titleStyle.Render("Hello, TUI"))

색·여백·박스·정렬·정렬·줄임·줄 바꿈 모두 메서드 체인으로. 둥근 모서리 박스도 한 줄.

Bubbles — 위젯 라이브러리

Bubble Tea 코어는 의도적으로 작다. 위젯은 별도 라이브러리 `bubbles`에서. textinput, textarea, list, table, viewport, spinner, progress, paginator, key 등.

Wish & Soft Serve — TUI를 SSH로

Charm의 또 다른 작품. `wish`는 SSH 서버 라이브러리. Bubble Tea 앱을 SSH로 노출시키면, 사용자는 `ssh tui.example.com`만으로 접속한다. install 필요 없음. `soft-serve`(Charm의 셀프호스트 Git 서버)가 이 패턴의 대표 예.

누가 쓰는가

- **gum** — 셸 스크립트 위젯

- **glow** — 마크다운 뷰어

- **vhs** — 터미널 녹화

- **soft-serve** — Git 서버 TUI

- **bubbletea-app-template** 기반 신생 앱 다수

- **lazygit** 일부 — 점진 마이그레이션

강점·약점

- 강점 — 깔끔한 아키텍처, 함수형이라 테스트 쉬움, Lipgloss로 시각적 폴리시 높음, Charm 생태계의 거대한 도구 모음.

- 약점 — Go 인터페이스의 매번 캐스팅 보일러플레이트(`tea.Msg` 타입 스위치), 매우 큰 모델에서는 immutable update의 비용, ad-hoc 비동기는 `tea.Cmd`로 감싸야 함.

5장 · Textual — Python의 선언적 TUI

출발점

Will McGugan(Rich 라이브러리 저자)이 2021년에 시작. Rich가 "예쁜 print"라면 Textual은 "예쁜 앱". 2026년 5월 기준 v5.x, GitHub star 약 28k. posting의 성공이 결정타였다.

핵심 철학 — 웹스러운 선언

CSS-like 스타일링(`.tcss`), 위젯 트리, 이벤트 핸들러, async-first. 한마디로 "터미널에서 도는 React 비슷한 것" — 다만 명시적 가상 DOM은 없다.

from textual.app import App, ComposeResult

from textual.widgets import Header, Footer, Static

class CounterApp(App):

BINDINGS = [

("up", "increment", "Increment"),

("down", "decrement", "Decrement"),

("q", "quit", "Quit"),

]

def __init__(self):

super().__init__()

self.count = 0

def compose(self) -> ComposeResult:

yield Header()

yield Static(f"count: {self.count}", id="counter")

yield Footer()

def action_increment(self):

self.count += 1

self.query_one("#counter", Static).update(f"count: {self.count}")

def action_decrement(self):

self.count -= 1

self.query_one("#counter", Static).update(f"count: {self.count}")

if __name__ == "__main__":

CounterApp().run()

여기서 보이는 것 — `compose`로 트리 선언, `BINDINGS`로 단축키 매핑, `action_*` 메서드가 자동으로 호출됨, `query_one`으로 CSS-like 셀렉터로 위젯 조회.

CSS-like 스타일 — TCSS

별도 `.tcss` 파일에 스타일을 분리.

Static {

background: $boost;

padding: 1 2;

border: round $primary;

text-align: center;

}

#counter {

width: 30;

height: 5;

margin: 1;

}

색은 디자인 시스템 변수(`$primary`, `$boost`, `$success` 등)를 그대로 쓸 수 있다.

위젯과 풍부함

Static, Label, Button, Input, TextArea, Select, Switch, Checkbox, RadioSet, ListView, DataTable, Tree, Tabs, ContentSwitcher, ProgressBar, Sparkline, LoadingIndicator, Pretty, Markdown, RichLog, Log, Header, Footer, Placeholder. 정말 많다.

Async-first

async def on_button_pressed(self, event: Button.Pressed) -> None:

async with httpx.AsyncClient() as client:

result = await client.get("https://api.example.com")

self.query_one("#result", Static).update(result.text)

Python의 `asyncio`를 일급으로 받아들여서 HTTP·DB·파일 IO가 자연스럽다. posting이 이걸 활용한 대표 예.

Textual Web — 같은 코드, 웹으로

`textual serve`로 같은 앱을 브라우저에서 띄울 수 있다. SSH·로컬·웹 세 가지 배포 경로. 2026년 현재 베타지만 작동한다.

누가 쓰는가

- **posting** — 터미널 HTTP 클라이언트(Postman 대안)

- **harlequin** — DuckDB·Postgres SQL IDE

- **textual-keymaps** — 시각적 키맵

- **frogmouth** — 마크다운 뷰어

- **memray** — 메모리 프로파일러(부분)

- 사내 데이터 도구 다수

강점·약점

- 강점 — Python 친화적, async 자연스러움, CSS 분리로 디자이너·개발자 분업 쉬움, Textual Web으로 웹 배포까지, 풍부한 위젯, Rich와의 시너지(마크다운·코드 하이라이트).

- 약점 — Python 시작 시간(인터프리터 + import), 메모리 사용량(Rust/Go 대비 큼), GIL의 한계, async 학습 곡선.

6장 · Ink — Node.js + JSX/React for CLIs

출발점

Vadim Demedes가 2017년에 시작. "React 컴포넌트로 CLI를 짜고 싶다"는 단순한 아이디어에서 출발. 2026년 5월 기준 v5.x, GitHub star 약 28k. Claude Code, GitHub Copilot CLI, Gatsby CLI, Prisma CLI, AWS Amplify CLI 모두 Ink로 짰다.

핵심 철학 — React 컴포넌트가 텍스트를 렌더

브라우저에서 React가 DOM을 렌더하듯, Ink는 React를 터미널 텍스트로 렌더한다. `<div>`/`<span>` 대신 `<Box>`/`<Text>`.

const Counter = () => {

const [count, setCount] = useState(0);

useInput((input, key) => {

if (key.upArrow) setCount(c => c + 1);

if (key.downArrow) setCount(c => c - 1);

if (input === 'q') process.exit(0);

});

return (

);

};

render(<Counter />);

React Hooks가 그대로 작동한다. `useState`, `useEffect`, `useReducer`, 커스텀 훅까지. 라우팅이 필요하면 `ink-router`, 폼이 필요하면 `ink-form`, 입력은 `ink-text-input`, 멀티-셀렉트는 `ink-multi-select`.

Flexbox layout

브라우저처럼 Flexbox로 배치한다.

`flexDirection`, `flexGrow`, `justifyContent`, `alignItems`, `padding`, `margin`, `width`, `height` 모두 친숙한 CSS 이름 그대로.

Suspense·streaming·async

React 18+ 패턴(Suspense, transitions)이 Ink 5에서 작동한다. AI 코딩 도구가 토큰 스트리밍 응답을 보여주는 데 적합한 이유.

누가 쓰는가

- **Claude Code** — Anthropic의 코딩 에이전트

- **GitHub Copilot CLI** — `gh copilot`

- **Gatsby CLI**

- **Prisma CLI**

- **AWS Amplify CLI**

- **Twilio CLI**

- **terkelg/prompts**의 React 버전들

강점·약점

- 강점 — React 지식 그대로 전이, npm 생태계, JSX의 표현력, AI/스트리밍 UI 자연스러움, 디자이너 친화적.

- 약점 — Node.js 시작 비용(node + V8 + 의존성 로드), 메모리(가장 무거움), `Text` 내부에 `Box` 못 넣는 등 텍스트-노드 트리의 제약, 큰 트리에서 리렌더 비용.

7장 · 같은 카운터 앱, 네 언어로 — 직접 비교

가장 간단한 앱: "↑/↓로 카운트 증감, q로 종료." 네 toolkit이 어떻게 다른지 한 화면에 보자.

Ratatui (Rust)

use crossterm::{

event::{self, Event, KeyCode},

terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},

ExecutableCommand,

};

use ratatui::{

backend::CrosstermBackend,

widgets::{Block, Borders, Paragraph},

Terminal,

};

use std::io;

fn main() -> io::Result<()> {

enable_raw_mode()?;

let mut stdout = io::stdout();

stdout.execute(EnterAlternateScreen)?;

let backend = CrosstermBackend::new(stdout);

let mut terminal = Terminal::new(backend)?;

let mut count: i32 = 0;

loop {

terminal.draw(|f| {

let block = Block::default().borders(Borders::ALL).title("Counter");

let para = Paragraph::new(format!("count: {}", count)).block(block);

f.render_widget(para, f.area());

})?;

if event::poll(std::time::Duration::from_millis(100))? {

if let Event::Key(key) = event::read()? {

match key.code {

KeyCode::Char('q') => break,

KeyCode::Up => count += 1,

KeyCode::Down => count -= 1,

_ => {}

}

}

}

}

disable_raw_mode()?;

terminal.backend_mut().execute(LeaveAlternateScreen)?;

Ok(())

}

Bubble Tea (Go)

package main

"fmt"

"os"

tea "github.com/charmbracelet/bubbletea"

)

type model struct{ count int }

func (m model) Init() tea.Cmd { return nil }

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {

if k, ok := msg.(tea.KeyMsg); ok {

switch k.String() {

case "q", "ctrl+c":

return m, tea.Quit

case "up":

m.count++

case "down":

m.count--

}

}

return m, nil

}

func (m model) View() string {

return fmt.Sprintf("count: %d\n\n(q to quit)\n", m.count)

}

func main() {

if _, err := tea.NewProgram(model{}).Run(); err != nil {

fmt.Println(err)

os.Exit(1)

}

}

Textual (Python)

from textual.app import App, ComposeResult

from textual.widgets import Static

class CounterApp(App):

BINDINGS = [

("up", "inc", "Up"),

("down", "dec", "Down"),

("q", "quit", "Quit"),

]

count = 0

def compose(self) -> ComposeResult:

yield Static(f"count: {self.count}", id="c")

def render_count(self):

self.query_one("#c", Static).update(f"count: {self.count}")

def action_inc(self):

self.count += 1

self.render_count()

def action_dec(self):

self.count -= 1

self.render_count()

if __name__ == "__main__":

CounterApp().run()

Ink (TSX)

const Counter = () => {

const [count, setCount] = useState(0);

useInput((input, key) => {

if (key.upArrow) setCount(c => c + 1);

if (key.downArrow) setCount(c => c - 1);

if (input === 'q') process.exit(0);

});

return (

);

};

render(<Counter />);

코드 라인 수 비교

- Ratatui: 약 35줄(터미널 raw 모드 셋업 포함). 가장 길지만 가장 명시적.

- Bubble Tea: 약 30줄. MVU 패턴이 깔끔하지만 타입 스위치가 보일러플레이트.

- Textual: 약 20줄. 가장 짧고 가장 읽기 쉽다.

- Ink: 약 15줄. JSX 친화적이라면 가장 짧다.

라인 수가 곧 우열은 아니다. 가장 짧은 코드(Ink)는 가장 무거운 런타임(Node + V8)을 깔고 있고, 가장 긴 코드(Ratatui)는 가장 가벼운 단일 정적 바이너리를 만든다.

8장 · 첫 TUI 30분 — 실전 경로

Path A: Ratatui로 시작 (성능·배포 우선)

1. 새 프로젝트

cargo new my_tui && cd my_tui

2. 의존성 추가

cargo add ratatui crossterm

3. src/main.rs에 위 카운터 예제 붙여넣기

cargo run

배포는 `cargo build --release` 한 줄로 단일 정적 바이너리. 의존성 zero.

Path B: Bubble Tea로 시작 (Go 생태계 친화)

mkdir my_tui && cd my_tui

go mod init my_tui

go get github.com/charmbracelet/bubbletea

main.go에 위 예제 붙여넣기

go run main.go

크로스 플랫폼 빌드는 `GOOS=linux GOARCH=amd64 go build` 식으로 간단.

Path C: Textual로 시작 (Python 친화)

pip install textual

python counter.py

`textual run --dev counter.py`로 hot reload + dev console까지 켜진다.

Path D: Ink로 시작 (React 친화)

npx create-ink-app my-cli --typescript

cd my-cli

npm run dev

스캐폴드가 텍스트 입력·테스트·빌드까지 다 해준다.

그다음에 할 일 — 공통 체크리스트

- **alt screen 모드** 진입/이탈을 정확히 처리(Ratatui는 명시적, 나머지는 자동).

- **raw mode** 진입 시 Ctrl+C 처리도 직접(또는 toolkit이 처리).

- **resize 이벤트** 받으면 다시 그리기.

- **mouse 이벤트** 옵트인.

- **truecolor** 지원되는지 `$COLORTERM` 확인.

- **Unicode 너비** 처리(이모지·CJK는 2칸 차지). `unicode-width` 크레이트나 `wcwidth` 사용.

- **로그 분리** — println으로 디버그 못 함. 별도 로그 파일에 쓰거나 dev console로.

9장 · 정직한 trade-off — TUI가 안 어울리는 경우

TUI가 모든 걸 해결하지는 않는다. 다음 경우엔 다른 도구가 낫다.

1. 차트·그래프 무거운 대시보드

박스 그리기 문자로 sparkline은 그릴 수 있지만, 산점도·히트맵·3D 차트는 한계가 명확하다. 데이터 시각화가 중심이면 웹/Streamlit·Grafana가 낫다.

2. 이미지·동영상

iTerm2·Kitty·WezTerm의 inline image 프로토콜이 있지만 사용자 환경 의존성이 크다. tmux 안에서 깨지는 경우가 많다.

3. 마우스 중심 워크플로

복잡한 드래그 앤 드롭, 시각적 편집(다이어그램·이미지·캔버스). 가능은 하지만 사람들이 좋아하지 않는다.

4. 비개발자 대상

TUI는 사용자에게 키 단축키와 모드 개념을 요구한다. 일반 사용자에게는 학습 곡선이 너무 가파르다.

5. 접근성이 critical한 경우

스크린리더는 GUI/웹에 최적화되어 있고, TUI에 대한 지원은 부분적이다. 시각장애 사용자 비율이 큰 도구는 GUI/웹이 낫다.

6. 매우 큰 데이터를 한 화면에

터미널의 한 줄당 문자 수, 화면당 줄 수의 제한이 있다. 가상화 스크롤·페이지네이션이 필요한데, 웹에서 더 자연스럽다.

기억할 한 줄: **"TUI는 키보드 우선 워크플로, 원격 접근, 가벼움이 가치인 곳에서 빛난다. 시각화 중심·일반 사용자·접근성 critical인 곳에서는 GUI/웹이 낫다."**

10장 · 도구 선택 매트릭스

상황별로 어떤 toolkit을 고를지 한 표로.

| 상황 | 추천 | 이유 |

|---|---|---|

| 시스템 도구(파일/Git/모니터링) | Ratatui | 성능·배포·단일 바이너리 |

| 셸 스크립트 위젯 | Bubble Tea(gum) | 즉시 사용 가능, 표준 도구 |

| 사내 데이터 도구(SQL·HTTP·분석) | Textual | async·Python 라이브러리·풍부한 위젯 |

| AI 코딩 도구·CLI 마법사 | Ink | 스트리밍 UI·React 친화 |

| 새 Go 서비스의 관리 콘솔 | Bubble Tea | 같은 Go 코드베이스 안에서 |

| Rust 서버의 관리자 TUI | Ratatui | 같은 바이너리 안에서 |

| 빠른 프로토타입 | Textual | 가장 적은 보일러플레이트 |

| SSH로 노출할 멀티유저 TUI | Bubble Tea + wish | Charm의 SSH 라이브러리 |

| 클러스터 운영(k8s 등) | Bubble Tea or Ratatui | 키 단축키·즉시 반응 |

에필로그 — 터미널은 죽지 않았다, 부활했다

긴 글이었다. 정리한다.

한 줄 요약

**TUI 르네상스의 본질은 '레트로 감성'이 아니라 '키보드 우선·원격 친화·가벼운 배포'라는 세 가지 가치의 재발견이고, Ratatui·Bubble Tea·Textual·Ink가 각자 다른 언어 생태계에서 그 가치를 구현한다.**

30초 체크리스트

1. 새 TUI를 시작하기 전에, 정말 TUI가 맞는 형식인지 9장의 anti-case와 대조했는가?

2. 대상 사용자가 키보드 중심 워크플로에 익숙한가?

3. 언어 생태계·팀 역량에 맞는 toolkit을 골랐는가(Rust→Ratatui, Go→Bubble Tea, Python→Textual, Node→Ink)?

4. 단일 바이너리 배포가 중요한가? Rust/Go가 맞다.

5. async가 중요한가? Textual·Ink가 자연스럽다.

6. raw mode·alt screen·resize·Unicode 너비를 처리할 준비가 됐나?

7. 터미널 디버그 어려움(println 못 씀)을 위한 별도 로그 파일/dev console을 준비했나?

8. 마우스 옵션을 켜야 한다면, 어떤 사용자가 거부할지 생각했나?

9. truecolor를 가정해도 되는가? `$COLORTERM`을 확인하는가?

10. GIF/스크린샷용 vhs 같은 도구로 데모를 만들어 마케팅까지 챙겼나?

안티패턴 10가지

1. 매 키 입력마다 무거운 계산을 동기로 돌려서 UI가 멈춤.

2. immediate-mode(Ratatui)에서 매 프레임 비싼 IO를 다시 함.

3. Ink의 `Text` 안에 `Box`를 넣어서 런타임 에러.

4. Bubble Tea의 `Update`에서 부수효과를 `Cmd`로 격리하지 않고 직접 실행.

5. Textual의 `compose`에서 동기 IO를 길게 막아 시작 시간 폭주.

6. raw mode를 활성화한 채 panic으로 종료해 터미널 상태가 깨짐.

7. 터미널 resize 이벤트를 무시해서 레이아웃 깨짐.

8. CJK·이모지 너비 처리를 안 해서 위젯 경계가 어긋남.

9. `println`/`fmt.Println`/`print()`을 디버그로 써서 화면을 망가뜨림.

10. 사용자가 다양한 터미널을 쓴다는 사실을 잊고 `$TERM`/`$COLORTERM` 미체크.

다음 글 예고

다음 글 후보: **Charm 생태계 정복기 — Bubble Tea·Lipgloss·Bubbles·Wish·Glow·VHS 한 호흡으로**, **Ratatui로 Git TUI 만들기 — lazygit의 Rust 클론을 한 주 만에**, **Textual + SQLite로 사내 데이터 도구 — Python의 강점을 TUI로**.

> "GUI는 마우스의 시대를 정의했고, TUI는 키보드의 시대를 다시 가져왔다. 둘은 적이 아니라, 같은 작업에 대한 다른 답이다."

— TUI 르네상스 2026, 끝.

참고 / References

- [Ratatui 공식 사이트](https://ratatui.rs/)

- [Ratatui GitHub](https://github.com/ratatui/ratatui)

- [Ratatui Book (Manning)](https://www.manning.com/books/ratatui-cookbook)

- [Bubble Tea GitHub](https://github.com/charmbracelet/bubbletea)

- [Charm 공식 사이트](https://charm.sh/)

- [Lipgloss GitHub](https://github.com/charmbracelet/lipgloss)

- [Bubbles 위젯 라이브러리](https://github.com/charmbracelet/bubbles)

- [Wish — SSH 서버 라이브러리](https://github.com/charmbracelet/wish)

- [Textual 공식 문서](https://textual.textualize.io/)

- [Textual GitHub](https://github.com/Textualize/textual)

- [Rich 라이브러리](https://github.com/Textualize/rich)

- [Ink 공식 GitHub](https://github.com/vadimdemedes/ink)

- [Ink UI 컴포넌트](https://github.com/vadimdemedes/ink-ui)

- [lazygit GitHub](https://github.com/jesseduffield/lazygit)

- [gh CLI](https://github.com/cli/cli)

- [gum — 셸 스크립트 위젯](https://github.com/charmbracelet/gum)

- [k9s — 쿠버네티스 TUI](https://github.com/derailed/k9s)

- [atuin — 셸 히스토리](https://github.com/atuinsh/atuin)

- [posting — HTTP 클라이언트(Textual)](https://github.com/darrenburns/posting)

- [slumber — HTTP 클라이언트(Ratatui)](https://github.com/LucasPickering/slumber)

- [gitui — Git TUI(Ratatui)](https://github.com/extrawurst/gitui)

- [yazi — 파일 매니저(Ratatui)](https://github.com/sxyazi/yazi)

- [bottom — 시스템 모니터(Ratatui)](https://github.com/ClementTsang/bottom)

- [btop — 시스템 모니터(C++)](https://github.com/aristocratos/btop)

- [glow — 마크다운 뷰어](https://github.com/charmbracelet/glow)

- [vhs — 터미널 녹화](https://github.com/charmbracelet/vhs)

- [crossterm — Rust 터미널 IO](https://github.com/crossterm-rs/crossterm)

- [FTXUI — C++ TUI](https://github.com/ArthurSonzogni/FTXUI)

- [Notcurses — C TUI](https://github.com/dankamongmen/notcurses)

- [blessed-contrib — Node](https://github.com/yaronn/blessed-contrib)

- [harlequin — SQL IDE(Textual)](https://github.com/tconbeer/harlequin)

- [television — fuzzy finder(Ratatui)](https://github.com/alexpasmantier/television)

- [tabiew — CSV/Parquet 뷰어(Ratatui)](https://github.com/shshemi/tabiew)

- [Alacritty](https://github.com/alacritty/alacritty)

- [WezTerm](https://wezfurlong.org/wezterm/)

- [Kitty terminal](https://sw.kovidgoyal.net/kitty/)

- [Ghostty](https://ghostty.org/)

- [The Elm Architecture](https://guide.elm-lang.org/architecture/)

현재 단락 (1/415)

2010년대 중반, 누군가에게 "터미널 UI를 만든다"고 하면 두 가지 반응이 돌아왔다. 첫째, "왜?" 둘째, "ncurses?" 그리고 대화는 거기서 끝났다.

작성 글자: 0원문 글자: 18,008작성 단락: 0/415