Claw Code 깊이 읽기 #8 — TUI 렌더링 파이프라인과 미래 로드맵

조현상·2026년 4월 2일

ClaudeCode

목록 보기
8/17
post-thumbnail

Claude Code 클론 프로젝트 Claw Code의 소스를 한 줄씩 읽으며, 아키텍처 결정의 "왜"를 추적하는 시리즈입니다.


들어가며: 3,897줄의 모놀리스와 정교한 렌더러의 공존

시리즈의 마지막 편이다. 1편에서 프로젝트의 큰 그림을 조망하고, 2~5편에서 Rust 크레이트의 내부를 해부하고, 6편에서 Python 참조 구현을, 7편에서 테스트 전략을 분석했다. 이제 사용자가 실제로 마주하는 터미널 인터페이스를 들여다볼 차례다.

Claw Code의 CLI(rusty-claude-cli)는 흥미로운 이중성을 품고 있다. render.rs는 641줄의 정교한 마크다운 렌더링 파이프라인으로, pulldown-cmark 이벤트 파싱, syntect 구문 강조, 스트리밍 안전 경계 탐지까지 갖추고 있다. 반면 main.rs는 3,897줄의 모놀리스로, REPL 루프, 23개 슬래시 커맨드, 40개 이상의 포맷팅 함수, 세션 관리가 전부 하나의 파일에 뒤섞여 있다.

이번 편에서는 이 두 세계를 모두 분석하고, 프로젝트가 공개한 6단계 TUI 개선 로드맵의 기술적 의미를 해석하며, PARITY.md에 기록된 미구현 영역을 통해 프로젝트의 미래 방향을 전망한다.


1. 마크다운 렌더링 파이프라인: render.rs의 아름다움

1.1 3층 구조: 파싱 → 상태 추적 → ANSI 출력

render.rs의 렌더링 파이프라인은 세 개의 계층으로 구성된다:

pulldown_cmark::Parser (마크다운 → 이벤트 스트림)
       ↓
RenderState (이벤트 → 상태 추적)
       ↓
TerminalRenderer (상태 → ANSI 이스케이프 시퀀스)

TerminalRenderer의 핵심 구조를 보자:

pub struct TerminalRenderer {
    syntax_set: SyntaxSet,      // syntect: 1000+ 언어 문법
    syntax_theme: Theme,        // base16-ocean.dark
    color_theme: ColorTheme,    // 터미널 색상 11개
}

pub fn render_markdown(&self, markdown: &str) -> String {
    let mut output = String::new();
    let mut state = RenderState::default();
    let mut code_buffer = String::new();
    let mut in_code_block = false;

    for event in Parser::new_ext(markdown, Options::all()) {
        self.render_event(event, &mut state, &mut output,
                          &mut code_buffer, &mut code_language, &mut in_code_block);
    }
    output.trim_end().to_string()
}

Parser::new_ext(markdown, Options::all())로 CommonMark의 모든 확장(표, 각주, 취소선 등)을 활성화한다. 각 이벤트(Start(Tag::Heading), Text("hello"), End(TagEnd::Heading) 등)를 순회하면서 RenderState를 업데이트하고 ANSI 색상 코드가 포함된 문자열을 누적한다.

1.2 RenderState: 카운터 기반 중첩 추적

#[derive(Debug, Default, Clone, PartialEq, Eq)]
struct RenderState {
    emphasis: usize,           // *이탤릭* 중첩 깊이
    strong: usize,             // **볼드** 중첩 깊이
    heading_level: Option<u8>, // 현재 h1-h6
    quote: usize,              // > 인용구 중첩 깊이
    list_stack: Vec<ListKind>, // 순서/비순서 리스트 스택
    link_stack: Vec<LinkState>,
    table: Option<TableState>,
}

