• NDJSON(Newline Delimited JSON)은 μ€„λ°”κΏˆ(\n)으둜 JSON 객체λ₯Ό κ΅¬λΆ„ν•˜λŠ” 직렬화 포맷
  • 각 쀄이 ν•˜λ‚˜μ˜ μ™„μ „ν•œ JSON 객체 β€” 그게 μ „λΆ€
  • TCP, Unix pipe, stdio λ“± 슀트리밍 μ±„λ„μ—μ„œ μ—¬λŸ¬ JSON을 μˆœμ„œλŒ€λ‘œ 전솑할 λ•Œ μ‚¬μš©
  • JSON-RPC, SSE 같은 β€œν”„λ‘œν† μ½œβ€μ΄ μ•„λ‹ˆλΌ 포맷(ν˜•μ‹) β€” 쀄 μ•ˆμ˜ λ‚΄μš© ꡬ쑰λ₯Ό κ°•μ œν•˜μ§€ μ•ŠμŒ

ν•΄λ‹Ή κ°œλ…μ΄ ν•„μš”ν•œ 이유

  • 슀트리밍 채널(TCP, stdio, pipe)μ—μ„œ μ—¬λŸ¬ JSON을 λŠκΉ€ 없이 μˆœμ„œλŒ€λ‘œ 전솑해야 ν•  λ•Œ
  • 둜그 파일, 이벀트 슀트림, λŒ€μš©λŸ‰ 데이터λ₯Ό ν•œ 쀄씩 읽으며 μ²˜λ¦¬ν•΄μ•Ό ν•  λ•Œ (λ©”λͺ¨λ¦¬ 효율)
  • μ„œλ²„-ν΄λΌμ΄μ–ΈνŠΈ κ°„ μ‹€μ‹œκ°„ 이벀트 pushκ°€ ν•„μš”ν•  λ•Œ (AI 응닡 streaming, 둜그 μˆ˜μ§‘ λ“±)
  • JSON λ°°μ—΄([{...},{...}])은 λκΉŒμ§€ λ°›μ•„μ•Ό νŒŒμ‹± κ°€λŠ₯ν•˜μ§€λ§Œ, NDJSON은 ν•œ 쀄 도착할 λ•Œλ§ˆλ‹€ μ¦‰μ‹œ 처리 κ°€λŠ₯

AS-IS: JSON λ°°μ—΄ β€” 전체λ₯Ό λ°›μ•„μ•Ό νŒŒμ‹± κ°€λŠ₯

sequenceDiagram
    autonumber
    participant Producer as Producer
    participant Consumer as Consumer

    Producer->>Consumer: [{...},{...},{...}] 전솑 μ‹œμž‘
    Note over Consumer: 아직 νŒŒμ‹± λΆˆκ°€ (배열이 λ‹«νžˆμ§€ μ•ŠμŒ)
    Producer->>Consumer: 전솑 μ™„λ£Œ (']' 도착)
    Consumer->>Consumer: 전체 νŒŒμ‹±
    Note over Consumer: λ§ˆμ§€λ§‰ λ°”μ΄νŠΈκΉŒμ§€ κΈ°λ‹€λ €μ•Ό 처리 κ°€λŠ₯

TO-BE: NDJSON β€” ν•œ μ€„λ§ˆλ‹€ μ¦‰μ‹œ 처리 (Claude Code CLI μ˜ˆμ‹œ)

sequenceDiagram
    autonumber
    participant App as Application
    participant CLI as claude CLI (--output-format stream-json)

    CLI-->>App: {"type":"system","subtype":"init",...}\n
    App->>App: μ¦‰μ‹œ νŒŒμ‹± β†’ μ„Έμ…˜ μ΄ˆκΈ°ν™” 처리
    CLI-->>App: {"type":"stream_event","event":{"delta":{"type":"text_delta","text":"Hello"}}}\n
    App->>App: μ¦‰μ‹œ νŒŒμ‹± β†’ UI에 토큰 좜λ ₯
    CLI-->>App: {"type":"result","result":"...","session_id":"abc"}\n
    App->>App: μ¦‰μ‹œ νŒŒμ‹± β†’ μ™„λ£Œ 처리

NDJSON 포맷 κ·œμΉ™ 3κ°€μ§€

κ·œμΉ™ 1. 각 쀄 = ν•˜λ‚˜μ˜ μ™„μ „ν•œ JSON

{"name":"Alice","age":30}\n
{"name":"Bob","age":25}\n
{"name":"Carol","age":35}\n

JSON 내뢀에 λ¦¬ν„°λŸ΄ μ€„λ°”κΏˆμ΄ 있으면 λ°˜λ“œμ‹œ \\n으둜 escapeν•΄μ•Ό ν•œλ‹€. μ€„λ°”κΏˆ = λ©”μ‹œμ§€ κ΅¬λΆ„μžμ΄κΈ° λ•Œλ¬Έμ΄λ‹€.

