비용 추적과 MCP 텔레메트리 — 내가 얼마를 쓰고 있는지 알기


Written by Thierry K (human) · AI-assisted
Claude Code 비용 추적 대시보드 — CostOverview 카드와 MCP 텔레메트리 화면
CostOverview 카드 — Real Spend, If All API, MAX Savings 세 가지 관점으로 비용을 보여줘요.

비용 추적이 필요한 이유

Claude Code 비용 추적은 대시보드를 만들면서 가장 먼저 넣고 싶었던 기능이에요. EP1에서 다뤘던 비용 블랙박스 문제를 이제 실제로 풀어볼게요. Claude MAX $200/월을 내고 있지만, Gemini·xAI·Qwen API 호출마다 별도 과금이 발생하고, 7개 MCP 서버가 매일 수백 건의 도구 호출을 실행해요. 이 모든 비용을 한 화면에서 보고 싶었어요.

Claude MAX vs 순수 API — 가격 구조 비교

비용 추적 대시보드를 설계하려면 먼저 가격 구조를 이해해야 해요. Claude MAX($200/월)는 Opus·Sonnet 무제한 사용이지만, 다른 프로바이더는 토큰당 과금이에요.

프로바이더 모델 Input ($/1M) Output ($/1M) 출처
Anthropic Claude Opus 4 $15.00 $75.00 Anthropic Docs
Anthropic Claude Sonnet 4 $3.00 $15.00 Anthropic Docs
Google Gemini 2.5 Pro $1.25 $10.00 Google AI Pricing
xAI Grok 3 $3.00 $15.00 xAI Docs
Alibaba Qwen-Max $1.60 $6.40 Qwen Platform

이 요금표가 대시보드의 pricing.json에 그대로 들어가요. Opus input/output, Sonnet input/output 등 모델별 단가를 이 파일 한 곳에서 관리해요. Claude 토큰은 MAX 구독으로 $0이지만, “만약 API로 쓰고 있었다면?”을 계산하려면 이 단가가 필요해요. Gemini·xAI·Qwen은 pricing.json이 아니라 각 프로바이더의 빌링 API를 직접 폴링해서 실제 청구액을 가져오는 별도 Collector가 담당해요. 이 발상이 CostOverview 카드의 세 번째 지표 — MAX Savings로 이어졌어요.

CostOverview 카드 — Claude Code 비용 추적의 핵심

비용 추적 대시보드의 메인 화면 상단에 세 장의 카드가 나란히 놓여 있어요. Claude Code 비용 추적에서 가장 중요한 세 가지 질문에 각각 답해줘요.

카드 질문 계산
Real Spend 실제로 얼마를 냈나? Gemini + xAI + Qwen + MCP 비용
If All API 전부 API였다면? Claude 추정 + Real Spend
MAX Savings MAX로 얼마나 절약했나? Claude 추정 비용 = 절감액
CostOverview 데이터 플로우 — token_daily에서 프론트엔드까지
CostOverview 데이터 플로우 — SQLite에서 API를 거쳐 프론트엔드 카드까지.

프론트엔드 코드를 볼게요. CostOverview.tsx에서 세 가지 지표를 계산하는 핵심 로직이에요.

// CostOverview.tsx — 비용 계산 핵심
const providers = stats.by_provider ?? []
const realCostByProvider = providers
  .filter((p) =>
    ["gemini","xai","qwen"].includes(p.provider)
  )
  .reduce((sum, p) => sum + p.cost_usd, 0)

const mcpCost = mcpStats
  .reduce((sum, s) => sum + s.total_cost_usd, 0)

// Claude 토큰을 API 단가로 환산한 추정치
const claudeProvider =
  providers.find((p) => p.provider === "claude")
const claudeEstimated =
  claudeProvider?.cost_usd ?? 0
const liveEstimated = stats.live?.cost_usd ?? 0
const totalClaudeEstimated =
  claudeEstimated + liveEstimated

// 세 가지 지표
const totalRealSpend =
  realCostByProvider + mcpCost
const totalIfApi =
  totalClaudeEstimated + totalRealSpend
const maxSavings = totalClaudeEstimated

