16%라던 백그라운드 요약 비용, 필자 환경에서 재 보니 0이었습니다#

2026-11-10

램프가 비추는 나무 책상에서 사람이 두 손으로 든 아날로그 계량기의 바늘은 눈금 시작점에 멈춰 있고, 그 너머에서 작은 로봇 셋이 편지를 나르고 종이를 읽고 카드를 정리하며 조용히 각자의 일을 하는 그림

10월 31일 자 AI 뉴스 브리프를 정리하다가 한 항목에서 손이 멈췄습니다. Claude Code가 백그라운드 에이전트의 상태를 화면에 띄우려고 사용자가 요청하지 않은 호출을 계속 보내고 있고, 한 회사에서는 그 호출이 전체 지출 추정치의 16%를 차지했다는 측정이었습니다. 필자도 서브에이전트를 백그라운드로 자주 돌리는 편이라, 브리프에 “같은 방식으로 집계해 볼 생각"이라고 적어 두었습니다. 새는 토큰이 있다면 막아야겠다는 생각이었습니다.

결론부터 적으면 이렇습니다. 끄는 방법은 원문이 소개한 방식보다 간단한 길이 이미 생겨 있었고, 나흘 동안 직접 재 보니 필자의 환경에서 그 요청은 한 번도 생기지 않았습니다. 허탈했지만, 새는 곳이 없었다는 것을 확인한 안도감도 컸습니다. 이 글은 그 확인 과정과, 왜 0이었는지 찾아낸 기록입니다. 확인 시점의 Claude Code 버전은 2.1.319입니다.

브리프에서 본 16%#

Claude Code에서 서브에이전트(메인 대화가 일을 나눠 맡기는 별도 에이전트)를 백그라운드로 돌리면, 에이전트 패널에 각 에이전트가 지금 무엇을 하는지 짧은 문장이 뜹니다. Flamerix AI의 분석에 따르면 이 문장은 공짜가 아닙니다. Claude Code는 백그라운드 에이전트마다 약 30초 간격으로 그 에이전트의 대화 기록 전체를 모델에 보내 “최근에 한 일을 3–5단어로 설명하라"고 요청합니다. 이때 쓰는 모델은 그 에이전트의 모델이라 대개 Opus입니다. 이 동작은 8월 버전 2.1.232부터 대화형 세션에서 기본으로 켜졌습니다.

백그라운드 에이전트일하는 중대화 기록 전체30초마다 다시 전송모델 호출대개 Opus상태 문장 3–5단어에이전트 패널에 표시30초 뒤 다시, 에이전트가 끝날 때까지 반복
상태 문장 하나를 만들 때마다 대화 전체를 다시 읽으므로, 백그라운드 에이전트가 오래 돌수록 비용이 쌓입니다.

원문 작성자의 회사는 일주일 동안 요청을 출처별로 묶어 봤고, 이 요약 요청이 지출 추정치의 16.4%, 요청 수의 19.7%를 차지했습니다. 메인 대화(15.7%)보다 많았습니다. 반론도 있었습니다. 캐시 할인을 반영해 다시 계산하면 약 8%라는 댓글이 달렸고, 작업 방식에 따라 비중이 크게 달라진다는 지적도 있었습니다. 어느 쪽이든 사용자가 요청하지 않은 호출이 청구서나 구독 한도에 그대로 잡힌다는 점은 같았습니다.

끄기 전에 따져 본 것: 답변 품질#

끄는 방법을 찾기 전에 먼저 확인한 것은 끄면 무엇을 잃느냐였습니다. 필자에게 화면에 뜨는 상태 문장은 중요하지 않습니다. 중요한 것은 최종 답변의 품질이 떨어지느냐입니다.

상태 문장이 모델에게 다시 쓰이는 곳이 한 군데 있습니다. 대화가 길어져 앞부분을 요약본으로 바꾸는 압축이 일어날 때, 아직 돌고 있는 백그라운드 에이전트가 있으면 그 에이전트의 최근 상태 문장이 Progress: 한 줄로 메인 모델에 다시 붙습니다. 원문 댓글에서 지적되고 작성자가 인정한 내용입니다. 요약을 끄면 이 한 줄이 빠집니다.

