LLM을 프론트엔드에 연결하는 세 가지 설계 결정

LLM을 프론트엔드에 연결하는 세 가지 설계 결정

SSE 토큰 스트리밍·Claude API 자동화·MCP 표준화가 함께 가리키는 것—AI를 실제 제품에 붙이는 일은 API 호출이 아니라 연결 구조를 설계하는 일이다

LLM 스트리밍 SSE MCP Claude API Next.js AI 토큰 스트리밍 AI 통합 설계
광고

"로딩 스피너는 거짓말이다." dev.to에 올라온 SSE 스트리밍 구현기의 첫 문장이다. 무언가 일어나고 있다는 신호만 줄 뿐, 무엇이 일어나는지는 말해주지 않는다는 뜻이다. LLM이 15~40초에 걸쳐 텍스트를 생성할 때, 완성된 응답을 한 번에 던지는 것과 토큰 단위로 흘려보내는 것은 기술적으로 같은 대기 시간이지만 사용자 경험으로는 완전히 다른 제품이다. 이 차이를 만들어내는 것이 바로 설계 결정이다.

최근 세 편의 실전 구현 사례가 서로 다른 층위에서 같은 질문을 던지고 있다. LLM을 실제 프론트엔드 제품에 연결할 때, 어떤 구조적 선택이 경험의 질을 결정하는가. SSE 토큰 스트리밍, Claude API 기반 블로그 자동화, 그리고 MCP라는 새로운 연결 표준—세 사례를 관통하는 공통 질문은 하나다. AI를 '붙이는 것'과 '제대로 작동하게 만드는 것' 사이의 간극을 어떻게 메울 것인가.

첫 번째 결정: 스트리밍 방식과 취소 신호의 연결

SSE 토큰 스트리밍 구현기(dev.to/pavelespitia)가 가장 먼저 짚는 것은 EventSource 대신 fetch + ReadableStream을 선택하는 이유다. 브라우저 내장 EventSource는 재연결을 자동으로 처리해주지만 GET 요청만 지원한다. 모델 선택이나 컨텍스트 데이터를 POST 바디로 전달해야 하는 실전 시나리오에서는 쓸 수 없다. fetchresponse.body.getReader()를 직접 다루는 방식이 번거롭지만, 그 대가로 AbortController를 통한 명시적 취소 제어권을 얻는다.

이 취소 신호가 핵심이다. 구현기에서 가장 인상적인 부분은 request.signal이 Next.js Route Handler에서 모델 fetch 호출까지 체인으로 전달된다는 점이다. 사용자가 UI에서 중단 버튼을 누르는 순간, 브라우저 → Next.js → 모델 요청까지 취소가 전파된다. GPU를 40초 동안 태우는 고아 작업이 생기지 않는다. 여기에 nginx 버퍼링을 막는 X-Accel-Buffering: no 헤더와 프록시 압축을 차단하는 no-transform이 더해져야 토큰이 실제로 실시간으로 흘러내린다. 헤더 하나가 빠지면 스트리밍 전체가 무의미해지는 구조다.

구현 패턴으로 보면, 서버는 모델의 SSE 클라이언트이자 브라우저의 SSE 서버로 동시에 동작한다. streamModel 함수가 비동기 제너레이터로 텍스트 델타를 추출하고, Route Handler가 이를 ReadableStream으로 감싸 재방출한다. Ollama든 Claude SDK든 제너레이터 인터페이스만 동일하면 나머지 파이프라인은 교체 없이 유지된다. 이 추상화가 유지보수 비용을 결정한다.

두 번째 결정: AI 에이전트에게 API를 주는 범위

Claude API로 블로그 자동 발행을 구현한 사례(dev.to/sensational5510)는 다른 층위의 설계 결정을 보여준다. 질문은 간단하다. Claude Code 세션에서 터미널에 있는 API 키를 그대로 활용해 블로그에 글을 발행할 수 있다면, 워크플로우가 어떻게 달라지는가. 구현 자체는 단순하다. POST /api/ai/posts 엔드포인트에 Zod 검증과 슬러그 자동 생성을 붙이면 Claude가 직접 드래프트를 올릴 수 있다.

