프로그레스 바는 API가 아니다

CLI가 쓸 만해지면, 언젠가 그 CLI는 사람이 직접 실행하는 도구만은 아니게 됩니다.
누군가는 스크립트 안에 넣습니다. 누군가는 제품 workflow 안에서 호출합니다. 그러면 다른 시스템이 묻기 시작합니다.
"지금 무슨 일이 일어나고 있지?"
이 지점에서 progress bar가 문제가 될 수 있습니다.
사람에게는 이런 출력이면 충분합니다.
Translating markdown files 12/40 30% docs/intro.md
명령이 살아 있는지 알 수 있습니다. 얼마나 진행됐는지도 보입니다. 지금 어떤 파일을 처리 중인지도 바로 보입니다.
하지만 다른 프로그램이 이 문장에서 product state를 만들기 시작하면 이야기가 달라집니다. Progress bar는 더 이상 단순한 UI가 아닙니다. 의도하지 않은 API가 됩니다.
Co-op Translator v0.20.0에서 다룬 문제도 이 경계에 가까웠습니다. 이번 버전에서는 사람이 보기 좋은 Rich 기반 progress UI를 넣었고, 동시에 시스템이 읽을 수 있는 structured translation event도 넣었습니다.
실제 문제는 progress를 어떻게 출력할지가 아니었습니다. 사람이 보는 출력과 시스템이 의존하는 상태가 하나의 불안정한 표면으로 합쳐지지 않게 만드는 일이었습니다.
이 글에서는 세 가지를 차례로 보겠습니다.
첫째, 왜 console output을 파싱하고 싶어지는지.
둘째, 왜 더 좋은 UI가 automation에는 breaking change가 될 수 있는지.
셋째, 하나의 event contract가 CLI, Python API, MCP, 그리고 Localizeflow 같은 product integration을 어떻게 함께 지탱하는지.
먼저 단순한 버전부터 보기
Console output은 가장 만들기 쉬운 progress interface입니다.
Translation command를 실행할 때 제가 빠르게 알고 싶은 것은 보통 이런 것들입니다.
- 명령이 아직 실행 중인가
- 지금 어떤 stage인가
- 어떤 파일을 처리 중인가
- 얼마나 남았는가
- 실패한 작업이 있는가
Progress bar는 이 일에 잘 맞습니다. 실행 상태를 사람이 빠르게 훑어볼 수 있는 형태로 압축해 줍니다.
긴 translation job에서 아무 피드백도 주지 않는 CLI는 실제로는 동작 중이어도 멈춘 것처럼 보입니다. 그래서 사람을 위한 progress UI는 필요합니다.
문제는 그 텍스트가 상태가 존재하는 유일한 장소가 될 때 시작됩니다.
예를 들어 Localizeflow가 더 큰 workflow 안에서 Co-op Translator를 실행한다고 생각해 보겠습니다. Localizeflow에는 터미널에 찍힌 문장보다 더 안정적인 사실이 필요합니다.
- 어떤 translation job이 시작됐는가
- 어떤 target language가 active인가
- 어떤 stage가 실행 중인가
- 어떤 파일이 완료됐는가
- 어떤 파일이 실패했는가
- 전체 중 몇 개가 끝났는가
- 실행이 성공했는가
이 사실들이 모두 display text 안에만 있으면, Localizeflow는 사람이 읽는 문장을 다시 파싱해서 machine state를 복원해야 합니다.
그건 안정적인 contract가 아닙니다. 추측에 가깝고, 다른 제품이 그 추측에 의존하기 시작하면 비용이 커집니다.
더 좋은 UI도 automation을 깨뜨릴 수 있다
이 문제에서 불편한 부분은 CLI가 좋아졌는데도, 그 위에 만든 automation은 깨질 수 있다는 점입니다.
어떤 integration이 다음 문자열을 찾고 있다고 해보겠습니다.
Translating markdown files: 12/40
나중에 CLI를 더 읽기 좋게 바꿉니다. 같은 정보가 Rich table, progress bar, status panel, 또는 더 짧은 label 안으로 이동합니다.
명령을 지켜보는 사람에게는 분명한 개선일 수 있습니다.
하지만 parser에게는 실패일 수 있습니다.
작은 문구 변경도 충분합니다.
Retranslating outdated markdowns
나중에 이 label을 조금 더 명확하게 바꿉니다.
Retranslating outdated markdown files
이 변경은 원래는 안전해야 합니다. Display text를 더 이해하기 좋게 다듬은 것뿐입니다.
하지만 외부 시스템이 그 정확한 문장에 의존하고 있었다면, UI 문구 수정이 integration breaking change가 됩니다.
이걸 보고 나면 해결 방향이 달라집니다. Progress bar를 더 잘 파싱하게 만드는 것이 답이 아닙니다. Progress bar에게 두 가지 일을 시키지 않는 것이 답입니다.
상태는 한 번 만들고, 표면은 둘로 나누기
v0.20.0에서는 progress 정보를 한 번 만든 뒤 두 표면으로 보냅니다.
첫 번째 표면은 사람을 위한 renderer입니다.
CLI에서는 Rich가 translation state를 header, table, progress bar, status line으로 보여줍니다. 이 출력은 읽기 좋아야 합니다. 그리고 더 나은 표시 방식이 생기면 바뀔 수 있어야 합니다.
두 번째 표면은 시스템을 위한 event stream입니다.
외부 integration은 console output을 긁어 오지 않아야 합니다. 대신 stable schema를 가진 event를 읽어야 합니다.
같은 진행 상황을 데이터로 표현하면 이렇게 됩니다.
{
"schema": "co-op.translation.event.v1",
"type": "stage_progress",
"stage_key": "translating_markdown_files",
"stage_label": "Translating markdown files",
"language": "ko",
"current_path": "docs/intro.md",
"completed": 12,
"total": 40,
"progress": 30
}
Progress bar와 event는 같은 내부 상태에서 나올 수 있습니다. 다만 읽는 대상이 다릅니다.
UI는 사람이 이해하기 쉬운 label과 layout을 가집니다. Event는 다른 시스템이 저장하고 비교하고 다시 재생할 수 있는 stable field를 가집니다.
Label은 설명하고, key는 약속한다
여기서 작은 설계 결정 하나가 중요합니다. stage_label과 stage_key를 분리하는 것입니다.
stage_label은 사람이 보는 문구입니다.
{
"stage_label": "Translating markdown files"
}
이 문구는 CLI를 더 명확하게 만들기 위해 나중에 바뀔 수 있습니다.
stage_key는 integration이 의존해야 하는 값입니다.
{
"stage_key": "translating_markdown_files"
}
다른 시스템은 이 값을 기준으로 동작해야 합니다.
이 분리는 양쪽을 모두 지켜 줍니다. CLI 문구는 계속 다듬을 수 있고, integration은 모든 문구 변경을 위험으로 받아들이지 않아도 됩니다.
Progress는 percentage가 아니라 workflow다
유용한 event stream은 숫자만 전달해서는 부족합니다.
Progress는 workflow가 여러 상태를 지나며 움직이는 과정입니다.
Co-op Translator에서는 이런 흐름을 event로 표현할 수 있습니다.
run_started
estimate_ready
stage_started
stage_progress
file_completed
run_completed
경고와 실패는 별도의 event type으로 표현할 수 있습니다.
warning
file_failed
run_failed
이렇게 나누면 integration code가 단순해집니다.
Dashboard는 run_started를 받으면 job을 만들 수 있습니다. estimate_ready를 받으면 token estimate를 보여줄 수 있습니다. stage_progress를 받으면 화면의 progress를 갱신할 수 있습니다. run_completed를 받으면 job을 완료 상태로 바꿀 수 있습니다.
Dashboard는 Co-op Translator의 terminal sentence를 이해할 필요가 없습니다.
Event contract만 이해하면 됩니다.
하나의 contract가 여러 interface를 지탱한다
Co-op Translator는 CLI만 제공하는 도구가 아닙니다.
Python API와 MCP server도 함께 제공합니다. 그래서 progress contract는 특정 terminal renderer에 묶이면 안 됩니다.
CLI에서는 사람이 Rich progress UI를 볼 수 있습니다. 다른 시스템이 machine-readable output을 필요로 한다면 NDJSON event를 쓸 수 있습니다.
translate -l "ko ja" -md --json-events progress.ndjson
Python API에서는 callback으로 같은 종류의 event를 받을 수 있습니다.
from co_op_translator.api import run_translation
def on_event(event):
record_translation_event(job_id, event.to_dict())
run_translation(
language_codes="ko ja",
root_dir=".",
markdown=True,
progress_callback=on_event,
)
MCP에서는 run_translation 결과 payload에 event를 담을 수 있습니다. Agent와 host application은 terminal output을 긁어 오지 않고도 어떤 일이 일어났는지 이해할 수 있습니다.
Interface는 다릅니다. 하지만 contract는 같습니다.
- CLI: Rich renderer와 NDJSON event file
- Python API:
progress_callback - MCP: tool result의 event payload
Display와 event를 분리하는 실용적인 이유가 여기에 있습니다. 화면은 계속 좋아질 수 있고, API와 MCP integration은 흔들리지 않습니다.
Localizeflow 관점에서 달라지는 점
Localizeflow 입장에서 보면 차이는 더 구체적입니다.
약한 방식에서는 Localizeflow가 문장을 받아 의미를 추출해야 합니다.
Done: Translated README.md to Korean.
이 문장에서 파일명, 언어, 완료 상태를 거꾸로 추론해야 합니다.
Structured event가 있으면 사실을 바로 저장할 수 있습니다.
{
"type": "file_completed",
"language": "ko",
"current_path": "README.md"
}
이 event는 database에 append하기 쉽습니다. 현재 job status는 event stream에서 materialize할 수 있습니다. Log는 여전히 남아도 됩니다. 다만 product state의 source of truth가 될 필요는 없습니다.
이 경계가 더 건강합니다. 사람은 실행을 조사할 때 log를 읽고, 제품은 workflow를 관리할 때 event에 의존합니다.
Rich는 여전히 중요하다
Event와 UI를 분리한다고 해서 UI가 덜 중요해지는 것은 아닙니다.
오히려 UI를 더 편하게 개선할 수 있습니다.
사람이 보는 CLI는 명확해야 합니다. 실행 중인 command, target language, estimate, current stage, current file, failure를 한눈에 볼 수 있어야 합니다.
Rich는 이런 CLI를 만들기에 좋습니다.
- Header로 실행 정보를 묶을 수 있음
- Table로 estimate와 stage progress를 정리할 수 있음
- Progress bar로 긴 작업을 따라가기 쉽게 만들 수 있음
- GitHub Actions log에서도 비교적 읽기 좋은 출력을 만들 수 있음
하지만 Rich output은 사용자가 더 잘 이해할 수 있다면 계속 바뀔 수 있어야 합니다.
이 설계에서는 Rich가 사람이 보는 화면을 렌더링합니다. Versioned event schema는 machine-readable state를 전달합니다.
두 표면은 같은 실행에서 나오지만, 서로 다른 독자에게 책임을 집니다.
원칙
이번 작업에서 남은 기준은 단순합니다.
Logs are for humans. Events are for systems.
사람은 문장을 읽고 맥락을 이해할 수 있습니다. 작은 문구 변경도 보통은 문제가 되지 않습니다.
하지만 시스템에는 stable key, schema version, event type, typed field가 필요합니다.
그래서 progress output을 설계할 때 첫 질문은 이것이 아니어야 합니다.
이 텍스트를 어떻게 더 잘 파싱하게 만들까?
더 좋은 질문은 이것입니다.
누가 이 출력에 의존하게 될까?
사람이 읽는다면 UI를 명확하게 만들면 됩니다.
다른 시스템이 의존한다면 versioned structured event를 제공해야 합니다.
Progress bar는 여전히 중요합니다. 오래 걸리는 CLI가 살아 있다는 느낌을 줍니다.
다만 API가 되어서는 안 됩니다.