그래도 메인 모델은 그 에이전트가 실행 중이라는 사실을 알고, 에이전트가 끝나면 결과 전문을 받습니다. 최종 답변은 이 결과 전문을 바탕으로 만들어지고, 3–5단어 상태 문장은 쓰이지 않습니다. 영향이 있다면 에이전트가 도는 도중에 압축이 일어나고, 메인 모델이 결과를 기다리지 않고 진행 상황에 기대어 판단해야 하는 드문 경우뿐입니다. 필자는 꺼도 손해가 없다고 판단했습니다.

끄는 방법은 이미 바뀌어 있었습니다#

원문이 제시한 방법은 프리로드 스크립트였습니다. Claude Code가 실행될 때 함께 불러오는 70줄짜리 스크립트로, 요약 타이머만 골라 영원히 울리지 않게 만듭니다. 작성자 스스로 약관과 충돌할 수 있다고 적었습니다. 업데이트로 코드 모양이 바뀌면 아무 표시 없이 효과가 사라지고, 스크립트 파일에 문제가 생기면 Claude Code가 아예 시작되지 않는다는 단점도 있습니다.

그런데 원문 아래 댓글에서 더 간단한 길이 나왔습니다. 2.1.318부터 Claude Code에 CLAUDE_CODE_DISABLE_AGENT_PROGRESS_SUMMARIES=1이라는 환경 변수가 들어갔고, 이 값을 주면 타이머가 꺼진다는 것입니다. 작성자도 실행 파일에서 확인했다고 답했습니다. 필자도 설치된 2.1.319 실행 파일을 직접 검색해 봤고, 이 변수는 Claude Code가 인식하는 환경 변수 목록에 들어 있었습니다. 다만 변경 기록과 공식 환경 변수 문서에는 아직 나오지 않습니다. 프로그램에는 있지만 공식 안내는 없는 설정입니다.

우회 스크립트를 들일 이유는 사라졌습니다. 다만 원문에서 낡지 않은 것이 하나 있었습니다. 요청을 출처별로 쪼개 보는 측정 방법입니다. 끄는 법은 낡았지만, 재는 법은 그대로 쓸 만했습니다.

세션 로그에는 남지 않는 요청#

필자는 지난 글에서 로컬 세션 로그를 집계해 토큰이 어디에 쓰이는지 진단했습니다. 같은 방법으로 재려다가 원문에서 걸리는 문장을 발견했습니다. 작성자에 따르면 이 요약 요청은 디스크의 세션 로그에 기록되지 않아서, 로그 파일로 비용을 세는 도구에는 보이지 않는다는 것이었습니다. 지난번 방법에 사각지대가 있었던 셈입니다.

그래서 원문 작성자처럼 Claude Code의 기록을 쓰기로 했습니다. 기본으로 꺼져 있는 이 기능을 켜면, Claude Code는 API 요청 하나마다 모델, 토큰 수, 추정 비용, 그리고 그 요청을 보낸 주체(query_source)를 담은 이벤트를 내보냅니다. 메인 대화, 대화 압축, 각 서브에이전트가 서로 다른 값으로 찍히고, 백그라운드 요약은 agent_summary로 찍힙니다.

이벤트는 라는 방식으로 받는 쪽에 보냅니다. 보통은 모니터링 서비스로 보내지만, 필자는 외부 서비스에 기대지 않으려고 컴퓨터 안에 20줄짜리 파이썬 수신기를 띄워 파일로만 쌓았습니다. 프롬프트와 응답 내용, 실행한 명령어는 기본 설정에서는 기록되지 않습니다.

설정은 사용자 설정 파일(~/.claude/settings.json)에 넣었습니다. 공식 문서에 따르면 OpenTelemetry 전송 설정은 저장소의 프로젝트 설정에서는 무시됩니다. 대신 사용자 설정에 한 번 넣으면 이 컴퓨터에서 여는 모든 저장소의 세션이 함께 측정됩니다. 구체적인 설정은 부록에 적었습니다.

