스트리밍 응답

Streaming response

답변 전체가 준비될 때까지 기다리지 않고, 토큰(글자 조각) 단위로 생성되는 즉시 화면에 이어 붙이는 방식.

다른 이름: Token streamingTypewriter response
···
html
<div class="chat">
  <div class="bubble user">이번 분기 매출 요약해줘</div>
  <div class="bubble ai"><span id="txt"></span><span class="caret">▍</span></div>
</div>
css
.chat{width:min(280px,92%);display:grid;gap:8px}
.bubble{padding:9px 12px;border-radius:12px;font-size:12px;line-height:1.55;max-width:92%}
.bubble.user{justify-self:end;background:var(--accent);color:#fff;border-bottom-right-radius:4px}
.bubble.ai{justify-self:start;background:var(--surface);border:1px solid var(--line);color:var(--fg);border-bottom-left-radius:4px}
.caret{display:inline-block;color:var(--accent);animation:blink 1s step-end infinite}
@keyframes blink{50%{opacity:0}}
js
const full = '3분기 매출은 전 분기 대비 상승했고, 특히 구독 갱신 비중이 늘었어요. 신규 유입보다 재구매가 더 크게 기여했습니다.';
const el = document.getElementById('txt');
let i = 0, timer = null;
function step() {
  el.textContent = full.slice(0, i);
  i++;
  if (i <= full.length) { timer = setTimeout(step, 26 + Math.random() * 30); }
  else { timer = setTimeout(() => { i = 0; el.textContent = ''; step(); }, 1800); }
}
step();

LLM은 답을 한 번에 만들지 않고 다음 토큰을 순서대로 예측합니다. 스트리밍은 그 과정을 그대로 화면에 흘려보내는 것뿐이라 서버 쪽 구현은 SSE(Server-Sent Events)나 청크 단위 HTTP 응답으로 비교적 단순합니다. 클라이언트는 받은 조각을 기존 텍스트 끝에 계속 append합니다.

효과는 체감 속도에 있습니다. 전체 응답 시간은 똑같아도 첫 토큰이 도착하는 순간(TTFT, time to first token) 사용자는 "일이 진행되고 있다"는 신호를 받고, 끝까지 읽는 동안 자연스럽게 눈이 따라가므로 기다림 자체가 짧게 느껴집니다. 깜빡이는 커서(▍)는 "아직 끝나지 않았다"는 신호를 텍스트만으로는 주기 어려워서 덧붙이는 장치입니다.

마크다운이 스트리밍 중이면 리스트나 코드블록이 닫히지 않은 채로 잠깐 보일 수 있습니다 — 파서가 매 청크마다 불완전한 마크다운을 다시 그리므로 깜빡임이나 레이아웃 점프가 생기지 않게 렌더러를 고르는 게 중요합니다. 사용자가 스크롤을 올려 과거 내용을 보고 있으면 새 토큰이 도착해도 자동 스크롤을 강제하지 않아야 합니다.

언제 쓰나

응답이 한두 문장을 넘어가는 모든 대화형 UI의 기본값으로 씁니다. 응답이 아주 짧거나(예/아니오) 구조화된 UI로 바로 렌더될 거라면 스트리밍 효과가 오히려 부자연스러울 수 있습니다.