견고한 Claude Code JSONL 파서 구축하기: 12번의 CLI 릴리스를 견뎌낸 3가지 패턴
요약
Claude Code의 JSONL 세션 파일 스키마가 빈번하게 변경되는 환경에서, 데이터 손실 없이 견고한 파서를 구축하는 3가지 패턴을 소개합니다. 화이트리스트 활용, 파생 데이터 버전 관리 등을 통해 CLI 업데이트에 유연하게 대응하는 방법을 다룹니다.
핵심 포인트
- 명시적 화이트리스트를 사용하여 미지의 데이터 타입을 안전하게 보존
- 스키마 변경에 대응하기 위해 파생 데이터의 버전 관리 메커니즘 도입
- 버전 업데이트 시 자동 재파싱(backfill)을 통한 데이터 정합성 유지
- 식별자 필드 길이를 제한하여 파싱 안정성 확보
~/.claude/projects/ 경로에 있는 Claude Code의 JSONL 세션 파일을 파싱하기 위한, 관대한 화이트리스트(whitelist), 버전화된 파생 데이터(versioned derived data), toWellFormed() 문자열 정규화(normalization)를 포함한 3가지 검증된 패턴을 배워보세요.
견고한 Claude Code JSONL 파서 구축하기: 12번의 CLI 릴리스를 견뎌낸 3가지 패턴
Claude Code와 나누는 모든 대화는 ~/.claude/projects/ 아래에 JSONL 형식으로 디스크에 기록됩니다. 당신의 결정, 막다른 길, 세 번의 세션에 걸쳐 진행된 버그 수정 과정까지 모두 그곳에 있습니다. 아마 여러분은 한 번도 이를 열어본 적이 없을 것입니다.
문제는 이 형식이 내부 구현 세부 사항(internal implementation detail)이라는 점입니다. 문서도 없고, 버전 필드도 없으며, 안정성 보장도 없습니다. CLI가 업데이트될 때마다 스키마(schema)가 변경되는데, 현재 속도로 보면 거의 매일 발생합니다. 하지만 올바른 패턴을 사용한다면, 이러한 변화 속에서도 살아남고 그 변화로부터 배울 수 있는 도구를 구축할 수 있습니다.
다음은 이 파일들을 기반으로 읽기 전용 재생(replay) 및 검색 도구를 구축하고, 12번의 CLI 릴리스 동안 이를 유지해 온 개발자가 제안하는 세 가지 패턴입니다.
패턴 1: 명시적 화이트리스트(Whitelist)를 활용한 관대한 라인 파싱(Tolerant Line Parsing)
단순한 루프(각 라인을 JSON.parse하고 type에 따라 switch 문을 사용하는 방식)는 첫날에는 잘 작동합니다. 문제는 CLI 업데이트로 인해 아무도 본 적 없는 type이 도입되었을 때 어떤 일이 벌어지느냐 하는 것입니다. 이는 가설이 아닌 실제 상황입니다.
버티는 데 성공하는 접근 방식은 다음과 같습니다: 알려진 타입에 대한 명시적인 화이트리스트(whitelist)를 유지하고, 그 외의 모든 것은 "파싱 실패, 하지만 보존됨"으로 취급하는 것입니다.
const KNOWN_MESSAGE_TYPES = new Set([
'user', 'assistant', 'system',
'queue-operation', 'last-prompt',
...
이 화이트리스트는 저장 정책(storage policy)으로서 이중 역할을 수행합니다. 알려진 타입의 경우, 추출된 컬럼(column)만으로 충분하므로 원본 JSON은 버려도 됩니다. 이것만으로도 디스크 공간의 대부분을 확보할 수 있습니다. 알 수 없는 타입의 경우, 원본 라인은 손대지 않은 채 아카이브 테이블(archive table)로 들어갑니다. 나중에 파서의 미래 버전이 새로운 형태를 학습하게 되면, 그 증거는 여전히 그곳에 남아있을 것입니다.
효과를 보는 한 가지 세부 사항이 더 있습니다: 식별자 필드(uuid, requestId)를 신뢰하기 전에 128자 정도의 합리적인 길이로 제한하는 것입니다.
패턴 2: 스키마뿐만 아니라 파생 데이터도 버전 관리하기
알 수 없는 데이터(unknowns)를 보존하는 것은 나중에 이를 활용할 수 있을 때만 의미가 있습니다. 그 메커니즘은 세션별로 저장되는 SUMMARY_VERSION 정수입니다. 파서가 새로운 기술을 습득하면 버전을 올리세요. 인덱서(indexer)는 오래된 버전을 감지하고 다음 동기화 시 해당 세션들을 자동으로 다시 파싱(re-parse)합니다.
이를 통해 "스키마가 또 바뀌었습니다"라는 상황은 마이그레이션 위기가 아닌 일상적인 작업이 됩니다. 파서를 확장하고, 버전을 올리고, 백필(backfill)이 실행되도록 두기만 하면 됩니다. 수동 작업도, 데이터 손실도, "인덱스를 삭제하고 처음부터 다시 시작해 주세요"라는 릴리스 노트도 필요 없습니다.
실전 사례 1: 고립된 서로게이트(Lone Surrogates)
어느 날 인덱서가 다운스트림 소비자(downstream consumers)를 충돌시키는 문자열을 생성하기 시작했습니다. 원인은 일부 JSONL 라인에 _짝이 맞지 않는 UTF-16 서로게이트(unpaired UTF-16 surrogates)_가 포함되어 있었기 때문입니다. 도구 오류 메시지 속에 숨어 있던 이모지의 절반이 문제였습니다.
이모지의 절반이 어떻게 디스크에 저장될 수 있었을까요? 이전 버전의 Claude Code(약 2.1.132 버전까지)는 긴 도구 출력값을 바이트 길이 기준으로 잘라냈는데(truncate), 이때 잘리는 지점이 이모지의 중간일 때가 있었습니다. JSON.stringify는 이 고립된 서로게이트를 \udXXX 이스케이프 문자로 기쁘게 기록하며, 파일은 깨끗한 ASCII처럼 보이지만, JSON.parse는 읽기 시점에 깨진 문자열을 충실하게 재구성합니다.
적절한 위치에 적용하기만 한다면, 해결책은 단 한 줄입니다:
// 파서의 종료 경계(exit boundary)에서, 추출된 모든 문자열에 적용
export function ensureWellFormed(s: string): string {
return s.toWellFormed() // 고립된 서로게이트 → U+FFFD
...
실제 교훈은 적용 위치에 있습니다. 데이터 수집(ingestion) 단계에서 한 번만 정규화(normalize)하면, 다운스트림의 모든 소비자(검색 인덱스, 렌더러, Markdown 내보내기 등)는 영원히 잘 형성된(well-formed) 문자열을 가정하고 작업할 수 있습니다. (String.prototype.toWellFormed()를 사용하려면 Node.js 20 이상이 필요합니다.)
실전 사례 2: 자신을 두 번 계산한 토큰들
해당 도구의 토큰 대시보드는 한때 실제보다 약 2.3배 높은 사용량 수치를 보고한 적이 있습니다. 원인은 다음과 같습니다: 하나의 API 응답이 여러 개의 JSONL 라인이 될 수 있다는 점입니다. 응답에 여러 개의 콘텐츠 블록(텍스트 및 도구 호출 (tool calls))이 포함된 경우, Claude Code는 각 블록마다 하나의 assistant 항목을 작성하며, 각 항목은 전체 토큰 수의 _사본 (copy)_을 포함합니다. 만약 라인당 토큰 수를 단순히 합산한다면, 블록의 개수만큼 중복 계산하게 됩니다.
해결책: 응답의 첫 번째 assistant 라인에서만 토큰을 계산하거나, 더 나은 방법으로는 합산하기 전에 requestId를 기준으로 중복을 제거(deduplicate)하는 것입니다.
지금 바로 시도해보세요
- 자신의 세션 탐색하기:
ls ~/.claude/projects/를 입력하여 무엇이 있는지 확인하세요.head -n 5 ~/.claude/projects/<project>/<session>.jsonl을 사용하여 형식을 살짝 엿볼 수 있습니다. - 내결함성 파서 (tolerant parser) 구축하기: 위에서 언급한 화이트리스트 패턴으로 시작하세요. 안정성을 가정하지 말고, 알 수 없는 타입에 대비한 계획을 세우세요.
- 문자열 정규화 (string normalization) 추가하기: 다운스트림(downstream)에서의 충돌을 방지하기 위해 데이터 수집 경계(ingestion boundary)에서
toWellFormed()를 적용하세요. - 파생 데이터의 버전 관리: 세션별로 버전 번호를 저장하여, 파서가 개선되었을 때 다시 파싱할 수 있도록 하세요.
출처: dev.to
원문 게시지: gentic.news
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기