전체 프로젝트

RAG · MULTI-TURN · STREAMING

AI의 답변이 사용자의 경험이 되기까지.

국가기록원 생성형 AI

기록물 기반 질의응답에서 응답 스트리밍·멀티턴 대화·Markdown 표시를 구현하고, 사용자·관리자 앱의 독립 배포 환경을 구성했습니다.

PERIOD
2025.09 — 2026.09
ROLE
프론트엔드 선임
PROJECT
팀 프로젝트
Next.jsTypeScriptReactRedux ToolkitTanStack QueryVitestGitLab CI/CDDockerNginx
NATIONAL ARCHIVESUI CONCEPT
기록 속에서 답을 찾다
맥락을 이해하는 기록 검색

질문에서 다음 질문으로,
끊김 없이 이어지는 대화.

참고 기록물
RETRIEVE. CONNECT. ANSWER.↗

01 / CONTEXT & ROLE

어떤 문제를 풀었나요?

기록물을 검색하고 AI에 질문하는 과정에서, 답변이 생성되는 흐름과 이어지는 대화를 사용자가 자연스럽게 이해할 수 있는 화면이 필요했습니다. 사용자용 서비스와 관리자용 애플리케이션을 함께 개발하고 배포하는 환경도 구성해야 했습니다.

제가 맡은 일

TypeScript·Next.js 기반 사용자·관리자 프론트엔드 개발, 응답 스트리밍 및 멀티턴 대화 UI, Markdown 렌더링, 모노레포 구성과 CI/CD 파이프라인 구축을 담당했습니다. 1년간 전체 커밋의 약 87%를 맡았습니다.

02 / DEEP DIVE

끊겨서 도착하는 응답을 어떻게 끊기지 않는 화면으로 만들었나요?

문제
응답은 네트워크 사정에 따라 잘려서 도착합니다. 한글이 글자 중간에서 끊기거나 줄 하나가 두 청크에 걸치면 화면에 깨진 글자가 보였습니다. 사용자가 답변을 중단하거나 곧바로 다음 질문을 보내면, 이미 출발한 이전 응답이 새 화면에 섞여 들어올 수 있었습니다.
접근
TextDecoder를 스트림 모드로 사용해 멀티바이트 문자가 잘리지 않게 하고, 완성되지 않은 마지막 줄은 버퍼에 보관했다가 다음 청크와 이어 붙였습니다. 요청마다 AbortController를 만들어 중단·이탈 시 연결을 정리하고, 콜백에서는 현재 살아 있는 요청인지 확인한 뒤에만 화면에 반영했습니다. 스트림은 본문뿐 아니라 진행 상태·참조 기록물·추천 질문을 함께 실어 오기 때문에, 청크 종류별로 나눠 각각의 화면 영역에 연결했습니다.
결과
사용자는 답변이 완성되기 전부터 도착한 내용을 읽고, 중단하거나 곧바로 다음 질문을 이어갈 수 있습니다. 중단한 답변은 그 시점까지의 내용이 남고, 늦게 도착한 이전 응답이 새 대화를 덮어쓰지 않습니다.
예시 데이터로 스트리밍 UI 살펴보기

03 / ENGINEERING DECISIONS

구현을 이끈 기술적 결정.

01

잘려서 오는 응답을, 깨지지 않는 글자로

TextDecoder를 스트림 모드로 사용해 한글이 바이트 중간에서 잘리지 않게 하고, 두 청크에 걸친 마지막 줄은 버퍼에 보관했다가 다음 청크와 이어 붙였습니다. 스트림이 끝나면 남은 바이트와 버퍼를 마지막으로 비워 잘린 응답이 남지 않게 했습니다.

TextDecoderChunk bufferingSSE
02

중단해도, 곧바로 다시 물어도 섞이지 않게

요청마다 AbortController를 만들어 중단·화면 이탈 시 연결을 정리했습니다. 응답 콜백에서는 지금 살아 있는 요청인지 먼저 확인해, 늦게 도착한 이전 응답이 새 대화를 덮어쓰지 않도록 했습니다.

AbortControllerRace conditionMulti-turn
03

AI가 만든 문서를, 안전하게 화면에

모델이 만든 Markdown과 HTML을 그대로 그리면 XSS 위험이 있습니다. rehype-sanitize를 적용한 변환 파이프라인과 DOMPurify 정제를 함께 두어, 화면에 들어가는 모든 경로에서 정제를 거치게 했습니다. 정제 결과는 메모이제이션해 입력마다 다시 계산하지 않습니다.

DOMPurifyrehype-sanitizeXSS
04

기다림을 줄이는 선행 요청

서버 데이터는 TanStack Query, 대화 진행 상태는 Redux Toolkit으로 역할을 나눴습니다. 사용자가 나중에 열어볼 그래프 데이터는 대화 스트림과 동시에 미리 받아두고 캐시를 공유해, 탭을 열었을 때 기다리지 않게 했습니다.

TanStack QueryPrefetchRedux Toolkit
05

함께 관리하고, 독립적으로 배포하기

하나였던 앱을 사용자·관리자 두 앱과 공용 패키지(UI·서비스·타입)로 나눴습니다. 옮기기 전후 파일 해시를 비교해 내용이 바뀌지 않았음을 확인했고, 라우트가 앱 경계를 넘지 않는지 검사하는 스크립트를 빌드 단계에 넣었습니다. GitLab CI/CD와 Docker로 앱별 독립 배포를 구성했습니다.

MonorepoGitLab CI/CDDocker

04 / HOW IT CONNECTS

기능을 연결하는 흐름.

  1. 01질문 입력멀티턴 대화 UI
  2. 02응답 수신SSE · TextDecoder
  3. 03청크 해석본문 · 참조 · 추천 분리
  4. 04안전한 표시Markdown · 정제

프론트엔드 담당 기능을 정리한 개념 흐름입니다. 스트림은 본문뿐 아니라 진행 상태와 참조 기록물을 함께 실어 오며, 표시 단계에서는 정제를 거칩니다.

05 / OUTCOME

서비스에 남긴 변화.

  • 청크 경계·중단·경쟁 상태를 처리한 스트리밍 대화 UI 구현
  • Vitest·Testing Library 기반 테스트 177개로 회귀 확인
  • 단일 앱을 모노레포로 전환하고 앱별 독립 배포 환경 구축

자료 안내

보안 정책상 실제 서비스 화면 대신 UI 개념도로 대체했습니다.

NEXT CASE STUDY

GenWave

06 / LET’S CONNECT

함께 풀어갈 문제를
기다립니다.

새로운 기회나 기술에 관한 대화, 편하게 연락 주세요.