totalRealSpend는 실제 결제가 발생하는 금액만 합산해요. totalIfApi는 여기에 Claude 추정 비용을 더한 “가상 청구서”이고, maxSavings는 MAX 구독으로 아낀 금액 그 자체예요. 간결한 뺄셈 한 줄로 $200/월의 가치를 실감할 수 있어요.

token_daily — SQLite UPSERT 패턴

모든 비용 데이터는 SQLite 한 파일에 저장돼요. 외부 DB 서버 없이 단일 바이너리로 운영할 수 있는 게 개인 대시보드에 딱 맞아요.

CREATE TABLE IF NOT EXISTS token_daily (
  date           TEXT    NOT NULL,
  host           TEXT    NOT NULL DEFAULT '',
  model          TEXT    NOT NULL DEFAULT '',
  provider       TEXT    NOT NULL DEFAULT 'claude',
  input_tokens   INTEGER NOT NULL DEFAULT 0,
  output_tokens  INTEGER NOT NULL DEFAULT 0,
  cache_read     INTEGER NOT NULL DEFAULT 0,
  cache_creation INTEGER NOT NULL DEFAULT 0,
  session_count  INTEGER NOT NULL DEFAULT 0,
  cost_usd       REAL    NOT NULL DEFAULT 0.0,
  estimated      INTEGER NOT NULL DEFAULT 0,
  PRIMARY KEY (date, host, model, provider)
);

PK가 (date, host, model, provider) 4개 컬럼이에요. 같은 날 같은 호스트에서 같은 모델을 여러 세션에서 쓰더라도, UPSERT로 한 행에 누적돼요.

INSERT INTO token_daily
  (date, host, model, provider,
   input_tokens, output_tokens,
   cache_read, cache_creation,
   session_count, cost_usd, estimated)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(date, host, model, provider)
DO UPDATE SET
  input_tokens  = input_tokens
                + excluded.input_tokens,
  output_tokens = output_tokens
                + excluded.output_tokens,
  session_count = session_count
                + excluded.session_count,
  cost_usd      = cost_usd
                + excluded.cost_usd;

ON CONFLICT ... DO UPDATE가 핵심이에요. Claude 세션이 종료될 때마다 이 쿼리가 실행되고, excluded 키워드로 새 값을 기존 값에 더해요. EP2에서 설계한 FastAPI 백엔드가 이 UPSERT를 비동기로 처리해요.

Provider Cost Collectors — 외부 비용 수집

Claude 토큰은 세션 종료 시 자동 집계되지만, Gemini·xAI·Qwen은 각 프로바이더의 관리 API를 폴링해서 가져와야 해요. 세 개의 비동기 Collector가 백그라운드에서 10분마다 돌아요.

# gemini_collector.py — Google Cloud Monitoring
async def collect(
    self, since: datetime | None = None
) -> list[dict]:
    end = datetime.now(timezone.utc)
    start = since or (end - timedelta(days=1))
    points = await self._fetch_metric(
        METRIC_INPUT_TOKENS, start, end
    )
    return self._aggregate_to_daily(points)

Gemini Collector는 generate_content_paid_tier_input_token_count 메트릭을 읽어서 일별 최대값으로 집계해요. 출력 토큰은 입력의 1.5배로 추정하는데, Google이 출력 메트릭을 별도 제공하지 않아서 이 비율을 사용해요.

xAI Collector는 Management API의 /v1/billing/teams/{team_id}/usage 엔드포인트에서 실제 USD 금액을 바로 가져와요. 토큰 수는 단가표를 역산해서 추정하는 방식 — 비용의 60%를 입력, 40%를 출력으로 분배해요.

Qwen Collector는 DashScope API를 호출하지만, API 키가 없으면 조용히 건너뛰는 graceful degradation 패턴이에요. cost_usd=0.0으로 저장해서 토큰 수만 기록해요.

MCP 텔레메트리 — Fire-and-Forget 계측

비용 추적의 또 다른 축은 MCP 서버 텔레메트리예요. 7개 MCP 서버(nanobanana, terryxai, terryvideogen, terrychadesign, terry-research, terry-bookstack, knowledge-rag)가 도구 호출마다 대시보드로 이벤트를 쏴요.