나흘을 재 보니 0이었습니다#

11월 7일부터 10일까지 평소처럼 일했습니다. 블로그 작업과 업무용 저장소 작업을 오가며 세션 17개에서 API 요청 약 7천 건이 기록됐습니다. 그런데 agent_summary는 한 건도 없었습니다. 이름이 다르게 찍혔을까 싶어 원본 기록에서 “summary"가 들어간 값을 모두 찾아봤지만 역시 없었습니다.

맥이 빠졌습니다. 새는 토큰을 찾으려고 측정 도구까지 세웠는데, 샐 곳이 없었습니다. 그래도 바로 믿지는 않고 측정이 잘못됐을 가능성부터 확인했습니다.

  • 요약이 생길 상황이 없었던 것 아닌가? 업무용 저장소의 세션 기록을 보니 11월 9일 하루에만 세 세션에서 서브에이전트를 명시적으로 백그라운드로 약 100번 실행했습니다. 상황은 충분했습니다.
  • 텔레메트리를 켠 것이 결과를 바꾸지 않았나? 2.1.318 변경 기록에 “텔레메트리가 꺼져 있을 때” 진행 요약의 동작이 바뀌었다는 줄이 있어서 확인했습니다. 캐시가 식었을 때 응답을 얼마나 기다리는지에 관한 변경이었고, 요약을 켜고 끄는 것과는 관계가 없었습니다.
  • 끄기 설정이 이미 들어가 있었던 것 아닌가? 측정 기간에는 넣지 않았고, 측정을 마친 뒤에 넣었습니다.

0은 측정 오류가 아니었습니다.

왜 0이었나: 에디터 확장은 요약을 켜지 않습니다#

실마리는 같은 기록에 있었습니다. 메인 대화의 요청 출처가 공식 문서에 예로 나온 repl_main_thread가 아니라 전부 sdk로 찍혀 있었습니다. 필자는 Claude Code를 터미널이 아니라 Cursor 에디터 확장으로 씁니다. 확장은 Claude Code를 로 띄우고, 화면은 확장이 직접 그립니다.

원문의 코드 분석에도 이 구분이 있었습니다. 일반 백그라운드 서브에이전트의 요약은 터미널의 대화형 모드에서만 기본으로 켜지고, SDK 모드에서는 Claude Code를 띄우는 쪽 프로그램이 agentProgressSummaries 옵션을 켜야 생깁니다. 필자가 확인한 것도 같았습니다. Claude Code에 들어 있는 SDK 옵션 설명에는 진행 요약을 “이 옵션이 켜져 있을 때만” 만든다고 적혀 있고, 설치된 Cursor 확장 코드는 이 옵션을 비워 둔 채로 넘깁니다.

터미널에서 실행대화형 모드백그라운드 서브에이전트요약 타이머 켜짐요약 요청 발생30초마다 대화 전체 전송에디터 확장에서 실행SDK 모드백그라운드 서브에이전트요약 옵션이 비어 있음요약 요청 없음나흘 측정에서 0건
같은 Claude Code라도 에디터 확장은 요약 옵션을 켜지 않고 실행하므로, 서브에이전트를 백그라운드로 돌려도 요약 요청이 생기지 않습니다.

예외도 있습니다. 원문에 따르면 대화를 통째로 물려받는 fork 서브에이전트와 fork로 실행되는 스킬은 SDK 모드에서도 요약됩니다. 측정 기간에 필자는 이 둘을 쓰지 않았습니다. 터미널에서 Claude Code를 직접 띄워 백그라운드 에이전트를 돌리는 사람이라면 원문의 16%가 그대로 해당할 수 있습니다. 확장이 앞으로 이 옵션을 켜는 쪽으로 바뀔 수도 있으므로, 이 결과는 2.1.319와 지금의 확장 코드 기준입니다.

그래도 스위치는 켜 두었습니다#