bool이 아니라 usize인가? 마크다운에서 ***bold italic***처럼 강조가 중첩될 수 있다. Event::Start(Tag::Emphasis)가 오면 emphasis += 1, Event::End(TagEnd::Emphasis)가 오면 emphasis -= 1. 카운터가 0이 아니면 이탤릭을 적용한다. bool이었다면 중첩된 경우 End 이벤트에서 스타일이 조기 해제되는 버그가 발생한다.

스타일 적용 로직도 우선순위가 명확하다:

fn style_text(&self, text: &str, theme: &ColorTheme) -> String {
    let mut style = text.stylize();
    if matches!(self.heading_level, Some(1 | 2)) || self.strong > 0 {
        style = style.bold();
    }
    if self.emphasis > 0 {
        style = style.italic();
    }
    if let Some(level) = self.heading_level {
        style = match level {
            1 => style.with(theme.heading),  // Cyan
            2 => style.white(),
            3 => style.with(Color::Blue),
            _ => style.with(Color::Grey),
        };
    }
    // ...
}

h1은 Cyan + Bold, h2는 White + Bold, h3은 Blue, h4+는 Grey. 제목 레벨이 내려갈수록 시각적 강도가 줄어드는 합리적인 계층 구조다.

1.3 코드 블록 구문 강조: syntect + 24-bit ANSI

pub fn highlight_code(&self, code: &str, language: &str) -> String {
    let syntax = self.syntax_set
        .find_syntax_by_token(language)
        .unwrap_or_else(|| self.syntax_set.find_syntax_plain_text());
    let mut highlighter = HighlightLines::new(syntax, &self.syntax_theme);

    for line in LinesWithEndings::from(code) {
        match highlighter.highlight_line(line, &self.syntax_set) {
            Ok(ranges) => {
                let escaped = as_24_bit_terminal_escaped(&ranges[..], false);
                colored_output.push_str(&apply_code_block_background(&escaped));
            }
            Err(_) => colored_output.push_str(&apply_code_block_background(line)),
        }
    }
    colored_output
}

두 가지 영리한 설계가 있다. 첫째, find_syntax_by_token(language)가 실패하면 plain text로 폴백한다. 알 수 없는 언어 태그가 와도 크래시하지 않는다. 둘째, apply_code_block_background()의 트릭:

fn apply_code_block_background(line: &str) -> String {
    let with_background = trimmed.replace("\u{1b}[0m", "\u{1b}[0;48;5;236m");
    format!("\u{1b}[48;5;236m{with_background}\u{1b}[0m{trailing_newline}")
}

