Hono + Cloudflare Workers로 LLM 스트리밍 API 배포하기 — 타입 안전한 AI 레이어를 엣지에서 운영하는 방법
저는 Express로 만든 LLM 프록시가 Vercel에서 콜드 스타트 때문에 매 요청마다 2초씩 걸리는 걸 보면서, "이게 맞나?" 싶었던 시절이 있었습니다. 그 다음엔 NestJS에 @nestjs/event-emitter로 SSE를 억지로 구겨 넣다가, 구조가 너무 복잡해져서 결국 손을 놓았습니다. Hono를 처음 만난 건 바로 그 시점이었고, 지금은 AI API 레이어를 설계할 때 거의 반사적으로 꺼내게 됐습니다.
이 글의 핵심은 하나입니다: Web Standards 기반 설계 덕분에 코드 한 벌로 엣지부터 Lambda까지 어디서든 실행되며, LLM 스트리밍과 타입 안전성까지 최소한의 코드로 챙길 수 있습니다. 2026년 현재 React 프론트엔드 + Hono API(Cloudflare Workers) + 모델 프로바이더 구성이 AI 스타트업의 표준 스택으로 자리 잡은 데는 이유가 있습니다. Cloudflare, Deno, Clerk, Unkey 같은 곳들이 프로덕션에서 이미 쓰고 있고, Vercel과 Cloudflare 모두 공식 통합 문서를 제공합니다.
TypeScript와 REST API 기본 개념이 있으면 이 글을 따라오는 데 충분합니다. NestJS나 Vercel 경험이 없어도 괜찮습니다.
핵심 개념
Hono가 AI API에 맞는 이유
Hono는 ~14KB짜리 TypeScript 웹 프레임워크입니다. 숫자만 보면 "그냥 작은 Express 아닌가?" 싶지만, 설계 철학이 다릅니다.
| 특징 | Express/Fastify | Hono |
|---|---|---|
| 기반 API | Node.js http 모듈 |
Web Standards (Request/Response) |
| 런타임 | Node.js 전용 | Workers, Bun, Deno, Lambda, Node.js |
| 스트리밍 | 별도 설정 필요 | streamSSE() 헬퍼 내장 |
| 타입 안전 RPC | 없음 | hc 클라이언트로 end-to-end 타입 |
| Cold start | 런타임 의존 | 0에 가까움 (Workers 기준) |
AI API는 본질적으로 I/O 바운드 워크로드입니다. LLM이 토큰을 생성하는 동안 서버는 결과를 기다렸다가 클라이언트로 중계하는 게 전부입니다. 이런 워크로드에서 Node.js 이벤트 루프의 무거운 초기화 비용은 낭비입니다. 반면 Cloudflare Workers 같은 엣지 환경은 요청별로 격리된 V8 컨텍스트에서 실행되어 대기 시간이 최소화되고, 지리적으로 분산된 엣지 노드가 사용자와의 물리적 거리를 줄여줍니다. 초경량 프레임워크가 이 워크로드에 딱 맞는 이유가 여기 있습니다.
SSE(Server-Sent Events) — HTTP 연결을 열어두고 서버가 클라이언트로 단방향 이벤트 스트림을 보내는 방식입니다. LLM이 토큰을 생성할 때마다 실시간으로 클라이언트에 전달하는 ChatGPT 스타일 응답이 대표적인 사례입니다.
Web Standards 기반이 가져다주는 이점
Hono 라우트 핸들러가 받는 Request, 반환하는 Response는 브라우저 fetch()와 동일한 표준 객체입니다. 이게 왜 중요하냐면, Cloudflare Workers에서 작성한 코드가 Bun이나 Vercel Edge에서도 그대로 동작하기 때문입니다. Node.js 전용 http 모듈을 쓰는 Express는 런타임별로 별도 어댑터가 필요하지만, Hono는 그렇지 않습니다.
// 이 코드는 Cloudflare Workers, Bun, Vercel Edge 어디서나 동일하게 동작합니다
import { Hono } from 'hono'
const app = new Hono()
app.get('/health', (c) => {
return c.json({ status: 'ok', runtime: 'any' })
})
export default appAI 모델 프로바이더를 교체하거나 배포 플랫폼을 옮길 때 프레임워크 API를 다시 익힐 필요가 없다는 건, 생각보다 큰 장점입니다. 프로젝트 초기에 "일단 Bun으로 개발하고 나중에 Workers로 올리자"가 그냥 되니까요.
실전 적용
예시 1: LLM 토큰 스트리밍 엔드포인트
가장 기본이 되는 패턴입니다. 외부 LLM API(OpenAI, Anthropic 등)를 프록시하면서 토큰을 실시간으로 클라이언트에 전달해야 할 때 쓰는 구조입니다. 처음 팀에 이 패턴을 도입했을 때, "이게 이렇게 짧아도 되는 거야?" 반응이 제일 많았습니다.
import { Hono } from 'hono'
import { streamSSE } from 'hono/streaming'
import { streamText } from 'ai'
import { openai } from '@ai-sdk/openai'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
const app = new Hono()
const chatSchema = z.object({
messages: z.array(
z.object({
role: z.enum(['user', 'assistant']),
content: z.string(),
})
),
})
app.post('/api/chat', zValidator('json', chatSchema), async (c) => {
const { messages } = c.req.valid('json')
return streamSSE(c, async (stream) => {
try {
const { textStream } = await streamText({
model: openai('gpt-4o'),
messages,
})
for await (const text of textStream) {
await stream.writeSSE({ data: JSON.stringify({ text }) })
}
await stream.writeSSE({ data: '[DONE]' })
} catch (err) {
// 스트림 도중 에러가 나면 이미 200 헤더가 나간 상태이므로
// 에러 이벤트를 스트림으로 전달해야 합니다
await stream.writeSSE({
event: 'error',
data: JSON.stringify({ message: 'LLM API 오류가 발생했습니다' }),
})
}
})
})
export default app| 코드 포인트 | 역할 |
|---|---|
zValidator('json', chatSchema) |
요청 본문을 Zod 스키마로 검증, 실패 시 자동 400 반환 |
c.req.valid('json') |
검증된 데이터를 타입 안전하게 꺼냄 |
streamSSE(c, ...) |
SSE 헤더 설정과 스트림 관리를 Hono가 처리 |
stream.writeSSE({ event: 'error', ... }) |
스트리밍 도중 에러를 클라이언트에 전달하는 패턴 |
zValidator미들웨어 —@hono/zod-validator패키지가 제공하는 Hono 전용 미들웨어입니다. 검증 실패 시 자동으로 400 응답을 반환하고, 이후 핸들러에서는 검증된 타입의 데이터만 접근할 수 있습니다.
예시 2: Cloudflare Workers AI와 직접 통합
외부 API 비용이 부담스럽거나, 프로토타입을 빠르게 만들고 싶을 때 유용한 패턴입니다. Cloudflare Workers에는 c.env.AI 바인딩으로 접근할 수 있는 내장 AI 모델이 있어서, 별도 API 키 없이 Cloudflare의 GPU를 활용할 수 있습니다.
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
type Bindings = {
AI: Ai
}
const app = new Hono<{ Bindings: Bindings }>()
const generateSchema = z.object({ prompt: z.string().min(1) })
app.post('/api/generate', zValidator('json', generateSchema), async (c) => {
const { prompt } = c.req.valid('json')
const response = await c.env.AI.run('@cf/meta/llama-3.1-8b-instruct', {
prompt,
stream: true,
})
// Workers AI SDK 타입 정의가 stream: true 반환값을 ReadableStream으로
// 좁히지 않아서 타입 단언이 필요합니다
return new Response(response as ReadableStream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
},
})
})
export default appwrangler.toml에 AI 바인딩을 추가하면 바로 사용할 수 있습니다:
[ai]
binding = "AI"모델 가용성 주의 —
@cf/meta/llama-3.1-8b-instruct는 작성 시점 기준입니다. Cloudflare Workers AI의 지원 모델 목록과 비용 정책은 변경될 수 있으므로, 공식 문서에서 현재 상태를 확인하는 것을 권장합니다.
예시 3: 타입 안전 RPC로 마이크로서비스 간 통신
AI 애플리케이션이 커지면 임베딩 서비스, 요약 서비스처럼 역할별로 API를 분리하고 싶어집니다. 이때 쓸 수 있는 게 Hono의 RPC 기능입니다.
tRPC를 이미 쓰고 계신다면 "왜 Hono RPC를?"이라는 질문이 자연스럽게 나올 텐데요. tRPC는 프론트엔드-백엔드 통합에 더 특화되어 있고, OpenAPI codegen은 별도 빌드 단계가 필요합니다. Hono RPC는 서버 타입을 클라이언트가 직접 import하는 방식이라, 코드 생성 도구 없이 서비스 간 타입 안전성을 유지할 수 있습니다. 마이크로서비스끼리 이미 Hono로 구축되어 있다면 가장 마찰이 적은 선택입니다.
솔직히 처음 봤을 때 "이게 되는 거라고?" 싶었습니다.
// embed-service/src/index.ts
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
const embedSchema = z.object({ text: z.string().min(1) })
const app = new Hono()
const route = app.post(
'/api/embed',
zValidator('json', embedSchema),
async (c) => {
const { text } = c.req.valid('json')
// 실제 임베딩 로직은 모델 프로바이더에 따라 교체해서 사용하시면 됩니다
const embedding: number[] = Array(1536).fill(0) // 더미 구현
return c.json({ embedding, dimensions: embedding.length })
}
)
// 서버 타입을 export — 클라이언트가 이 타입을 import합니다
export type AppType = typeof route
export default app// another-service/src/client.ts
import { hc } from 'hono/client'
import type { AppType } from '../../embed-service/src/index'
const client = hc<AppType>('https://embed-service.workers.dev')
async function getEmbedding(text: string) {
// IDE에서 자동완성과 타입 체크가 동작합니다
const res = await client.api.embed.$post({ json: { text } })
const { embedding } = await res.json()
return embedding
}Hono RPC — OpenAPI 스펙이나 별도 코드 생성 없이, 서버 라우트 타입을 그대로 클라이언트에서 재사용하는 방식입니다. gRPC의 타입 안전성을 REST 위에서 구현한다고 이해하면 됩니다.
장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 초경량 | ~14KB, 의존성 최소화로 Workers 환경에서 Cold start 제로에 수렴 |
| 런타임 독립 | 단일 코드베이스로 Workers, Bun, Lambda, Vercel Edge 모두 배포 가능 |
| 스트리밍 내장 | streamSSE(), streamText() 헬퍼로 LLM 스트리밍 구현이 단순함 |
| 타입 안전 RPC | hc 클라이언트로 코드 생성 없이 end-to-end 타입 안전성 확보 |
| 미들웨어 생태계 | Bearer Auth, CORS, Rate Limit, Zod Validator 등 공식/서드파티 미들웨어 충실 |
| 공식 통합 | Cloudflare Workers, Vercel AI SDK 공식 통합 문서 및 템플릿 제공 |
DI 없이 팀 컨벤션을 잡는 게 처음엔 막막할 수 있는데, 라우터를 도메인별로 파일로 쪼개는 것만으로도 꽤 해결됩니다. AI API 레이어처럼 역할이 명확한 서비스에서는 오히려 군더더기가 없어서 좋더라고요.
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| DI·데코레이터 없음 | NestJS 같은 구조화 기능 부재, 대규모 팀에서 컨벤션 직접 수립 필요 | 라우터를 도메인별 파일로 분리하고 팀 컨벤션 문서화 |
| WebSocket 제한 | Cloudflare Workers 환경에서 WebSocket 지원이 제한적 | SSE로 단방향 스트림 대체, 양방향이 필수라면 Durable Objects 검토 |
| 상태 관리 없음 | 서버리스/엣지 환경에서 요청 간 상태 유지 불가 | Cloudflare KV, D1, Upstash Redis 등 외부 스토어와 조합 |
| Rate Limit 직접 구현 | AI API 비용 통제 로직을 개발자가 직접 추가해야 함 | hono-rate-limiter 또는 workers-hono-rate-limit 미들웨어 활용 |
Durable Objects — Cloudflare Workers에서 제공하는 상태 있는 서버리스 컴퓨팅 단위입니다. 일반 Workers가 무상태(stateless)인 반면, Durable Objects는 요청 간 메모리와 스토리지를 유지할 수 있어 WebSocket 세션 관리에 사용됩니다.
실무에서 가장 흔한 실수
- Rate Limiting 빠뜨리기 — AI API는 토큰당 비용이 발생합니다. 인증과 Rate Limiting 없이 LLM 엔드포인트를 열어두면 청구서 폭탄을 맞을 수 있습니다.
hono-rate-limiter를 초기부터 붙여두는 것을 권장합니다. - 에러 응답 형식 불통일 — 검증 에러, LLM API 에러, 내부 에러가 제각각 다른 형식으로 나가면 클라이언트에서 처리하기 어려워집니다.
app.onError()훅으로 전역 에러 핸들러를 정의해두면 좋습니다. - 스트리밍 도중 예외 처리 놓치기 — 예시 1의 코드처럼,
streamSSE콜백 내부에서 LLM API 호출이 실패하면 이미 200 응답 헤더가 나간 상태라 상태 코드로 에러를 전달할 수 없습니다.event: 'error'같은 특수 이벤트를 스트림으로 보내고 클라이언트에서 이를 처리하는 패턴을 미리 정해두면 나중에 디버깅이 훨씬 수월합니다.
마치며
Hono는 AI API 레이어에 필요한 세 가지—스트리밍, 타입 안전성, 엣지 실행—를 하나의 프레임워크로 해결하며, 현시점에서 가장 실용적인 선택입니다. 콜드 스타트 없이, 코드 생성 도구 없이, 런타임 교체 없이 이 세 가지를 동시에 챙길 수 있는 다른 선택지를 찾기가 쉽지 않습니다.
지금 바로 시작해볼 수 있는 3단계:
- 프로젝트 초기화:
pnpm create hono@latest my-ai-api를 실행하면 대화형 CLI로 런타임을 선택할 수 있습니다. Cloudflare Workers를 고르면wrangler가 함께 설정됩니다. - AI SDK 연결:
pnpm add ai @ai-sdk/openai로 Vercel AI SDK를 추가한 뒤, 예시 1의 스트리밍 엔드포인트 코드를src/index.ts에 붙여넣고pnpm dev를 실행해보세요..env에OPENAI_API_KEY만 있으면 바로 동작합니다. - 엣지 배포 확인:
pnpm run deploy(Cloudflare Workers 기준) 한 번으로 글로벌 엣지에 배포됩니다.wrk나hey같은 부하 테스트 도구로 로컬 Node.js 서버와 응답 시간을 직접 비교해보시면 차이를 체감할 수 있습니다.
참고 자료
처음 시작이라면 이 세 가지부터 보시면 됩니다:
- Hono 공식 문서 | hono.dev — 처음 시작할 때 가장 먼저 읽어볼 곳
- Hono RPC 가이드 | hono.dev — 서비스 간 타입 안전 통신을 더 깊이 알고 싶다면
- Cloudflare Workers AI — Vercel AI SDK 통합 | cloudflare.com — Workers AI 공식 통합 설정
추가 참고: