Develop

로컬 추정치란?

상태줄의 로컬 리셋 추정이 실제와 3시간 어긋나, Claude가 전달하는 rate_limits 기반 위젯으로 바꿨다

한 줄로

로컬 추정치는 서버가 알려주는 진짜 값이 없을 때, 클라이언트가 자기 손에 있는 흔적만으로 답을 만들어내는 것이다. 어제 Claude Code 상태줄을 뜯어고치면서 이 추정치가 3시간이나 틀린 값을 아무 경고 없이 보여주고 있던 걸 발견했다. 렌더 시간을 4,070ms에서 240ms로 줄인 것보다, 빠르게 보여주던 숫자가 틀린 숫자였다는 쪽이 더 오래 붙잡은 문제였다.

로컬 추정치란 무엇인가

어떤 값의 출처는 크게 두 가지다.

  • 서버가 직접 보내주는 값 — 권위 있는 원본이 그대로 전달된다.
  • 로컬 추정치 — 클라이언트가 가진 부스러기(로그, 캐시, 파일 수정 시각)로 원본을 역산한다.

로컬 추정치의 문제는 틀린다는 것 자체가 아니라, 틀렸을 때 틀렸다고 말해주지 않는다는 것이다. 서버 응답은 실패하면 에러가 나지만, 추정은 언제나 그럴듯한 숫자 하나를 내놓는다. 화면에 뜬 숫자만 보고는 그게 원본인지 역산인지 구분할 수 없다.

실제로 겪은 것 — block-timer 의 3시간

상태줄에 붙여 쓰던 block-timer 위젯은 사용량 블록이 언제 리셋되는지를 보여준다. 이 위젯은 API credentials 가 없으면 로컬 transcript(대화 기록 파일)를 읽어 블록 시작 시각을 추정한다.

캐시에는 이렇게 박혀 있었다.

"startTime": "2026-09-17T00:00:00.000Z"

KST 09:00, 즉 이 머신에서 Claude 를 처음 켠 시각이다. 여기에 블록 길이를 더해 리셋을 14:00 으로 계산했다. 그런데 실제 리셋은 17:00 이었다. 3시간 차이다.

더 고약한 건, 그 전까지 경과 시간 표시가 맞아떨어지는 것처럼 보였다는 점이다. 그건 검증이 아니라 우연이었다. 추정치가 한동안 맞는 것처럼 보이는 구간이 있으면 그걸 근거로 신뢰하게 되는데, 그 신뢰에는 아무 근거가 없다.

대체 — Claude 가 직접 보내주는 값

session-usage 위젯은 Claude Code 가 직접 전달하는 값을 쓴다. 래퍼에 일시 캡처를 걸어 실제로 무엇이 들어오는지 확인했다.

"rate_limits": {
  "five_hour": { "used_percentage": 72, "resets_at": 1789632000 },
  "seven_day": { "used_percentage": 58, "resets_at": 1789707600 }
}

노트에서 resets_at을 변환해 확인한 시각은 17:00, 실제 리셋과 일치한다. credentials 도 필요 없다. Claude Code v1.2.80 이상에서 들어오는 필드다.

정리하면 이렇다.

  block-timer session-usage
출처 로컬 transcript 역산 Claude 가 보내는 rate_limits
리셋 계산 14:00 (틀림) 17:00 (/usage 와 일치)
credentials 없으면 추정 모드 불필요

속도 이야기 — 병목은 도구가 아니라 래퍼였다

이 작업의 출발점은 원래 속도였다. 상태줄 렌더에 4,070ms 가 걸리고 있었다.

구조는 이랬다. ~/.claude/statusline.sh(dotfiles의 claude/statusline.sh로 연결된 심링크) 가 ccstatusline 출력을 받아서 인증 표시(👤 세션)와 중첩 서브리포 목록(📦 dotfiles:main ✳2)을 덧붙였다.

실측해 보니 범인은 예상 밖이었다.

후보 시간
find 스캔 35ms
node 기동 54ms
settings.json 의 custom-command 위젯이 subrepos.sh 를 두 번 계산 별도 소요 시간은 기록하지 않음

캐시 TTL(유효 시간) 2초가 상태줄 렌더 간격보다 짧아 자주 만료됐다.

도구를 바꿀 생각도 했지만 기각했다. GitHub stars 를 실측(2026-09-17)해 보니 ccstatusline 12,906 / CCometixLine 3,461(Rust, 6개월 미푸시) / claude-powerline 1,169 로, 이미 가장 유명하고 활발한 것을 쓰고 있었다. 더 중요한 건 Rust 로 갈아타도 병목인 래퍼의 중복 계산은 그대로 남는다는 점이었다. 느린 건 렌더러가 아니라 그 위에 얹은 셸 로직이었다.

그래서 커스텀 셸 로직을 걷어내고 위젯 조합으로 전면 교체했다.

Model: Opus 5 | Thinking: high | Session: 55.0% | 1hr 5m
Context: [████░░░░░░░░░░░░] 52k/200k (26%) | ↻ 0
⎇ main | (+24,-18) | cwd: ~/dotfiles

3줄, 위젯 8개. 속도는 4,070ms → ~300ms(Block 포함) → ~240ms → ~220ms(session-usage 전환 후) 로 내려갔다.

정보량 0인 위젯은 뺀다

위젯을 고르는 기준이 하나 더 생겼다. 그 자리에 있어서 실제로 알려주는 게 있는가.

  • worktree 위젯 — 본체에서 일할 때 브랜치와 무관하게 main 고정값을 반환한다. 항상 같은 값이면 정보량은 0이다. 제거.
  • vim-mode 위젯 — 요청으로 제거.
  • 나머지 50여 개(cache 통계, 입출력 속도, jj, 세션 이름) — 넣지 않았다. 커뮤니티 표현대로 “붐비는 바는 시끄러운 채팅만큼 나쁘다”.

cwd 는 abbreviateHome 옵션이 이미 있어 스크립트를 고칠 필요가 없었고(홈 밖 경로는 전체 경로를 유지하는 것도 확인), 위치만 4번째 줄에서 3번째 줄 우측으로 옮겼다. 색은 전부 bright 계열로 통일했다 — Model 80, Thinking 140, Context 178, git 브랜치 231, git 변경 155, cwd 111, 컴팩션 59.

옵션 값이 목록 전체를 대체하는 함정

작업 중에 부작용이 하나 나왔다. git 위젯의 hide 옵션에 "no-git" 만 지정했더니 ↑0↓0 이 새로 나타났다.

노트에는 hide 값이 목록 전체를 대체하는 것이 원인으로 기록돼 있다. ahead/behind 위젯의 기본 zero 숨김을 다시 명시해 복원했다.

hide: "no-git,no-upstream,zero"

기본값에 있던 것을 다시 명시해 복원했다. git 아닌 곳에서 (no git) 이 3회 반복되던 것도 같이 해결됐다.

남는 것

결과 커밋은 5579e57 refactor(statusline): 커스텀 셸 로직을 걷고 ccstatusline 위젯으로 대체 다. statusline-subrepos.sh, statusline_replace.py 두 파일이 삭제됐다.

속도 개선은 숫자가 눈에 보이니 성과처럼 느껴지지만, 이 작업에서 실제로 위험했던 건 틀린 값을 빠르게 보여주고 있던 쪽이었다. 상태줄처럼 항상 눈에 들어오는 UI 는 거기 뜬 숫자를 무의식적으로 믿게 만든다. 그래서 위젯을 고를 때 “이 값의 출처가 서버인가 역산인가” 를 한 번 확인하는 게 렌더 시간보다 먼저 볼 항목이었다.