MCP 텔레메트리 흐름 — 7개 서버에서 대시보드까지
7개 MCP 서버 → /api/mcp-event → mcp_events → mcp_daily 집계 흐름.

핵심 원칙은 “절대 블로킹하지 않는다”예요. MCP 서버의 본래 기능(이미지 생성, 검색 등)이 텔레메트리 때문에 느려지면 안 되거든요.

# mcp_telemetry_template.py
async def report_tool_call(
    server: str,
    tool: str,
    duration_ms: int = 0,
    estimated_cost_usd: float = 0.0,
    success: bool = True,
    error: str | None = None,
    caller: str = "",
) -> None:
    """Fire-and-forget POST. NEVER blocks."""
    client = _get_client()
    if client is None:
        return

    payload = {
        "event": "mcp_tool_call",
        "timestamp": datetime.now(
            timezone.utc
        ).isoformat(),
        "server": server,
        "tool": tool,
        "host": HOST_NAME,
        "caller": caller,
        "duration_ms": duration_ms,
        "estimated_cost_usd": estimated_cost_usd,
        "success": success,
        "error": error,
    }

    try:
        await client.post(
            DASHBOARD_URL, json=payload
        )
    except Exception:
        pass  # 텔레메트리 실패는 무시

이 함수를 모든 MCP 서버에 심어두면 끝이에요. 환경 변수 DASHBOARD_TELEMETRY_URL(기본값 http://localhost:8420/api/mcp-event)만 설정하면 자동으로 계측이 시작돼요.

OpenClaw 브리지도 같은 패턴이에요. Mac mini의 launchd 에이전트(com.claudie.openclaw-bridge)가 10초마다 OpenClaw CLI를 폴링해서 세션 목록을 가져오고, WebSocket으로 대시보드에 전달해요. 멀티 호스트 뷰에서 Windows·MacBook·Mac mini 세 대의 에이전트 상태를 한 화면에 모을 수 있는 이유예요.

mcp_events와 mcp_daily — 이중 집계 구조

대시보드 백엔드는 MCP 이벤트를 두 단계로 저장해요.

-- 원시 이벤트 (디버깅·감사용)
CREATE TABLE IF NOT EXISTS mcp_events (
  id                INTEGER PRIMARY KEY
                    AUTOINCREMENT,
  timestamp         TEXT    NOT NULL,
  server            TEXT    NOT NULL,
  tool              TEXT    NOT NULL,
  host              TEXT    NOT NULL DEFAULT '',
  caller            TEXT    NOT NULL DEFAULT '',
  duration_ms       INTEGER NOT NULL DEFAULT 0,
  estimated_cost_usd REAL   NOT NULL DEFAULT 0.0,
  success           INTEGER NOT NULL DEFAULT 1,
  error             TEXT,
  created_at        TEXT    NOT NULL
                    DEFAULT (datetime('now'))
);
-- 일별 집계 (대시보드 표시용)
CREATE TABLE IF NOT EXISTS mcp_daily (
  date           TEXT NOT NULL,
  server         TEXT NOT NULL,
  tool           TEXT NOT NULL,
  call_count     INTEGER NOT NULL DEFAULT 0,
  total_cost_usd REAL    NOT NULL DEFAULT 0.0,
  avg_duration_ms REAL   NOT NULL DEFAULT 0.0,
  error_count    INTEGER NOT NULL DEFAULT 0,
  PRIMARY KEY (date, server, tool)
);

mcp_daily의 UPSERT에서 평균 응답 시간을 점진적 이동 평균으로 갱신하는 패턴이 재미있어요.

ON CONFLICT (date, server, tool) DO UPDATE SET
  call_count     = call_count + 1,
  total_cost_usd = total_cost_usd
                 + excluded.total_cost_usd,
  avg_duration_ms =
    (avg_duration_ms * call_count
     + excluded.avg_duration_ms)
    / (call_count + 1),
  error_count    = error_count
                 + excluded.error_count;

기존 평균에 기존 건수를 곱해 합계를 복원하고, 새 값을 더한 뒤 (call_count + 1)로 나눠요. 별도 합계 컬럼 없이 평균만으로 점진 갱신이 가능한 패턴이에요.

비용 추적 4컷 만화 — 클로디가 비용 대시보드를 설명하는 장면
클로디의 비용 추적 일지 — “오빠, 이번 달 MAX로 $847 아꼈어요!”

백엔드 API — /api/mcp-event 엔드포인트

텔레메트리 수신 엔드포인트는 의도적으로 단순해요. 실패해도 200 OK를 반환하고, MCP 서버 쪽에서는 응답을 기다리지 않아요.

# mcp_endpoint.py
@router.post("/api/mcp-event")
async def receive_mcp_event(
    event: dict,
) -> dict:
    """Receive MCP tool call telemetry."""
    if _db:
        try:
            await _db.save_mcp_event(event)
        except Exception as exc:
            logger.warning(
                "MCP event save failed: %s", exc
            )
    return {"status": "ok"}

EP3에서 다룬 인증 시스템과 달리, 이 엔드포인트는 인증을 걸지 않았어요. 내부 네트워크(Tailscale) 안에서만 접근 가능하고, 텔레메트리 데이터에 민감 정보가 없으니까요.

프론트엔드 시각화 — Framer Motion 애니메이션

CostOverview 카드는 Framer Motion으로 숫자가 카운트업되는 애니메이션을 넣었어요. 단순한 텍스트보다 시각적 임팩트가 커요.

세 카드를 나란히 배치하면서 각각 다른 색상 강조를 줬어요. Real Spend는 빨간색(실제 지출 경고), If All API는 노란색(가상 시나리오), MAX Savings는 초록색(절감 효과 강조). 색상만으로 의미가 전달되니까 레이블을 읽지 않아도 직관적이에요.

MCP 서버별 호출 횟수와 비용은 테이블로 표시하고, 각 서버 이름 옆에 성공률 배지를 달았어요. 성공률 분류 로직은 간단해요.

// McpUsage.tsx — 성공률 배지
function getSuccessBadge(rate: number) {
  if (rate >= 0.99)
    return "bg-green-500"
  if (rate >= 0.95)
    return "bg-yellow-500"
  return "bg-red-500"
}
// rate = (call_count - error_count) / call_count

이 배지 하나로 어떤 MCP 서버가 불안정한지 한눈에 파악해요. 에러율이 높은 서버가 보이면 서버 로그를 확인하거나 rate limit을 점검해요.

SD 이모지 — 클로디의 표정으로 상태 표현

대시보드의 SessionCard마다 에이전트 상태를 한눈에 알 수 있는 SD(Stable Diffusion으로 제작한) 이모지가 표시돼요. 클로디와 시월이 각각 5가지 상태에 맞는 표정을 갖고 있어요.

에이전트 상태 이모지 (파일명 기준) 의미
클로디 avatar (기본) wink (윙크) 대기 중 아바타
running (실행 중) cx02 (집중) “열심히 작업 중이에요~”
completed (완료) excited (신남) “오빠, 다 됐어요!”
error (에러) embarrassed (당황) “앗, 뭔가 잘못됐어요…”
idle (유휴) thinking (생각 중) / sleeping: cx04 (잠)
시월이 running (실행 중) fighting (파이팅) “열심히 할게요!”
completed (완료) ok (OK 포즈) “완료했습니다!”
error (에러) sorry (미안) “죄송해요…”
idle (유휴) heart (하트) “기다리고 있어요.”
sleeping (수면) please (부탁) “잠시 쉬는 중이에요.”

기술적으로는 Telegram 512×512 포맷의 PNG를 React 컴포넌트에서 상태값에 따라 분기하는 단순한 구조예요. 세션 상태가 바뀔 때마다 SessionCard의 이모지가 교체되니까, 여러 에이전트가 동시에 돌아가는 대시보드에서 무엇이 실행 중이고 무엇이 완료됐는지 텍스트를 읽지 않아도 바로 파악돼요. 이 작은 요소가 대시보드를 “나만의 도구”로 느끼게 해주는 포인트예요.

비용 관련 화면에서도 클로디 이모지가 활약해요 — 절감액이 클 때는 excited, 지출이 급증하면 embarrassed, 집계 로딩 중에는 thinking 표정이 나와요.

JSONL 세션 이력 스캐닝도 같은 맥락이에요. Claude Code가 끝낸 세션은 ~/.claude/projects/ 아래 JSONL 파일로 남아요. 이 파일을 전부 파싱하면 수천 개가 될 수 있어서, 30초 TTL 캐시와 파일 선두 8KB + 말미 2KB만 읽는 경량 스캔 전략을 썼어요. 세션 목록이 즉시 뜨고, “Resume” 버튼을 누르면 claude --resume <session-id>로 이전 컨텍스트를 그대로 이어받아요.

실제 데이터 — 계산 로직 투명 공개

비용 추적 시스템이 실제로 어떤 숫자를 내는지, 2일간의 실측 데이터로 보여드릴게요.

AI Cost Center — Real Spend $12.22 vs If All API $6,648, MAX Savings $6,636
실제 대시보드 비용 화면. 이틀간 API 환산 비용이 $6,648인데 실제 지출은 $12.22. MAX 구독의 위력이에요.

이틀 동안 94개 세션, input 26.3M + output 9.6M 토큰을 사용했어요. 하지만 이 숫자만으로는 $6,648이 나오지 않아요. 핵심은 cache tokens이에요.

# pricing.json — per 1M tokens (Anthropic 공식 요금)
# Claude Opus 4.6:
#   input: $15, output: $75
#   cache_read: $1.50 (input 대비 90% 할인)
#   cache_creation: $18.75 (input 대비 25% 할증)

def estimate_cost(model, input_tokens, output_tokens,
                  cache_read=0, cache_creation=0):
    p = PRICING[model]
    return (
        input_tokens * p["input"] / 1_000_000
        + output_tokens * p["output"] / 1_000_000
        + cache_read * p["cache_read"] / 1_000_000
        + cache_creation * p["cache_creation"] / 1_000_000
    )

# 실측 데이터 (2일간, Opus 4.6):
#   input: 1.49M, output: 4.85M
#   cache_read: 973M, cache_creation: 198M
# = $22 + $364 + $1,460 + $3,706 = $5,552

Claude Code는 매 턴마다 전체 대화 히스토리를 prompt caching으로 넘겨요. 그래서 cache_read가 9.73억 tokens, cache_creation이 1.98억 tokens까지 올라가는 거예요. 캐싱이 없었으면 이 토큰들이 전부 일반 input 가격($15/1M)으로 계산되어 $17,951이었을 거예요. 캐싱 덕분에 $12,399가 절약된 셈이지만, cache_creation만 $3,706이에요.

⚠️ 솔직한 고백: 이 계산이 100% 정확한지 아직 검증 중이에요. Claude Code JSONL에서 토큰을 파싱하는 방식이 맞는지, cache tokens이 이렇게 많은 게 정상인지 — 확인할 게 남아있어요. 만약 이 글을 읽는 분 중에 Claude API 과금 구조에 밝은 분이 있다면, 위 로직이 맞는지 댓글로 알려주시면 정말 감사하겠습니다. 🙏

확실한 건 하나예요: MAX 구독 없이 API를 직접 호출했으면 적어도 수백 달러에서 수천 달러 수준의 비용이 이틀 만에 발생했을 거라는 거예요. 정확한 숫자는 달라질 수 있지만, MAX $200의 가성비는 의심할 여지가 없어요.

운영 결과 — Claude Code 비용 추적 3개월 리포트

Claude Code 비용 추적 대시보드를 3개월 돌려본 결과, Claude MAX $200/월 대비 API 추정 비용은 매달 $600~$1,200 사이였어요. Opus 사용 비중이 높은 달은 $1,000을 넘겼어요. Real Spend(Gemini+xAI+Qwen)는 월 $15~$40 수준이고, MCP 서버 비용은 대부분 이미지 생성(xAI, Gemini)에서 발생해요.

숫자로 보면 MAX 구독이 3~6배의 가치를 제공하고 있다는 결론이에요. 이 데이터를 대시보드에서 매일 확인할 수 있다는 것 — 그게 Claude Code 비용 추적의 진짜 가치예요. “감으로 MAX가 이득일 것 같다”에서 “데이터로 증명된 이득”으로 바뀌니까요.

시리즈를 마치며

네 편에 걸쳐 “RDP 없이 Claude Code 원격 제어하기”를 완주했어요. 돌아보면 각 에피소드가 하나의 문제를 해결하면서 다음 문제를 드러내는 구조였어요.

EP1은 “왜 만들어야 하는가”였어요. RDP의 불편함, 비용 블랙박스, 멀티 호스트 분산이라는 세 가지 고통이 대시보드 프로젝트를 시작하게 했죠. EP2에서 FastAPI + WebSocket + React라는 미니멀한 스택으로 실시간 통신 기반을 놓았고, EP3에서 JWT 인증·TOTP 2FA·xterm.js 터미널로 “외부에서도 안전하게 접근하는 원격 제어”를 완성했어요. 그리고 이번 EP4에서 비로소 EP1에서 제기했던 비용 블랙박스 문제를 실제 데이터로 풀었어요.

대시보드를 만들기 전에는 매달 청구서를 열어보기 전까지 얼마를 쓰고 있는지 몰랐어요. 이제는 오늘 몇 토큰을 썼는지, 어떤 MCP 서버가 얼마를 소비했는지, MAX 구독이 이번 달 몇 달러를 아껴줬는지 실시간으로 볼 수 있어요. 클로디의 SD 이모지가 excited 표정으로 절감액을 알려줄 때 — 그게 이 시리즈 전체의 보상이에요.

개인 대시보드의 매력은 “필요한 것만, 내가 원하는 방식으로” 만들 수 있다는 거예요. 상용 도구는 범용성을 위해 복잡해지지만, 내 대시보드는 내가 쓰는 7개 MCP 서버와 3개 프로바이더만 추적하면 돼요. SQLite 파일 하나, FastAPI 서버 하나, React 프론트엔드 하나 — 이 미니멀한 스택으로 충분했어요.

클로디의 대시보드는 claude-code-dashboard 같은 오픈소스 도구에서 영감을 받았지만, MCP 텔레메트리와 멀티 프로바이더 비용 추적은 직접 구현한 부분이에요. 여러분의 워크플로에 맞는 대시보드를 만들어보세요. 코드는 생각보다 단순하고, 효과는 생각보다 커요.

시리즈를 마무리하면서 세 가지 교훈을 남겨요.

  • launcher가 앱보다 어렵다. FastAPI 서버 코드보다 Windows Task Scheduler + 프로세스 관리가 훨씬 까다로웠어요. VBS→bat→python 체인은 세 단계 모두 실패 모드가 달랐어요. 처음부터 단일 Python 진입점(launcher.py)으로 설계했다면 이틀치 삽질을 아꼈을 거예요.
  • 캐싱을 먼저 설계하라. JSONL 세션 파일이 수천 개가 되면 매번 전체를 스캔하는 게 불가능해요. 30초 TTL 캐시와 선두/말미 부분 읽기는 초기부터 설계에 넣어야 할 요소예요. 나중에 추가하면 API 인터페이스를 다 바꿔야 해요.
  • 대시보드를 제품처럼 다뤄라. “그냥 내가 쓰는 도구”라고 생각하면 테스트를 생략하고 싶어져요. 173개 테스트가 없었다면 인증 리팩토링 때 세션 집계 버그를 놓쳤을 거예요. 개인 프로젝트일수록 테스트가 빛나요.

📋 시리즈: RDP 없이 Claude Code 원격 제어하기

EP1 문제 인식 — 왜 RDP를 버렸는가 EP2 아키텍처 — FastAPI + WebSocket + React EP3 인증과 CLI 스트리밍 — 보안 원격 터미널
EP4 비용 추적과 MCP 텔레메트리 ← 현재 글

Discover more from AI-Girls Lab

Subscribe to get our latest posts delivered to your inbox.