그런데 실제 구현 과정에서 bcrypt 해시가 두 군데에서 깨지는 일이 벌어진다. Next.js의 환경변수 로더가 $2b$10$ 같은 bcrypt 해시 문자열의 $를 변수 참조로 해석해 값을 지워버린다. 싱글 쿼트도 막지 못한다. 이 문제는 AI 통합 구현기에서 자주 등장하는 패턴의 변형이다. 외부 도구와 연결하는 순간, 각 레이어의 파싱 규칙이 예상치 못한 방식으로 충돌한다. 해결책은 로컬에서는 원본 키를 타이밍 세이프 비교로 처리하고, 프로덕션에서는 Vercel 환경변수 UI를 통해 $ 해석을 우회하는 이중 전략이다.

이 사례에서 더 흥미로운 것은 실제 사용 패턴이다. AI가 러프 드래프트를 올리면 사람이 에디터에서 다듬는다. AI가 중간에 섹션을 추가하면 사람이 리프레시해서 확인한다. AI는 초안의 생산을 담당하고 사람은 편집 판단을 담당하는 분업이 워크플로우 안에 자연스럽게 자리잡는다. '무엇을 자동화하고 무엇을 사람이 할 것인가'라는 경계 설계가 도구 구현만큼 중요하다.

세 번째 결정: 표준 연결 레이어를 선택하는 시점

Model Context Protocol(MCP)을 'USB-C'에 비유한 아티클(dev.to/ayas_tech_2b0560ee159e661)은 더 긴 시간 축의 설계 결정을 다룬다. 지금까지 LLM에 외부 데이터를 연결하는 표준적인 방법은 Function Calling이었다. JSON 스키마를 정의하고, 실제 함수 구현은 따로 작성하고, 프로바이더마다 다른 형식을 맞추는 작업이 반복됐다. 스키마와 구현이 분리되어 있어 하나를 바꾸면 다른 쪽을 잊기 쉽고, 같은 도구를 다른 AI 앱에서 재사용하려면 처음부터 다시 써야 했다.

MCP는 이 문제를 프로토콜 표준화로 해결한다. MCP 서버를 한 번 구현해두면, Claude Desktop이든 커스텀 에이전트든 MCP를 지원하는 어떤 클라이언트든 동일하게 연결할 수 있다. Python SDK 기준으로 타입 힌트와 독스트링에서 스키마가 자동 생성되기 때문에 정의와 구현의 불일치 문제도 사라진다. Function Calling이 여전히 유효한 영역은 MCP 서버를 띄울 필요가 없는 단발성 전용 액션이나 레거시 시스템 래핑 같은 경우다. 두 방식은 대체 관계가 아니라 역할 분담이다.

세 가지 결정이 가리키는 방향

세 사례를 나란히 놓으면 패턴이 보인다. 스트리밍 구현은 '취소 신호가 모델까지 전달되는가'를 묻고, 블로그 자동화는 '에이전트에게 어디까지 권한을 줄 것인가'를 묻고, MCP는 '이 연결을 재사용 가능한 구조로 만들 것인가'를 묻는다. 세 질문 모두 API 호출 방법이 아니라 연결의 범위와 책임을 어디에 둘 것인가에 관한 설계 결정이다.

프론트엔드 개발자 입장에서 이 흐름이 의미하는 것은 분명하다. LLM을 제품에 연결하는 일은 점점 더 쉬워지고 있다. SDK가 추상화해주고, MCP가 표준화해주고, 스트리밍 패턴이 정형화되고 있다. 그렇기 때문에 오히려 '어떤 구조로 연결하느냐'의 판단이 제품의 완성도를 가르는 결정적 변수가 된다. 연결을 켜는 것과 연결이 제대로 작동하게 만드는 것은 여전히 다른 문제다. 그 간극을 채우는 것이 설계 판단이고, 그 판단은 아직 사람의 몫이다.

출처

더 많은 AI 트렌드를 Seedora 앱에서 확인하세요