측정을 마치고 텔레메트리는 껐고, 쌓인 원본 기록은 지웠습니다. 사용자 설정에는 끄기 변수 한 줄만 남겼습니다. 지금은 효과가 없지만 손해도 없습니다. 언젠가 터미널에서 백그라운드 에이전트를 돌리거나, fork를 쓰거나, 확장의 기본값이 바뀌었을 때를 위한 보험입니다.

다만 필자 환경에서는 원래 0이어서, 이 변수가 실제로 타이머를 끄는지는 직접 확인하지 못했습니다. 문서에 없는 설정이므로 버전이 오르면서 사라지거나 바뀔 수 있다는 점도 함께 기억해 둡니다.

맺으며#

“16%를 쓰고 있다"는 문장을 읽었을 때 필자가 먼저 떠올린 것은 줄일 방법이었습니다. 실제로 필요했던 것은 그 16%가 필자의 실행 방식에서도 생기는지 확인하는 일이었습니다. 같은 Claude Code라도 터미널에서 쓰는지, 에디터 확장에서 쓰는지에 따라 이 호출은 생기기도 하고 생기지 않기도 했습니다.

나흘의 측정이 남긴 것은 줄어든 비용이 아니라 두 가지 확인입니다. 필자의 토큰은 이 기능으로 새고 있지 않았습니다. 그리고 세션 로그만 보던 지난번 방법으로는 이런 요청을 볼 수 없다는 것도 알게 됐습니다. 다음에 비슷한 수치를 만나면 끄는 방법보다 재는 방법을 먼저 찾을 생각입니다.

부록: 설정 예시#

측정용 텔레메트리#

사용자 설정 파일(~/.claude/settings.json)의 env에 넣습니다. 설정을 바꾼 뒤 새로 연 세션부터 적용되고, 측정이 끝나면 지웁니다.

{
  "env": {
    "CLAUDE_CODE_ENABLE_TELEMETRY": "1",
    "OTEL_LOGS_EXPORTER": "otlp",
    "OTEL_EXPORTER_OTLP_PROTOCOL": "http/json",
    "OTEL_EXPORTER_OTLP_ENDPOINT": "http://127.0.0.1:4318"
  }
}

로컬 수신기#

받은 요청 본문을 날짜별 파일에 한 줄씩 쌓기만 합니다. 측정 기간 동안 띄워 둡니다.

# receiver.py
import json
from datetime import datetime
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer


class Handler(BaseHTTPRequestHandler):
    def do_POST(self):
        body = self.rfile.read(int(self.headers.get("Content-Length") or 0))
        with open(datetime.now().strftime("otel-%Y%m%d.jsonl"), "a") as f:
            f.write(json.dumps(json.loads(body)) + "\n")
        self.send_response(200)
        self.end_headers()

    def log_message(self, *args):
        pass


ThreadingHTTPServer(("127.0.0.1", 4318), Handler).serve_forever()

출처별 집계#

api_request 이벤트를 query_source별로 묶어 요청 수와 추정 비용 비율을 냅니다.

import collections, glob, json

count, cost = collections.Counter(), collections.Counter()
for path in glob.glob("otel-*.jsonl"):
    for line in open(path):
        for rl in json.loads(line).get("resourceLogs", []):
            for sl in rl.get("scopeLogs", []):
                for rec in sl.get("logRecords", []):
                    a = {x["key"]: list(x["value"].values())[0] for x in rec.get("attributes", [])}
                    if a.get("event.name") == "api_request":
                        count[a.get("query_source")] += 1
                        cost[a.get("query_source")] += float(a.get("cost_usd") or 0)

total = sum(cost.values()) or 1
for source, n in count.most_common():
    print(f"{source:32} 요청 {n:6}  비용 {100 * cost[source] / total:5.1f}%")

백그라운드 요약 끄기#

2.1.318 이상에서 인식하는 변수입니다. 공식 문서에는 아직 없습니다.

{
  "env": {
    "CLAUDE_CODE_DISABLE_AGENT_PROGRESS_SUMMARIES": "1"
  }
}

참고 자료#