소스: https://github.com/nirholas/claude-code
React 19 + Ink로 구축된 터미널 UI: 140개 컴포넌트, 80개 hooks, vim mode. 웹 앱과 동일한 React 패턴을 터미널에서.
react-ink, terminal-ui, ink-components, react-hooks, vim-mode, streaming-render, concurrent-rendering, chalk, useTextInput, useVimInput, usePasteHandler, design-system, REPL-screen
터미널 UI 프레임워크 선택지:
- readline / blessed: 전통적, 명령형
- React + Ink: 선언적, 컴포넌트 기반
- 직접 ANSI escape: 최대 제어, 최대 복잡도
Claude Code가 React를 선택한 이유:
- Streaming 중 UI 업데이트: 텍스트가 흘러나오면서 tool 상태가 바뀌고, permission 다이얼로그가 뜨는 동시 업데이트를 선언적으로 처리
- 140개 컴포넌트 규모: 이 규모에서 명령형 UI는 관리 불가능
- React Compiler: 자동 최적화된 re-render로 터미널 깜빡임 방지
- React 생태계: hooks, context, concurrent rendering 모두 활용
LLM 엔지니어링 인사이트: LLM 응답의 streaming은 본질적으로 연속적 상태 변화다. React의 선언적 렌더링 모델이 이를 가장 자연스럽게 표현한다. <StreamingText content={partialResponse} /> 하나로 끝.
src/components/
├── design-system/ # Box, Text, Spinner 등 기본 요소
├── messages/ # 메시지 렌더링 (user, assistant, tool, error)
├── permissions/ # Permission request 다이얼로그
├── input/ # 텍스트 입력, 자동완성
├── status/ # 상태바, progress indicator
└── [기능별 컴포넌트] # ~140개
| Component | 역할 |
|---|---|
REPL.tsx |
메인 화면 — 입력, 메시지 목록, 상태 |
Message.tsx |
메시지 렌더링 (syntax highlighting 포함) |
ToolUseBlock.tsx |
Tool 호출/결과 표시 |
PermissionRequest.tsx |
"이 작업을 허용하시겠습니까?" 다이얼로그 |
ThinkingIndicator.tsx |
Extended thinking 진행 표시 |
Spinner.tsx |
로딩 표시 |
useTextInput() // 기본 텍스트 입력 + 히스토리 탐색
useVimInput() // Vim 키바인딩 (normal/insert/visual mode)
useInputBuffer() // 입력 버퍼 관리 (멀티라인)
usePasteHandler() // 붙여넣기 (이미지 포함) 처리useCanUseTool() // Tool permission 체크
useIDEIntegration() // IDE 연결 상태
useDiffInIDE() // IDE에서 diff 표시
useMcpConnectivityStatus() // MCP 서버 상태useSessionBackgrounding() // 세션 백그라운드/포그라운드 전환
useRemoteSession() // Bridge/remote 세션 관리
useAssistantHistory() // 대화 히스토리 탐색
useSkillsChange() // Skill 변경 감지
useManagePlugins() // 플러그인 lifecycle 관리useToast() // Toast 알림
useRateLimitNotice() // Rate limit 경고
useDeprecationWarning() // 기능 폐지 알림src/vim/이 완전한 vim 키바인딩을 구현한다:
Normal mode:
h/j/k/l — 커서 이동
i/a/o — Insert mode 전환
dd — 줄 삭제
yy — 줄 복사
p — 붙여넣기
/ — 검색
: — 명령 모드
Insert mode:
일반 텍스트 입력
ESC — Normal mode 복귀
Visual mode:
v — 선택 시작
y/d — 선택 영역 복사/삭제
/vim 명령으로 활성화/비활성화. 활성화 시 useVimInput() hook이 useTextInput()을 대체한다.
LLM 엔지니어링 인사이트: CLI 도구의 타겟 사용자가 개발자라면, vim 키바인딩 지원이 채택률에 큰 영향을 줄 수 있다. "개발자의 근육 기억"을 존중하는 것이 UX의 핵심이다.
LLM 응답이 streaming으로 도착하면 매 delta마다 re-render가 발생한다. 최적화 전략:
- React Compiler: 자동으로 불필요한 re-render를 제거
- Content batching: 빠르게 도착하는 delta를 묶어서 한 번에 렌더링
- Selective re-render: 변경된 컴포넌트만 re-render (React의 기본 동작)
- ANSI escape 최적화: Ink가 변경된 부분만 터미널에 쓰기
| 웹 (DOM) | 터미널 (Ink) | |
|---|---|---|
| 렌더 타겟 | DOM elements | ANSI escape sequences |
| 레이아웃 | CSS (Flexbox, Grid) | Flexbox (Yoga layout) |
| 색상 | CSS colors | Chalk (256 color + TrueColor) |
| 이벤트 | DOM events | stdin keypress |
| 스크롤 | 브라우저 기본 | 수동 구현 |
src/components/design-system/에 터미널 전용 디자인 시스템:
<Box flexDirection="column" padding={1} borderStyle="round">
<Text bold color="green">Success</Text>
<Text dimColor>Details here</Text>
</Box>Ink의 Box와 Text가 터미널에서 CSS Flexbox와 유사한 레이아웃을 제공한다.
Step 1: Streaming-first UI 설계
LLM 응답은 본질적으로 streaming이다. UI를 "완성된 응답을 표시"가 아니라 "점진적으로 도착하는 데이터를 표시"로 설계한다:
// BAD: 응답 완료까지 대기
const response = await llm.complete(prompt)
render(response)
// GOOD: Streaming으로 점진적 렌더링
for await (const chunk of llm.stream(prompt)) {
appendToDisplay(chunk) // 도착하는 즉시 표시
}Step 2: Progress indicator는 필수
LLM 호출은 수 초~수십 초 걸린다. 사용자가 "동작 중인지" 알 수 있어야 한다:
- Thinking indicator: Extended thinking 중일 때 표시 (예: "Thinking..." + elapsed time)
- Tool execution status: 어떤 tool이 실행 중인지 표시 (예: "Running: npm test")
- Token counter: 현재까지 사용된 token 수 (비용 인식)
Step 3: Permission dialog 패턴
Tool 실행 전 사용자 승인이 필요한 경우, 명확한 permission dialog:
┌─ Permission Request ──────────────────────┐
│ BashTool wants to run: │
│ npm install lodash │
│ │
│ [y] Allow [n] Deny [a] Always allow │
└───────────────────────────────────────────┘
핵심: 무엇을 할 것인지 + 선택지 + 영구 허용 옵션. "Always allow" 옵션이 없으면 매번 승인하는 피로감이 쌓인다.
Step 4: 프레임워크 선택
- 10개 미만 컴포넌트: readline + chalk로 충분
- 10-50개 컴포넌트: Ink (React for terminal) 고려
- 50개 이상 또는 복잡한 상태: Ink + React hooks 필수
| UI 프레임워크 | 장점 | 단점 | 적합한 경우 |
|---|---|---|---|
| React + Ink (Claude Code) | 선언적, 컴포넌트 재사용, streaming 자연스러움 | 번들 크기 증가, 학습 곡선 | 복잡한 터미널 UI |
| blessed / blessed-contrib | 풍부한 위젯, 대시보드 | 유지보수 중단, 무거움 | 대시보드형 UI |
| readline + chalk | 가볍고 단순 | 복잡한 상태 관리 어려움 | 간단한 CLI |
| Web UI (Next.js 등) | 가장 풍부한 UI 가능 | 브라우저 필요, 배포 복잡 | 웹 앱, 비개발자 대상 |
- React for terminal: 140+ 컴포넌트 규모에서 선언적 UI 모델이 필수. Streaming 중 동시 업데이트를 자연스럽게 처리.
- Hook 패턴의 재사용: 80개 hook이 permission, session, input, notification을 관리. 웹 React와 동일한 composition 패턴.
- Vim mode = 개발자 UX: 타겟 사용자의 근육 기억을 존중하는 것이 채택률에 영향.
- Streaming render 최적화: React Compiler + content batching + selective re-render로 터미널 깜빡임 방지.
- 실전 적용 시: Streaming-first로 설계하고, progress indicator와 permission dialog를 초기에 넣어라. 이 두 가지가 LLM 앱 UX의 80%를 결정한다.