잘려서 오는 응답을, 깨지지 않는 글자로
TextDecoder를 스트림 모드로 사용해 한글이 바이트 중간에서 잘리지 않게 하고, 두 청크에 걸친 마지막 줄은 버퍼에 보관했다가 다음 청크와 이어 붙였습니다. 스트림이 끝나면 남은 바이트와 버퍼를 마지막으로 비워 잘린 응답이 남지 않게 했습니다.
RAG · MULTI-TURN · STREAMING
국가기록원 생성형 AI
기록물 기반 질의응답에서 응답 스트리밍·멀티턴 대화·Markdown 표시를 구현하고, 사용자·관리자 앱의 독립 배포 환경을 구성했습니다.
질문에서 다음 질문으로,
끊김 없이 이어지는 대화.
01 / CONTEXT & ROLE
기록물을 검색하고 AI에 질문하는 과정에서, 답변이 생성되는 흐름과 이어지는 대화를 사용자가 자연스럽게 이해할 수 있는 화면이 필요했습니다. 사용자용 서비스와 관리자용 애플리케이션을 함께 개발하고 배포하는 환경도 구성해야 했습니다.
TypeScript·Next.js 기반 사용자·관리자 프론트엔드 개발, 응답 스트리밍 및 멀티턴 대화 UI, Markdown 렌더링, 모노레포 구성과 CI/CD 파이프라인 구축을 담당했습니다. 1년간 전체 커밋의 약 87%를 맡았습니다.
02 / DEEP DIVE
03 / ENGINEERING DECISIONS
TextDecoder를 스트림 모드로 사용해 한글이 바이트 중간에서 잘리지 않게 하고, 두 청크에 걸친 마지막 줄은 버퍼에 보관했다가 다음 청크와 이어 붙였습니다. 스트림이 끝나면 남은 바이트와 버퍼를 마지막으로 비워 잘린 응답이 남지 않게 했습니다.
요청마다 AbortController를 만들어 중단·화면 이탈 시 연결을 정리했습니다. 응답 콜백에서는 지금 살아 있는 요청인지 먼저 확인해, 늦게 도착한 이전 응답이 새 대화를 덮어쓰지 않도록 했습니다.
모델이 만든 Markdown과 HTML을 그대로 그리면 XSS 위험이 있습니다. rehype-sanitize를 적용한 변환 파이프라인과 DOMPurify 정제를 함께 두어, 화면에 들어가는 모든 경로에서 정제를 거치게 했습니다. 정제 결과는 메모이제이션해 입력마다 다시 계산하지 않습니다.
서버 데이터는 TanStack Query, 대화 진행 상태는 Redux Toolkit으로 역할을 나눴습니다. 사용자가 나중에 열어볼 그래프 데이터는 대화 스트림과 동시에 미리 받아두고 캐시를 공유해, 탭을 열었을 때 기다리지 않게 했습니다.
하나였던 앱을 사용자·관리자 두 앱과 공용 패키지(UI·서비스·타입)로 나눴습니다. 옮기기 전후 파일 해시를 비교해 내용이 바뀌지 않았음을 확인했고, 라우트가 앱 경계를 넘지 않는지 검사하는 스크립트를 빌드 단계에 넣었습니다. GitLab CI/CD와 Docker로 앱별 독립 배포를 구성했습니다.
04 / HOW IT CONNECTS
프론트엔드 담당 기능을 정리한 개념 흐름입니다. 스트림은 본문뿐 아니라 진행 상태와 참조 기록물을 함께 실어 오며, 표시 단계에서는 정제를 거칩니다.
05 / OUTCOME
보안 정책상 실제 서비스 화면 대신 UI 개념도로 대체했습니다.
06 / LET’S CONNECT
새로운 기회나 기술에 관한 대화, 편하게 연락 주세요.