무색
기술블로그
에세이
연구
소개

무색

소프트웨어로 비즈니스의 가능성을 만듭니다. 웹·앱 개발, 음성 AI, 자동화 콘텐츠 제작까지 — 기술이 필요한 곳에 무색이 있습니다.

연락처

contact@museck.com

사업자 정보

상호: 무색

대표: 배성재

사업자등록번호: 577-58-00836

인천광역시 연수구 인천타워대로 323, 에이동 8층 801-802호 AB-132 (송도동, 송도 센트로드)

© 2026 무색. All rights reserved.
개인정보처리방침·이용약관·연락처
INCHEON, KR
Claude Code headless 모드의 JSON 파싱 함정 두 가지 — generative 스타일 키 비주얼
자막 파이프라인
2026. 1. 14.

Claude Code headless 모드의 JSON 파싱 함정 두 가지

claude-codeheadlessjson-parsingsubprocessasr

자막 파이프라인에서 ASR 출력을 Claude로 교정하는 단계가 있다. Claude Code의 headless 모드(claude -p)를 subprocess로 돌리는데, --json-schema 옵션을 붙이면 구조화된 JSON 응답을 받을 수 있다. 간단해 보였는데 파싱이 자꾸 깨졌다.

함정 1: --max-turns 1은 응답을 못 받는다

처음엔 --max-turns 1로 설정했다. 한 번만 물어보고 끝내면 되니까. 그런데 응답이 안 왔다.

Claude Code는 내부적으로 첫 번째 턴을 도구 호출 등에 소모한다. --max-turns 1이면 내부 턴 하나 쓰고 바로 error_max_turns로 종료해버린다. result 필드 자체가 없는 응답이 돌아온다.

해결은 단순하다. --max-turns 2로 올려서 내부 턴 + 실제 응답 턴을 확보하면 된다. 문서에 이 동작이 명확히 안 나와 있어서 한참 헤맸다.

함정 2: result가 아니라 structured_output

max-turns를 고치고 나니 응답은 오는데, result 필드를 파싱하면 빈 문자열이었다.

--json-schema 옵션을 쓰면 구조화된 응답이 result가 아닌 structured_output 필드에 들어간다. 이것도 문서에서 바로 찾기 어려웠고 stdout을 직접 로깅해서 알아냈다.

수정된 코드

Claude Code 호출 부분.

result = subprocess.run(
    [
        "claude",
        "-p", f"{input_json}",
        "--system-prompt", SYSTEM_PROMPT,
        "--output-format", "json",
        "--json-schema", CORRECTION_SCHEMA,
        "--max-turns", "2",  # 1이면 내부 턴 소모로 응답 없음
        "--dangerously-skip-permissions",
    ],
    capture_output=True, text=True, env=env,
)

응답 파싱은 structured_output을 먼저 보고 없으면 result로 fallback한다.

outer = json.loads(result.stdout)

# --json-schema는 structured_output 필드에 결과 저장
inner = outer.get("structured_output")
if inner is None:
    # Fallback: 이전 CLI 버전 호환
    inner = outer.get("result", outer)
    if isinstance(inner, str):
        inner = json.loads(inner)

이전에는 JSON 파싱 실패에 대비해서 정규식으로 {"segments": ...} 패턴을 추출하는 fallback도 있었는데 --json-schema를 쓰면 그럴 필요가 없다. 코드가 꽤 깔끔해졌다.

교훈

외부 CLI 도구를 subprocess로 붙일 때 출력 포맷이 "당연히 이럴 것"이라고 가정하면 안 된다. 특히 LLM CLI는 버전마다 응답 구조가 미묘하게 바뀔 수 있어서 방어적 파싱이 필수다.

디버깅할 때 가장 도움이 된 건 raw stdout을 통째로 로깅한 것이다. 파싱 에러 메시지만 보면 원인을 알 수 없지만 실제 응답 원문을 보면 바로 보인다. 파싱 로직 앞에 logger.error("raw: %s", result.stdout) 한 줄 넣어두는 게 삽질 시간을 확 줄여준다.

자주 묻는 질문

Claude Code headless 모드에서 JSON 파싱이 실패하는 이유는?
claude CLI는 JSON 외에도 로그나 경고 메시지를 stdout에 출력할 수 있습니다. 전체 stdout을 바로 json.loads()하면 파싱 에러가 발생합니다. stream-json 줄 단위 파싱이 필요합니다.
structured_output과 result 필드의 차이는?
result는 자유 텍스트 응답이고, structured_output은 --json-schema로 지정한 스키마에 맞춘 구조화된 데이터입니다. structured_output이 있으면 이를 우선 사용해야 합니다.
자막 파이프라인(3/10)
Prev

모델 로딩 30초를 없앤 방법: Persistent Worker 서버

Next

subprocess 환경변수, 기본값이 함정이다