syntect가 생성한 ANSI 리셋 코드(\e[0m)를 배경색 유지 코드(\e[0;48;5;236m)로 교체한다. 이렇게 하면 구문 강조 색상이 바뀌어도 코드 블록의 어두운 배경(48;5;236 = 짙은 회색)이 일관되게 유지된다.

코드 블록의 테두리도 유니코드 박스 드로잉 문자로 렌더링된다:

╭─ rust
│ fn main() { ... }
╰─

1.4 스트리밍 안전 경계: 코드 펜스 안에서 자르지 않는다

LLM 응답은 토큰 단위로 스트리밍되므로, 마크다운을 도착하는 대로 렌더링해야 한다. 하지만 코드 블록 중간에서 렌더링하면 파서가 혼란에 빠진다. find_stream_safe_boundary()가 이 문제를 해결한다:

fn find_stream_safe_boundary(markdown: &str) -> Option<usize> {
    let mut in_fence = false;
    let mut last_boundary = None;

    for (offset, line) in markdown.split_inclusive('\n').scan(0usize, |cursor, line| {
        let start = *cursor;
        *cursor += line.len();
        Some((start, line))
    }) {
        let trimmed = line.trim_start();
        if trimmed.starts_with("```") || trimmed.starts_with("~~~") {
            in_fence = !in_fence;
            if !in_fence {
                last_boundary = Some(offset + line.len()); // 펜스 닫힘 → 안전
            }
            continue;
        }
        if in_fence { continue; }  // 펜스 안 → 절대 자르지 않음
        if trimmed.is_empty() {
            last_boundary = Some(offset + line.len()); // 빈 줄 → 안전
        }
    }
    last_boundary
}

로직: 코드 펜스(```)가 열려 있으면 자르지 않는다. 펜스가 닫히거나 빈 줄이 나타날 때만 "안전한 경계"로 판정한다. MarkdownStreamState는 이 경계를 이용해 push() 시 안전한 지점까지만 렌더링하고 나머지는 pending 버퍼에 보존한다.

이 패턴은 3편에서 분석한 Rust SseParserwindows() 기반 프레임 경계 탐지와 같은 철학이다 — 불완전한 데이터는 처리하지 않는다.

1.5 스피너: 브라유 점자 애니메이션

impl Spinner {
    const FRAMES: [&str; 10] = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];

    pub fn tick(&mut self, label: &str, theme: &ColorTheme, out: &mut impl Write) -> io::Result<()> {
        let frame = Self::FRAMES[self.frame_index % Self::FRAMES.len()];
        self.frame_index += 1;
        queue!(out, SavePosition, MoveToColumn(0), Clear(ClearType::CurrentLine),
               SetForegroundColor(theme.spinner_active),
               Print(format!("{frame} {label}")),
               ResetColor, RestorePosition)?;
        out.flush()
    }

    pub fn finish(&mut self, label: &str, ...) {
        // ✔ (초록) + 라벨
        self.frame_index = 0;
    }

    pub fn fail(&mut self, label: &str, ...) {
        // ✘ (빨강) + 라벨
        self.frame_index = 0;
    }
}

10개의 브라유 문자가 회전하며 로딩 애니메이션을 만든다. SavePosition/RestorePosition으로 커서 위치를 보존하면서 같은 줄에서 프레임을 갱신한다. finish()fail()에서 frame_index를 0으로 리셋하는 것은 다음 스피너 사용을 위한 초기화다.

현재는 브라유 점자 하나뿐이지만, Phase 5.3에서 Bar(▉▊▋▌▍▎▏), Moon(🌑🌒🌓🌔🌕), Dots(⣾⣽⣻⢿⡿⣟⣯⣷) 등 대체 스타일이 계획되어 있다.


2. 모놀리식 REPL: 3,897줄의 main.rs

2.1 LiveCli — 모든 것을 담은 구조체

struct LiveCli {
    model: String,
    allowed_tools: Option<AllowedToolSet>,
    permission_mode: PermissionMode,
    system_prompt: Vec<String>,
    runtime: ConversationRuntime<AnthropicRuntimeClient, CliToolExecutor>,
    session: SessionHandle,
}

이 구조체 하나에 모델 설정, 권한, 시스템 프롬프트, 대화 런타임, 세션 핸들이 전부 들어있다. 그리고 이 구조체의 impl 블록 안에 다음이 모두 존재한다:

  • run_turn() — 턴 실행 (스피너 → API 호출 → 결과 표시)
  • handle_repl_command() — 23개 슬래시 커맨드 디스패치
  • run_bughunter(), run_commit(), run_pr(), run_issue() 등 개별 커맨드 핸들러
  • print_status(), print_cost() 등 정보 표시 함수
  • persist_session(), resume_session(), clear_session() 등 세션 관리

그 외에도 파일 레벨에 40개 이상의 독립 함수가 존재한다:

함수군개수예시
포맷팅15+format_status_report(), format_cost_report(), format_tool_call_start()
도구 렌더링8+format_bash_call(), format_read_result(), format_edit_result()
세션 관리4+create_managed_session_handle(), list_managed_sessions()
Git 통합3+git_output(), git_status_ok()

이것이 왜 문제인가? 단순히 "파일이 길다"가 아니다. 관심사가 분리되지 않았다. 포맷팅 함수를 수정하려면 세션 관리 코드 사이를 스크롤해야 하고, 새 슬래시 커맨드를 추가하려면 3,897줄 파일 전체를 이해해야 한다. 컴파일러도 전체 파일을 재분석해야 하므로 빌드 시간에도 영향을 준다.

2.2 슬래시 커맨드의 디스패치 패턴

fn handle_repl_command(&mut self, command: SlashCommand) -> Result<bool, Box<dyn std::error::Error>> {
    Ok(match command {
        SlashCommand::Help => { println!("{}", render_repl_help()); false }
        SlashCommand::Status => { self.print_status(); false }
        SlashCommand::Commit => { self.run_commit()?; true }  // true = 세션 저장
        SlashCommand::Model { model } => self.set_model(model)?,
        SlashCommand::Compact => { self.compact()?; false }
        SlashCommand::Unknown(name) => { eprintln!("unknown: /{name}"); false }
        // ... 17개 더
    })
}

Result<bool, _>의 반환값이 흥미롭다. true면 세션을 디스크에 저장하고, false면 건너뛴다. /commit 같은 상태 변경 커맨드는 true, /help 같은 조회 커맨드는 false. 이 패턴 자체는 깔끔하지만, 23개 브랜치가 하나의 함수에 있는 것은 확장성 문제다.

2.3 턴 실행: 스피너 → 사고 → 완료

fn run_turn(&mut self, input: &str) -> Result<(), Box<dyn std::error::Error>> {
    let mut spinner = Spinner::new();
    spinner.tick("🦀 Thinking...", &theme, &mut stdout)?;

    let result = self.runtime.run_turn(input, Some(&mut permission_prompter));

    match result {
        Ok(summary) => {
            spinner.finish("✨ Done", &theme, &mut stdout)?;
            if let Some(event) = summary.auto_compaction {
                println!("{}", format_auto_compaction_notice(event.removed_message_count));
            }
            self.persist_session()?;
        }
        Err(error) => {
            spinner.fail("❌ Request failed", &theme, &mut stdout)?;
        }
    }
}

🦀 Thinking...✨ Done / ❌ Request failed의 3-state 패턴은 단순하지만 효과적이다. 다만 현재는 스트리밍 중 실시간 피드백이 없다. 토큰이 얼마나 생성되었는지, 어떤 도구가 실행 중인지 사용자가 알 수 없다. 이것이 Phase 1(상태 바)과 Phase 2(스트리밍 강화)의 동기다.

2.4 레거시 CliApp: 제거 대상

app.rs에 398줄의 CliApp 구조체가 별도로 존재한다. LiveCli와 기능이 겹치지만 스트림 이벤트 핸들러 패턴이 다르다. 현재 사용되지 않는 것으로 보이며, Phase 0.2에서 제거 대상이다. 다만 CliApp의 스트림 이벤트 핸들링 패턴(AssistantEvent 콜백)은 LiveCli에 통합할 가치가 있다.


3. 입력 시스템: rustyline 기반 라인 에디터

pub struct LineEditor {
    prompt: String,
    editor: Editor<SlashCommandHelper, DefaultHistory>,
}

rustyline 15를 기반으로 한 라인 에디터는 세 가지 핵심 기능을 제공한다:

탭 완성 — 슬래시 커맨드 자동완성:

fn slash_command_prefix(line: &str, pos: usize) -> Option<&str> {
    if pos != line.len() { return None; }
    let prefix = &line[..pos];
    if prefix.contains(char::is_whitespace) || !prefix.starts_with('/') { return None; }
    Some(prefix)
}

커서가 줄 끝에 있고, 입력이 /로 시작하며, 공백이 없을 때만 완성을 시도한다. &line[..pos] 슬라이싱은 O(1)이므로 할당 없이 빠르다.

멀티라인 입력 — Shift+Enter 또는 Ctrl+J:

editor.bind_sequence(KeyEvent(KeyCode::Enter, Modifiers::SHIFT), Cmd::Newline);
editor.bind_sequence(KeyEvent(KeyCode::Char('J'), Modifiers::CTRL), Cmd::Newline);

비-TTY 폴백 — 파이프 입력 지원:

pub fn read_line(&mut self) -> io::Result<ReadOutcome> {
    if !io::stdin().is_terminal() || !io::stdout().is_terminal() {
        return self.read_line_fallback();
    }
    // ...
}

echo "explain this code" | claw처럼 파이프로 입력을 전달할 때 rustyline 대신 단순 stdin.read_line()으로 폴백한다. 이 방어 로직 없이는 파이프 입력 시 크래시가 발생한다.


4. 15가지 문제와 6단계 로드맵

프로젝트의 TUI-ENHANCEMENT-PLAN.md는 현재 CLI의 15가지 문제를 식별하고, 6단계 개선 계획을 제시한다. 단순한 위시리스트가 아니라 구체적인 기술 명세다.

4.1 현재의 15가지 문제

가장 영향이 큰 문제들을 분류하면:

구조적 부채 — main.rs 3,897줄 모놀리스, app.rs와의 중복, 인자 파서 미통합

정보 부족 — 상태 바/HUD 없음, 진행 표시줄 없음, "thinking" 모드 구분 없음, 토큰 카운터 미표시

렌더링 한계 — 스트리밍 8ms 인공 지연, 구문 강조가 도구 결과에만 적용, 시각적 diff 없음, 도구 출력 접기 불가

확장성 부족 — 색상 테마 하드코딩, 터미널 리사이즈 미처리, 도구 인자 자동완성 없음

4.2 Phase 0: 구조적 정리 — 모든 것의 기초

현재:
  main.rs (3,897줄 = 모든 것)

목표:
  main.rs       (~100줄, 엔트리포인트만)
  app.rs        (LiveCli, REPL, 커맨드 핸들러)
  format.rs     (40+ 포맷팅 함수)
  session_mgr.rs (세션 CRUD)
  args.rs       (인자 파싱 통합)
  tui/          (Phase 1~6 컴포넌트)

이 분해가 왜 Phase 0인가? Phase 1에서 상태 바를 추가하려면 tui/status_bar.rs가 필요하고, Phase 3에서 접기 가능한 도구 출력을 만들려면 tui/collapsible.rs가 필요하다. 모놀리스 상태에서는 이런 모듈을 추가할 자연스러운 위치가 없다. 구조 정리가 모든 후속 작업의 전제 조건이다.

4.3 Phase 1: 상태 바 — 가장 높은 UX 임팩트

› claude-opus-4-6 | DangerFullAccess | 12,345 tokens | $0.42 | main

모델명, 권한 모드, 누적 토큰, 비용, Git 브랜치를 터미널 하단에 고정 표시한다. crossterm::terminal::size()로 터미널 너비를 감지하고, 좁으면 컴팩트 형태로 전환한다.

핵심은 실시간 토큰 카운터다. StreamEvent::Usage 이벤트가 도착할 때마다 상태 바를 갱신한다. 현재는 스피너만 돌아가다가 끝나면 결과가 나타나지만, 상태 바가 있으면 "지금 얼마나 토큰을 쓰고 있는지"를 스트리밍 중에도 볼 수 있다.

4.4 Phase 2: 스트리밍 출력 강화

인공 지연 제거 — 현재 스트리밍에 8ms 인공 지연이 있다. "타이핑 효과"를 위한 것이지만, 긴 응답에서는 불필요한 속도 저하를 만든다. Phase 2.4에서 제거하거나 설정 옵션으로 전환한다.

Thinking 표시기 — Claude의 확장 사고(extended thinking) 모드에서:

🧠 Reasoning...    (펄싱 점 애니메이션)

스트리밍 진행 바 — max_tokens 대비 현재 생성된 토큰의 비율:

[████████████░░░░░░░░] 60% (19,200 / 32,000 tokens)

4.5 Phase 3: 도구 호출 시각화

접을 수 있는 도구 출력 — 현재 bash 도구가 1,000줄을 출력하면 화면을 완전히 점령한다:

🔧 bash (truncated 485 lines)
┌─ Output ─────────────────────────────────────────────┐
│ [처음 15줄 표시]                                        │
│ ...                                                  │
│ [마지막 5줄 표시]                                        │
│ [e] 펼치기 / [s] 파일 저장 / [h] 도움말                    │
└──────────────────────────────────────────────────────┘

Diff-aware edit_file — 파일 편집 결과를 통합 diff로 표시:

- old_line (빨강)
+ new_line (초록)

현재는 ✓ edit_file만 표시되어 무엇이 바뀌었는지 알 수 없다.

도구 호출 타임라인 — 여러 도구가 연속 실행될 때:

🔧 bash → ✓ | read_file → ✓ | edit_file → ✓ (3 tools, 1.2s)

4.6 Phase 4-5: 커맨드 강화와 테마

Phase 4는 /diff에 색상 적용, 긴 출력용 내장 pager(j/k/PgDn/PgUp), /search(대화 히스토리 검색), /undo(파일 편집 되돌리기), 대화형 세션 선택기 등을 추가한다.

Phase 5는 색상 테마 시스템이다. 현재 ColorTheme::default()에 하드코딩된 11개 색상을 설정 가능하게 만들고, 터미널 능력을 자동 감지한다:

COLORTERM=truecolor → 16M색 (24-bit)
TERM=*256color      → 256색
기타                 → 16색

Dark, Light, Solarized, Catppuccin, Nord, Dracula 프리셋이 계획되어 있다.

4.7 Phase 6: 전체 화면 TUI — 선택적 미래

[features]
full-tui = ["ratatui"]  # 선택적 의존성

ratatui 프레임워크 기반의 전체 화면 모드:

┌─ Conversation (scrollable) ──────────────────────────┐
│ [Turn 1] User: ...                                   │
│ [Turn 1] Assistant: ...                              │
├─ Input ──────────────────────────────────────────────┤
│ > _                                                  │
├─ Status ─────────────────────────────────────────────┤
│ claude-opus-4-6 | DangerFullAccess | 12K tokens      │
└──────────────────────────────────────────────────────┘

feature gate 뒤에 숨기는 것이 핵심이다. --features full-tui 없이 빌드하면 기존 인라인 CLI가 유지된다. ratatui 의존성을 추가해도 기본 빌드에는 영향이 없다.


5. PARITY.md: 미구현 영역과 미래 방향

5.1 현재 강점

PARITY.md가 기록한 Claw Code의 현재 완성도:

  • Anthropic API 클라이언트 + OAuth 인증
  • 세션 영속화와 복원
  • agentic 루프 기반 도구 실행
  • MCP(Model Context Protocol) stdio 통합
  • CLAUDE.md 프로젝트 파일 탐지
  • 기본 도구: bash, 파일 읽기/쓰기/편집, 검색, 웹

5.2 주요 미구현 영역

영역상태영향
플러그인 시스템완전 누락로더, 마켓플레이스, 확장 메커니즘 없음
런타임 훅설정 파싱만 구현PreToolUse/PostToolUse 실행 안 됨
스킬 레지스트리로컬 파일만번들 스킬, 팀 메모리 통합 없음
CLI 커맨드8개 누락/agents, /hooks, /mcp, /plugin
원격 전송미구현Structured IO, SSH 전송 계층 없음

플러그인 시스템의 부재가 가장 큰 갭이다. 원본 Claude Code는 플러그인 로더, 마켓플레이스 통합, 런타임 확장 메커니즘을 갖추고 있다. Claw Code는 이 전체가 없다.

런타임 훅은 설정을 파싱하는 코드는 있지만, 실제로 PreToolUse/PostToolUse 훅을 실행하는 파이프라인이 구현되지 않았다. 4편에서 분석한 HookRunner의 subprocess 프로토콜(exit code 0/2/기타)은 Rust에서 정의만 되어 있고 연결되지 않았다.

5.3 미래 방향

PARITY.md와 TUI 로드맵을 종합하면 프로젝트의 방향이 보인다:

단기 — Phase 0-1 구조 정리 + 상태 바. main.rs 모놀리스를 해체하고, 사용자 경험의 기본(토큰 표시, 진행 상태)을 확보한다.

중기 — Phase 2-3 스트리밍/도구 시각화 + 플러그인 기초. 스트리밍 출력을 완성하고, 훅 실행 파이프라인을 연결한다.

장기 — Phase 4-6 커맨드/테마/전체 화면 TUI + 플러그인 마켓플레이스. 프로덕션급 CLI 도구로 성숙시킨다.

Python 참조 구현은 점진적으로 Rust로 전환되며, 궁극적으로 Rust 바이너리가 프로덕션 배포 대상이 된다.


6. 시리즈를 마치며: Claw Code에서 배운 것

8편에 걸쳐 Claw Code의 소스를 한 줄씩 읽었다. 마지막으로 이 프로젝트 전체에서 추출할 수 있는 설계 원칙을 정리한다.

듀얼 포팅의 가치. Python으로 빠르게 구조를 검증하고, Rust로 성능과 안전성을 확보하는 전략은 대규모 재구현 프로젝트에서 효과적이다. 207개 커맨드를 전부 Rust로 먼저 작성하는 것보다, Python 스냅샷으로 카탈로그를 만들고 패리티를 추적하는 것이 현실적이다.

trait 기반 추상화의 힘. ApiClient, ToolExecutor, PermissionPrompter — 이 세 trait가 전체 런타임을 테스트 가능하고 확장 가능하게 만든다. 구체 구현을 교체해도 ConversationRuntime의 코드는 한 줄도 바뀌지 않는다.

불완전한 데이터는 처리하지 않는다. SSE 파서의 프레임 경계, 스트리밍 마크다운의 안전 경계, 지수 백오프의 산술 오버플로 검사 — 모든 곳에서 "충분하지 않으면 기다린다"는 원칙이 관철된다.

테스트는 제품 경계에서. Python은 subprocess로 CLI를 테스트하고, Rust는 mock TCP 서버로 HTTP 계층을 테스트한다. 내부 함수가 아니라 외부 계약을 검증하므로 리팩토링에 강하다.

모놀리스는 일시적이다. main.rs의 3,897줄은 "지금은 동작한다"의 산물이지만, TUI 로드맵의 존재 자체가 "이것이 영구적이지 않다"는 인식의 증거다. Phase 0에서 ~100줄로 줄이는 계획이 이미 문서화되어 있다. 기술 부채를 인식하고 해결 로드맵을 공개하는 것, 그것이 건강한 오픈소스 프로젝트의 징표다.


실습 가이드

<# render.rs의 마크다운 렌더링 테스트 실행
cd rust && cargo test --package rusty-claude-cli -- render

# main.rs의 현재 줄 수 확인
wc -l rusty-claude-cli/src/main.rs

# 전체 Rust 워크스페이스 빌드 (TUI 포함)
cargo build --workspace

# Phase 0 시도: main.rs에서 포맷팅 함수 grep
grep -n "^fn format_" rusty-claude-cli/src/main.rs

# crossterm으로 간단한 상태 바 프로토타입
# (별도 프로젝트에서)
cargo new status-bar-demo && cd status-bar-demo
cargo add crossterm

시리즈 목차
1편: 프로젝트 배경과 아키텍처 오버뷰
2편: Rust 워크스페이스와 크레이트 구조
3편: API 통신과 SSE 스트리밍
4편: 대화 런타임과 세션 관리
5편: 도구 시스템과 권한 모델
6편: Python 포팅 워크스페이스 분석
7편: 테스팅 전략과 패리티 추적
8편: TUI 개선 로드맵과 미래 방향 ← 현재 글 (최종편)

#ClawCode #ClaudeCode #TUI #Rust #터미널렌더링 #pulldown_cmark #syntect #코드분석 #시리즈완결

profile
꿈꾸는 개발자

0개의 댓글