κ·œμΉ™ 2. λ―Έλ””μ–΄ νƒ€μž…κ³Ό 파일 ν™•μž₯자

ν•­λͺ©κ°’
λ―Έλ””μ–΄ νƒ€μž…application/x-ndjson
파일 ν™•μž₯자.ndjson
인코딩UTF-8 ν•„μˆ˜

HTTP API μ‘λ‹΅μœΌλ‘œ μŠ€νŠΈλ¦¬λ°ν•  λ•Œ:

HTTP/1.1 200 OK
Content-Type: application/x-ndjson
 
{"id":1,"status":"processing"}\n
{"id":1,"status":"done","result":"ok"}\n

파일둜 μ €μž₯ν•  λ•Œ:

# logs.ndjson
{"ts":"2026-04-17T10:00:00Z","level":"info","msg":"μ„œλ²„ μ‹œμž‘"}
{"ts":"2026-04-17T10:00:01Z","level":"info","msg":"μš”μ²­ μˆ˜μ‹ "}
{"ts":"2026-04-17T10:00:02Z","level":"error","msg":"DB μ—°κ²° μ‹€νŒ¨"}

κ·œμΉ™ 3. 빈 쀄은 λ¬΄μ‹œ κ°€λŠ₯

νŒŒμ„œλŠ” 빈 쀄(\n\n)을 κ±΄λ„ˆλ›Έ 수 μžˆλ‹€. heartbeatλ‚˜ keep-alive μš©λ„λ‘œ ν™œμš©λœλ‹€.

Claude Code CLI의 NDJSON 톡신

--output-format stream-json ν”Œλž˜κ·Έλ‘œ ν™œμ„±ν™”ν•œλ‹€. 곡식 λ¬Έμ„œμ—μ„œ β€œFormat: NDJSON (newline-delimited JSON)” 으둜 λͺ…μ‹œν•œλ‹€.

좜처: Claude Agent SDK TypeScript Reference

단방ν–₯ 좜λ ₯ 슀트림 (κΈ°λ³Έ ν™œμš©)

claude -p "Fix the bug in auth.py" \
  --output-format stream-json \
  --allowedTools "Read,Edit,Bash"
{"type":"system","subtype":"init","session_id":"abc","model":"claude-sonnet-4-6",...}
{"type":"assistant","message":{"role":"assistant","content":[...]}}
{"type":"result","subtype":"success","result":"Fixed the bug.","session_id":"abc"}

Delta Streaming (토큰 λ‹¨μœ„ μˆ˜μ‹ )

--include-partial-messages μΆ”κ°€ μ‹œ stream_event νƒ€μž…μ˜ delta μ΄λ²€νŠΈκ°€ μΆ”κ°€λœλ‹€:

claude -p "Explain recursion" \
  --output-format stream-json \
  --include-partial-messages
{"type":"stream_event","event":{"type":"content_block_delta","delta":{"type":"text_delta","text":"μž¬κ·€"}}}
{"type":"stream_event","event":{"type":"content_block_delta","delta":{"type":"text_delta","text":"λŠ” ν•¨μˆ˜κ°€"}}}
{"type":"result","subtype":"success","result":"...","session_id":"abc"}

jq둜 delta만 μΆ”μΆœ:

claude -p "Write a poem" --output-format stream-json --include-partial-messages | \
  jq -rj 'select(.type == "stream_event" and .event.delta.type? == "text_delta") | .event.delta.text'

μ–‘λ°©ν–₯ 톡신 (stdin + stdout λͺ¨λ‘ NDJSON)

claude -p \
  --input-format stream-json \
  --output-format stream-json \
  --replay-user-messages

stdin에 NDJSON ν˜•μ‹μ˜ μ‚¬μš©μž λ©”μ‹œμ§€λ₯Ό 보내고, stdoutμ—μ„œ 이벀트 μŠ€νŠΈλ¦Όμ„ λ°›λŠ”λ‹€:

← stdin: {"type":"user","message":{"role":"user","content":[{"type":"text","text":"Fix auth.py"}]}}
 
β†’ stdout: {"type":"user","message":{...}}     ← --replay-user-messages둜 echo됨
β†’ stdout: {"type":"assistant","message":{...}}
β†’ stdout: {"type":"result","result":"Fixed.","session_id":"abc"}

Codex JSON-RPC 2.0 vs Claude SDKMessage

두 μ‹œμŠ€ν…œ λͺ¨λ‘ NDJSON을 전솑 포맷으둜 μ‚¬μš©ν•˜μ§€λ§Œ, 쀄 μ•ˆμ˜ ꡬ쑰가 λ‹€λ₯΄λ‹€.

ν”„λ‘œν† μ½œ λΉ„κ΅ν‘œ

