Streaming
같은 답변을 같은 토큰으로 잘라 렌더러 다섯 개에 동시에 흘려 보냅니다. 패널이 화면에 들어오면 시작합니다. 다음 세 가지를 보세요.
- 반쪽 마커(
**굵, 여는 백틱)가 텍스트로 보였다가 사라지는지 - 조사 바로 앞에서 끝나는 굵게(
**설정(config)**을)가 끝까지 렌더링되는지 - 강조나 표가 닫힐 때까지 화면이 멈춰 보이는지
WASM 빌드를 불러오는 중…
WASM 빌드를 불러오는 중…
- react-markdown 은 토큰이 올 때마다 누적된 텍스트 전체를 다시 렌더링합니다. 반쯤 쓴 마크업이 보였다가 사라집니다.
- Streamdown 도 다시 렌더링하지만, 닫히지 않은 구문을 먼저 보정합니다.
- mdwire → Streamdown 은 mdwire 스트리머를 앞에 둡니다. Streamdown 은 정규화된 누적 출력에
preview()를 붙인 텍스트를 받습니다. - mdwire
useMarkdownStream()은 보류 중인 내용(열린 굵게, 표의 행)도 입력이 여기서 끝난 것처럼 먼저 렌더링합니다. - mdwire
useMarkdownStream({ eager: false })는 확정된 출력만 보여 줍니다. 한 번 보인 내용은 바뀌지 않는 대신, 강조나 표에서 멈춰 보입니다.
DESIGN.md 에서는 에이전트가 쓴 한국어 문서 347건으로 이를 측정했습니다(Streamdown 2.6.0,
react-markdown 10.1.0 + remark-gfm). mdwire 스트리머를 Streamdown 앞에 두면 이미 보인 텍스트가
바뀌는 횟수가 약 60% 줄었고, 마커가 텍스트로 보이는 일이 사라졌습니다. 확정된 출력만 보여 주는
mdwire html 스트리머에서는 두 문제 모두 없었습니다. 다만 이 방식은 중간중간 멈춥니다.
이 샘플을 토큰 간격 60ms 로 흘리면 가장 긴 멈춤이 1,389ms 였고, 미리보기를 켜면 302ms 였습니다.
그래서 미리보기를 기본으로 켰습니다. 자세한 내용은
DESIGN.md 의 렌더러 앞단 절에 있습니다. 샘플
답변은 합성한 것이지만, 그 안의 형태는 모두 실제 에이전트 출력에서 깨졌던 것입니다.
이 데모의 코드
import { Streamer } from "@minjun0219/mdwire";
import { useMarkdownStream } from "@minjun0219/mdwire/react";
import { Streamdown } from "streamdown";
// mdwire → Streamdown: 정규화는 mdwire, 렌더링은 Streamdown
const s = new Streamer("github-markdown");
let done = "";
for (const token of tokens) {
done += s.push(token); // 확정된 출력. 다시 바뀌지 않음
setText(done + s.preview()); // + 보류 중인 내용을 입력이 끝난 것처럼 렌더링한 뒷부분
}
done += s.finish();
setText(done);
// <Streamdown>{text}</Streamdown>
// 또는 React 에서 바로 렌더링 (innerHTML 없이)
const live = useMarkdownStream(); // 보류 중인 내용도 먼저 렌더링(기본)
const settled = useMarkdownStream({ eager: false }); // 확정된 출력만
// tokens → live.push(token) … live.finish(); render {live.elements}전체 컴포넌트: site/src/components/StreamCompare.tsx. 번들러에서 WASM 을 불러오는 설정까지 넣어 같은 비교를 독립 Vite 앱으로 만든 예제는 examples/react-streaming에 있습니다.