Rich로 번역 CLI Progress UI 설계하기

번역 CLI에서 가장 불편한 순간은 명령이 오래 걸릴 때가 아닙니다.

더 불편한 순간은 명령이 오래 걸리는데, 지금 무엇을 하고 있는지 사용자가 다시 추측해야 할 때입니다.

Co-op Translator는 Markdown, notebook, image text를 여러 언어로 번역합니다. 작은 README 하나를 번역할 때는 간단한 bar나 Done message만으로도 충분합니다. 하지만 repository 전체를 번역하거나, 오래된 번역만 다시 처리하거나, token estimate를 함께 보여줘야 하면 progress output이 단순한 장식이 아니라 작업을 이해하는 화면이 됩니다.

Co-op Translator v0.20.0에서 Rich 기반 CLI progress UI를 넣은 이유도 여기에 있었습니다. 목표는 터미널을 화려하게 만드는 것이 아니라, 사용자가 다음으로 궁금해할 정보를 미리 배치하는 것이었습니다.

이 글에서는 세 가지를 다룹니다.

첫째, 왜 기존 emoji/bar 중심 출력이 커지는 workflow를 설명하기 어려워졌는지.

둘째, Rich header, table, progress를 어떤 질문에 맞춰 배치했는지.

셋째, GitHub Actions와 log 환경에서도 깨지지 않게 하려면 왜 plain fallback이 필요했는지.

퍼센트만으로는 부족했다

Progress UI를 만들 때 가장 먼저 떠올리는 값은 percentage입니다.

30%

하지만 번역 workflow에서 percentage는 답의 일부일 뿐입니다.

사용자는 보통 더 구체적인 질문을 합니다.

  • 어떤 command를 실행했는가
  • 어떤 mode로 실행 중인가
  • target language는 무엇인가
  • 전체 작업량은 어느 정도인가
  • 현재 어떤 stage인가
  • 지금 어떤 파일을 처리하고 있는가
  • 끝난 뒤 무엇이 만들어졌는가

이 질문들이 한 화면에서 자연스럽게 답을 얻지 못하면 사용자는 터미널 출력을 다시 읽기 시작합니다. 명령어를 위로 스크롤해서 확인하고, file path를 찾고, 이 progress가 Markdown인지 notebook인지 추측하게 됩니다.

좋은 progress UI는 이런 재계산을 줄여야 합니다.

그래서 이번 작업의 기준은 "bar를 더 예쁘게 만들자"가 아니었습니다. 기준은 "사용자가 지금 어디에 있는지 다시 계산하지 않게 만들자"였습니다.

기존 출력은 빠르게 시작하기에는 좋았다

처음에는 간단한 출력이 더 낫습니다.

작은 CLI에서는 emoji, 짧은 status line, 단순한 bar가 빠르고 직관적입니다. 구현도 작고, 사용자도 바로 이해합니다.

문제는 workflow가 커진 뒤에도 같은 출력 방식이 계속 남아 있을 때 생깁니다.

Co-op Translator의 translation command는 시간이 지나면서 더 많은 mode를 갖게 되었습니다.

  • Markdown translation
  • Notebook translation
  • Image text translation
  • README-only translation
  • Outdated translation refresh
  • Link migration
  • Evaluation 기반 재번역

각 mode는 사용자에게 다른 정보를 보여줘야 합니다. 예를 들어 Markdown 번역에서는 현재 source file이 중요하고, image translation에서는 vision provider 설정이 중요합니다. 재번역에서는 새 파일 개수보다 outdated file 개수가 더 중요할 수 있습니다.

단순한 progress bar 하나에 모든 정보를 붙이면 출력이 길어지고, 반대로 정보를 줄이면 사용자가 현재 상태를 잃습니다.

그래서 progress output을 하나의 line이 아니라 작은 interface로 보기 시작했습니다.

Rich를 선택한 이유

Rich를 선택한 이유는 색을 넣기 위해서만은 아니었습니다.

Rich가 유용했던 점은 terminal output을 작은 UI component처럼 구성할 수 있다는 데 있었습니다.

  • Panel로 command context를 묶을 수 있음
  • Table로 estimate와 summary를 정렬할 수 있음
  • Progress로 stage, count, percentage, current file을 같은 row에서 보여줄 수 있음
  • terminal 여부에 따라 rich output과 plain output을 나눌 수 있음

이 조합 덕분에 progress output을 "한 줄짜리 message"가 아니라 "사용자가 현재 작업을 이해하는 화면"으로 설계할 수 있었습니다.

하지만 Rich를 도입한다고 해서 모든 것을 Rich에 묶으면 안 됩니다.

실제 구현에서는 ProgressReporter라는 공통 레이어를 두고, 번역 로직은 Rich를 직접 알지 않게 했습니다. Translation code는 file_started, file_completed, update 같은 progress operation을 호출합니다. 그 아래에서 reporter가 interactive terminal이면 Rich로 렌더링하고, CI나 non-interactive 환경이면 plain text로 내려갑니다.

이 경계가 있어야 CLI UX를 개선하면서도 core translation flow를 UI library에 의존시키지 않을 수 있습니다.

화면은 네 개의 질문으로 나눴다

최종 화면은 크게 네 부분으로 나눴습니다.

첫 번째는 command header입니다.

여기서는 command, version, mode, target language, root directory를 보여줍니다. 사용자가 스크린샷을 보거나 오래된 log를 다시 볼 때도 "이게 어떤 실행이었는지" 바로 알 수 있어야 하기 때문입니다.

