본문 바로가기
C.W.K.
Stream
Lesson 01 of 04 · published

NDJSON, SSE 아냐

~18 min · streaming, ndjson

Level 0Downloader
0 XP0/41 lessons0/11 achievements
0/120 XP to next level120 XP to go0% complete

Streaming 가장 중요한 단 하나의 차이

Cloud API들 (OpenAI, Anthropic, Google)은 Server-Sent Events (SSE)로 stream — 각 chunk가 data: {json}\n\n. Ollama는 newline-delimited JSON (NDJSON)으로 stream — 각 chunk가 그냥 {json}\n. data: prefix 없고 double newline 없어.

Ollama에 SSE 파서를 물리면 응답을 거대한 blob 하나로 보고, 영원히 안 올 data:를 기다려. JSON 파서를 물리면 streaming 응답 전체를 값 하나로 읽다가 두 번째 {를 만나는 순간 깨지고.

NDJSON 줄 하나의 모양

각 줄은 그 자체로 온전한 JSON 객체야. 눈여겨볼 필드는 이것들:

  • message.content — 텍스트 delta (토큰 하나 또는 몇 개).
  • done — boolean. Incremental chunk면 false, 마지막이면 true.
  • 마지막 chunk에만 붙는 timing 필드 (total_duration, eval_count, eval_duration 등). 성능 metric 뽑을 때 써.

Ollama가 NDJSON 고른 이유

NDJSON은 server 쪽에서 만들기 단순하고, client 쪽에서 읽기도 단순해 (newline으로 자르고, 줄마다 JSON parse). SSE의 keep-alive comment 의식 같은 것도 필요 없고. 대신 치르는 값은 딱 하나야 — SSE library에 손대기 전에 이게 NDJSON이란 걸 먼저 알아야 한다는 것.

Code

Streaming 응답의 해부도·typescript
// stream:true /api/chat에서 wire로 받는 거
{"model":"qwen2.5:7b","created_at":"2026-05-03T10:30:00Z","message":{"role":"assistant","content":"The"},"done":false}
{"model":"qwen2.5:7b","created_at":"2026-05-03T10:30:00Z","message":{"role":"assistant","content":" sky"},"done":false}
{"model":"qwen2.5:7b","created_at":"2026-05-03T10:30:00Z","message":{"role":"assistant","content":" is"},"done":false}
// ...delta chunk 많이...
// 마지막 chunk:
{"model":"qwen2.5:7b","created_at":"2026-05-03T10:30:01Z","message":{"role":"assistant","content":""},"done":true,"total_duration":1234567890,"eval_count":42,"eval_duration":900000000}
SSE library에 손 대지 마·python
# 잘못 — Ollama에 sseclient 쓰면 영원히 hang
# import sseclient  # ❌

# 옳음 — raw line 읽고 각 줄 JSON parse
import httpx, json

with httpx.stream("POST", "http://localhost:11434/api/chat",
                  json={"model": "qwen2.5:7b",
                        "messages": [{"role": "user", "content": "hi"}],
                        "stream": True},
                  timeout=None) as r:
    for line in r.iter_lines():
        if not line:
            continue
        chunk = json.loads(line)
        print(chunk["message"]["content"], end="", flush=True)
        if chunk.get("done"):
            break

External links

Exercise

Curl로 streaming /api/chat request 보내고 jq로 line-by-line pipe. 각 줄이 독립적으로 parse되는지 확인. 그다음 좋아하는 언어로 raw line iteration을 써서 같은 request를 짜봐 (SSE library는 쓰지 마).

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.