ν•­λͺ©Codex app-serverClaude Code CLI
전솑 포맷NDJSONNDJSON
λ‚΄λΆ€ ν”„λ‘œν† μ½œJSON-RPC 2.0 (κΈ°μ‘΄ ν‘œμ€€)SDKMessage (Claude 고유)
곡식 μ •μ˜jsonrpc.org μŠ€νŽ™SDKMessage νƒ€μž… μœ λ‹ˆμ˜¨ (TypeScript SDK 곡식 λ¬Έμ„œ)
λ©”μ‹œμ§€ ꡬ쑰jsonrpc / method / id / paramstype / subtype / session_id
μš”μ²­-응닡 λ§€μΉ­id ν•„λ“œλ‘œ λ§€μΉ­μ—†μŒ (단방ν–₯ push)
톡신 λ°©ν–₯μš”μ²­β†”μ‘λ‹΅ μ–‘λ°©ν–₯좜λ ₯ 단방ν–₯ (μ˜΅μ…˜μœΌλ‘œ μ–‘λ°©ν–₯ κ°€λŠ₯)

SDKMessage 곡식 νƒ€μž… μ •μ˜

좜처: Claude Agent SDK TypeScript Reference

type SDKMessage =
  | SDKSystemMessage           // μ„Έμ…˜ μ΄ˆκΈ°ν™”
  | SDKAssistantMessage        // Claude 응닡
  | SDKUserMessage             // μ‚¬μš©μž μž…λ ₯
  | SDKResultMessage           // μ΅œμ’… κ²°κ³Ό
  | SDKPartialAssistantMessage // delta streaming (stream_event)
  | SDKHookStartedMessage      // hook 이벀트
  | SDKHookResponseMessage
  | SDKRateLimitEvent
  | SDKPluginInstallMessage
  // ... 총 20개 νƒ€μž…

μ‹€μ œ wire 데이터 비ꡐ

같은 μž‘μ—… β€œFix the bug”λ₯Ό μš”μ²­ν•  λ•Œ:

# Codex app-server (JSON-RPC 2.0 over NDJSON)
# ──────────────────────────────────────────────────────────
← stdin:  {"jsonrpc":"2.0","method":"turn/start","params":{"message":"Fix the bug"},"id":1}

β†’ stdout: {"jsonrpc":"2.0","id":1,"result":{"threadId":"t_abc"}}               ← id:1 응닡 λ§€μΉ­
β†’ stdout: {"jsonrpc":"2.0","method":"item/agentMessage/delta","params":{"delta":"I'll fix"}}
β†’ stdout: {"jsonrpc":"2.0","method":"item/agentMessage/delta","params":{"delta":" the bug"}}
β†’ stdout: {"jsonrpc":"2.0","method":"turn/completed","params":{}}
# Claude Code CLI (SDKMessage over NDJSON)
# ──────────────────────────────────────────────────────────
← μ‹€ν–‰:   claude -p "Fix the bug" --output-format stream-json

β†’ stdout: {"type":"system","subtype":"init","session_id":"abc","model":"claude-sonnet-4-6"}
β†’ stdout: {"type":"stream_event","event":{"delta":{"type":"text_delta","text":"I'll fix"}}}
β†’ stdout: {"type":"stream_event","event":{"delta":{"type":"text_delta","text":" the bug"}}}
β†’ stdout: {"type":"result","subtype":"success","result":"Fixed.","session_id":"abc"}

두 좜λ ₯ λͺ¨λ‘ μ€„λ°”κΏˆμœΌλ‘œ 자λ₯Έλ‹€ = NDJSON 동일. 쀄 μ•ˆμ˜ ꡬ쑰가 method/id vs type/subtype = ν”„λ‘œν† μ½œ 닀름.

[Codex]  쀄 = {"jsonrpc":"2.0", "method":"...", "id":1, "params":{...}}
                                               ↑
                                         μš”μ²­-응닡 id λ§€μΉ­ 있음

[Claude] 쀄 = {"type":"stream_event", "session_id":"abc", "event":{...}}
                                               ↑
                                         μ„Έμ…˜ κ΅¬λΆ„λ§Œ, id λ§€μΉ­ μ—†μŒ

NDJSON은 β€œμ–΄λ–»κ²Œ 자λ₯Ό 것인가” (μ€„λ°”κΏˆμœΌλ‘œ JSON ꡬ뢄)이고, ν”„λ‘œν† μ½œμ€ β€œκ·Έ μ•ˆμ— 무엇을 담을 것인가”닀.

이벀트 νƒ€μž… μ°Έμ‘° (SDKMessage)

typesubtypeμ„€λͺ…
systeminitμ„Έμ…˜ μ‹œμž‘, λͺ¨λΈ/툴 정보
systemapi_retryAPI μž¬μ‹œλ„ 이벀트
assistantβ€”Claude 응닡 λ©”μ‹œμ§€
userβ€”μ‚¬μš©μž λ©”μ‹œμ§€ (replay μ‹œ)
stream_eventβ€”delta streaming 이벀트
resultsuccessμ΅œμ’… κ²°κ³Ό + session_id
resulterror_*였λ₯˜ κ²°κ³Ό

μ°Έκ³  λ¬Έμ„œ