-
AI 답변은 어떻게 실시간으로 나타날까?카테고리 없음 2026. 9. 25. 16:22
AI를 사용하다보면 조금은 특이한 UI를 만나게 됩니다.
완성된 화면이 나오는 것이 아니라, 실시간으로 답변이 완성되는 과정이 화면에 나옵니다.
이제는 너무나도 익숙한 경험이지만 프론트엔드 개발자 관점에서 AI 답변이 화면으로 완성되는 과정을 풀어봤습니다.
*이번 글에서는 fetch API를 통해 HTTP Streaming Response를 직접 읽는 방식을 기준으로, 데이터가 서버에서 브라우저 화면까지 도착하는 과정을 살펴봅니다.
일반적인 HTTP 요청부터 생각해보자.
먼저 우리가 평소 사용하던 API 요청을 생각해보겠습니다.
계약에 맞춰서 API 요청을 보내고 우리는 서버의 응답을 기다립니다.
서버가 다음과 같은 응답을 반환한다고 해보겠습니다.
{ "message": "Streaming은 데이터를 한 번에 모두 전달하지 않고..." }이 방식에는 특별한 문제가 없습니다. 대부분의 REST API는 이런 방식으로 충분합니다.
하지만 AI 서비스는 조금 다른 문제가 발생합니다.
LLM이 답변을 생성하는데 10초가 걸린다고 생각해보겠습니다.
서버에서 응답을 받고, 10초 동안 사용자는 어떤 경험을 하게 될까요?
하염없이 로딩 UI만 바라보고 있겠죠.
그렇다면, 서버가 답변을 전부 생성할 때까지 기다리지 않고, 생성된 데이터부터 바로 브라우저에 전달하면 어떨까요?
AI의 답변은 정말 "한 글자"씩 전송할까?
화면만 보면 서버에서 한 글자씩 데이터를 보내는 것 같지 않나요?
서버에서는 정말 한 글자씩 데이터를 보낼까요?
결론부터 말하자면 아닙니다.
조금 더 구체적으로는 화면에 글자가 점진적으로 나타난다는 사실과 서버가 한 글자씩 데이터를 전송한다는 것은 다른 얘기입니다.
네트워크에서는 데이터를 일정한 크기의 조각으로 나누어 전달할 수 있습니다.
그렇다고 한 글자씩 조각을 나누어 전달할 필요는 없습니다.
중요한 것은 답변 전체가 완성되기 전에 데이터가 점진적으로 도착한다는 것입니다.
그렇다면 HTTP Response는 원래 하나의 응답인데, 서버는 어떻게 아직 끝나지 않은 데이터를 계속 보낼 수 있을까요?
서버는 끝나지 않은 HTTP Response를 어떻게 보낼까?
브라우저의 fetch API를 이용해 HTTP 요청을 보내면 Response 객체를 받습니다.
브라우저에서 제공하는 Web API인 ReadableStream은 Response.body 입니다.
이름 그대로 읽을 수 있는 Stream입니다.
이를 통해서 데이터 전체가 준비되기를 기다리지 않고, 도착하는 데이터를 순차적으로 처리할 수 있습니다.
그런데 여기서 또 하나의 문제가 있습니다.
서버로부터 HTTP Response 받은 데이터를 읽어서 Stream할 수 있다는 개념은 알겠는데,
ReadableStream에서 읽은 데이터는 우리가 기대하는 Javascript 문자열이 아닙니다.
ReadableStream에서 읽은 데이터는 왜 문자열이 아닐까?
결론적으로, Stream에서 읽는 값은 byte 데이터입니다.
자바스크립트에서는 이를 Uint8Array 형태로 만나게 됩니다.
Uint8Array는 Unit(Unsigned Integer, 부호 없는 정수) + 8(숫자 하나를 8비트로 저장) + Array의 줄임말로
요약하자면 부호 없는 8비트 정수의 배열 정도로 이해하면 되겠습니다.
즉 브라우저는 "안녕하세요." 라는 자바스크립트 문자열을 바로 읽는 것이 아니라,
[236, 149, 136, ...] 과 같이 문자열을 표현하고 있는 byte sequence를 읽습니다.
중요한 것은, 네트워크에서 전달되는 데이터와 자바스크립트 애플리케이션에서 사용하는 문자열 사이에는 변환 과정이 필요하다는 것입니다.
그렇다면 이 byte들을 어떻게 다시 "안녕하세요." 라는 문자열로 바꿀 수 있을까요?
Byte가 어떻게 Javascript 문자열이 될까? - TextDecoder
여기서 등장하는 Web API가 TextDecoder입니다.
TextDecoder의 역할은 간단한데요,
byte sequence를 특정 문자 인코딩 규칙에 따라 자바스크립트 문자열로 변환합니다.
왜 이런 과정이 필요하게 될까요?
컴퓨터와 네트워크는 결국 byte를 다루지만, 우리의 언어로 표현되는 것은 문자이기 때문입니다.
그리고 Streaming에서는 한 가지 더 고려해야 할 부분이 있습니다.
Stream에서 읽는 chunk의 경계와 문자의 경계는 일치하지 않을 수 있습니다.
무슨 말이냐면 예를 들어서, "가"는 UTF-8에서 하나의 byte가 아니라 [234, 176, 128] 이라는 3byte로 표현됩니다.
그런데 Streaming에서는 첫 번째 chunk에서 [234, 176] 까지만 읽고, 다음 chunk에서 나머지 [128]을 읽게 될 수도 있습니다.
즉, 하나의 문자가 두 개의 chunk에 나눠서 전달될 수 있습니다.
그래서 Streaming에서는 TextDecoder가 이전 chunk에서 완성되지 않은 byte를 기억했다가 다음 chunk와 이어서 문자열을 복원할 수 있도록 사용합니다.
결국 Stream에서 읽은 chunk는 완벽한 문자열이 될 수 없는 가능성이 있으므로 올바르게 이어주는 과정이 필요합니다.
문자열은 어떻게 화면에 나타날까?
서버에서 받는 데이터 조각을 읽고, 디코딩 과정을 통해 문자열로 받는 과정까지 알아보았습니다.
이제 프론트엔드 애플리케이션에서 익숙하게 다룰 수 있는 문자열로 화면에 나타낼 수 있습니다.
예를 들어, 처음에는 "Streaming 은 " 이 도착하고, 다음에는 "데이터를 "이 도착할 수 있습니다.
프론트엔드는 이 데이터를 기존 답변에 계속 누적하면서, 문장이 되는 화면을 만들어 나갑니다.
"Streaming은 데이터를 점진적으로 ..."
React라면 state가 되고, Angular라면 Signal이 상태가 될 수 있습니다.
중요한 것은 어떤 프레임 워크를 사용하는지가 아니라,
브라우저가 HTTP Response를 Stream으로 읽고, byte를 문자열로 변환하고, React나 Angular는 그 결과로 만들어진 문자열을 애플리케이션 상태에 반영하고 변경된 상태를 화면에 렌더링한다는 흐름입니다.
요약
이제 처음에 봤던 전체 흐름으로 돌아가 보겠습니다.
AI가 생성한 답변이 브라우저 화면에 나타나기까지는 다음과 같은 과정을 거칩니다.
Server → HTTP Response → ReadableStream → Uint8Array → TextDecoder → string → Application State → UI
각 단계의 역할을 하나씩 연결해보면 다음과 같습니다.
Server AI가 생성한 결과를 점진적으로 Response에 전달 HTTP Response 서버에서 브라우저로 데이터를 전달 ReadableStream Response Body를 도착하는 순서대로 읽을 수 있게 함 Uint8Array Stream에서 읽은 byte 데이터 TextDecoder byte 데이터를 JavaScript 문자열로 변환 string 애플리케이션에서 사용할 수 있는 텍스트 Application State 전달받은 텍스트를 기존 답변에 누적 UI 변경된 상태를 화면에 렌더링 
결론
fetch API를 통해 HTTP Streaming Response를 직접 읽는 방식을 기준으로, 데이터가 서버에서 브라우저 화면까지 도착하는 과정을 이해하기 쉽게 풀어봤습니다.
흐름 자체는 단순해 보이지만, 실제 Streaming UI를 구현하기 시작하면 프론트엔드 개발자가 고민해야 할 문제들이 하나씩 생겨납니다.
byte를 올바르게 문자열로 변환해서 완전한 문장으로 완벽하게 구현할 수 있을까요?
Stream이 종료되거나 끊어질 때는 어떻게 매끄럽게 사용자에게 전달해야 할까요?
답변을 다양한 방식으로 표현하고 싶어서 서버가 텍스트 외에 다른 데이터를 요청하면 어떻게 해야할까요?
이 방식 말고, SSE를 어떤 방식으로 더 안전하고, 간편하게 풀어볼 수 있을까요?
결국 Streaming Chat을 만든다는 것은 단순히 서버에서 전달되는 문자열을 화면에 이어 붙이는 것만으로 끝나지 않습니다.
데이터를 어떻게 읽을지, 어디까지를 하나의 메세지로 볼지, 어떤 상태를 가질지, 사용자에게 어떻게 보여줄지 함께 설계해야 합니다.