두 번째는 estimate summary입니다.

번역은 비용과 시간이 있는 작업입니다. 그래서 token estimate와 estimated words는 progress bar보다 먼저 나오는 것이 자연스럽습니다. 사용자는 진행률을 보기 전에 작업 규모를 알고 싶어 합니다.

세 번째는 live progress table입니다.

여기서는 stage, progress count, percentage, current file을 한 row에 둡니다. 이 부분은 사용자가 실행 중인 명령을 계속 바라볼 때 가장 자주 보는 영역입니다.

네 번째는 completion line입니다.

작업이 끝난 뒤에는 live progress보다 결과가 중요합니다. 어떤 파일이 번역됐고, 어디에 기록됐는지를 짧은 Done line으로 남깁니다.

실제 화면은 이런 형태입니다.

Rich CLI progress showing command context, estimate, progress, and completion lines

이 화면에서 각 영역은 서로 다른 질문에 답합니다.

  • Header: 내가 무엇을 실행했는가
  • Estimate: 이 작업의 규모는 어느 정도인가
  • Progress: 지금 어디까지 왔는가
  • Done lines: 무엇이 만들어졌는가

이 순서가 중요했습니다. 사용자는 먼저 context를 잡고, 그 다음 작업량을 보고, 그 다음 움직이는 progress를 확인하고, 마지막으로 결과를 확인합니다.

CI와 log에서는 다른 답이 필요했다

좋은 terminal UI가 항상 좋은 log output은 아닙니다.

Interactive terminal에서는 Rich progress bar가 유용합니다. 하지만 GitHub Actions나 redirect된 log에서는 live progress가 오히려 지저분해질 수 있습니다. ANSI control sequence가 남거나, 같은 progress line이 여러 번 찍히거나, table이 좁은 log viewer에서 읽기 어려울 수 있습니다.

그래서 output style은 환경에 따라 달라져야 했습니다.

Co-op Translator는 기본적으로 interactive terminal이면 Rich output을 사용하고, CI나 non-interactive 환경에서는 plain text로 fallback합니다.

필요하면 환경 변수로도 제어할 수 있습니다.

CO_OP_TRANSLATOR_OUTPUT_STYLE=plain
CO_OP_TRANSLATOR_OUTPUT_STYLE=rich
CO_OP_TRANSLATOR_NO_PROGRESS=1

plain은 사람이 읽을 수 있는 단순 text output을 강제합니다. rich는 Rich output을 강제합니다. CO_OP_TRANSLATOR_NO_PROGRESS=1은 summary는 유지하되 live progress bar를 끕니다.

이 옵션이 중요한 이유는 환경마다 좋은 출력의 기준이 다르기 때문입니다.

로컬 terminal에서는 progress가 움직이는 것이 좋습니다. CI에서는 나중에 검색 가능한 log가 더 중요합니다. File log에서는 색이나 box drawing보다 plain text가 더 안전합니다.

같은 상태를 보여주더라도, 어디에 출력되는지에 따라 표현은 달라져야 합니다.

예쁜 출력과 안정적인 통합은 다른 문제다

Rich UI를 만들면서 조심해야 했던 점이 하나 더 있었습니다.

예쁜 CLI output이 외부 integration의 contract가 되면 안 됩니다.

이 부분은 별도 글인 프로그레스 바는 API가 아니다에서 더 자세히 다뤘습니다. 핵심만 말하면, 사람이 보는 progress output과 시스템이 읽는 structured event는 분리되어야 합니다.

이 글의 관점에서 중요한 점은 Rich가 "사람이 보는 renderer"라는 것입니다.

Localizeflow나 다른 automation이 progress state를 읽어야 한다면 Rich table을 파싱해서는 안 됩니다. 그런 경우에는 --json-events나 API callback, MCP event payload처럼 structured event를 사용해야 합니다.

이렇게 나눠야 UI를 자유롭게 개선할 수 있습니다.

Header 문구를 바꾸거나, table layout을 조정하거나, color를 바꾸는 일이 integration breaking change가 되면 CLI UX를 개선하기 어려워집니다. Rich는 사용자 경험을 위한 표면이고, structured event는 시스템 통합을 위한 표면입니다.

배운 점

이번 작업을 하면서 progress UI를 조금 다르게 보게 되었습니다.

Progress UI는 단순히 오래 걸리는 명령에 bar를 붙이는 일이 아니었습니다.

사용자는 명령이 실행되는 동안 계속 작은 질문을 합니다.

지금 살아 있나?

어떤 단계인가?

무엇을 처리 중인가?

얼마나 남았나?

끝나면 무엇이 생기나?

좋은 CLI progress UI는 이 질문들이 나오기 전에 먼저 답합니다.

Rich는 그 답을 더 읽기 좋게 배치하는 데 도움이 되었습니다. 하지만 더 중요한 것은 어떤 정보를 어떤 순서로 보여줄지 결정하는 일이었습니다.

제가 이번 작업에서 가져간 원칙은 이것입니다.

좋은 progress UI는 명령을 꾸미는 것이 아니라, 사용자가 다음으로 물을 질문에 먼저 답하는 것이다.

CLI가 커질수록 progress output은 작은 제품 화면이 됩니다. 그 화면을 설계할 때는 percentage보다 먼저 사용자의 방향 감각을 생각해야 합니다.

댓글