Hono 완벽 가이드 — Cloudflare Workers·Deno·Bun·Node에서 돌아가는 초경량 웹 프레임워크
이 글의 핵심
Hono는 Cloudflare Workers·Deno·Bun·Node·AWS Lambda에서 동일한 코드로 돌아가는 초경량 TypeScript 웹 프레임워크입니다. 14KB 번들로 Express·Fastify보다 작고 빠르며, 타입 안전 RPC 클라이언트·zod/valibot 검증·OpenAPI 생성·JWT/CORS 미들웨어를 기본 제공합니다. 이 글은 설치·라우팅·검증·RPC·엣지 배포·프로덕션 패턴을 체계적으로 정리합니다.
이 글의 핵심
Hono(日本어로 “불꽃”)는 2022년 시작된 초경량 웹 프레임워크입니다. 핵심 특징:
- Universal: 하나의
app.ts가 Cloudflare Workers·Deno·Bun·Node·AWS Lambda·Vercel Edge·Netlify Edge·Fastly Compute에서 모두 동작 - 14KB: Express(260KB)·Koa(90KB) 대비 초경량 → cold start ~1ms
- TypeScript First: 타입 추론으로 경로 파라미터·쿼리·헤더가 컴파일 타임에 보임
- RPC: 서버 스키마를 클라이언트가
hc()로 import 해 tRPC 스타일 호출 - 표준 Web API:
Request/Response/Headers기반 → 런타임 독립성
2026년 현재 Cloudflare가 공식 추천하는 Workers 프레임워크이자 Deno·Bun의 사실상 표준 선택지입니다.
설치
Cloudflare Workers
pnpm create hono my-api
cd my-api
pnpm install
pnpm dev # wrangler dev
pnpm deploy # wrangler deploy
Bun
bun create hono my-api
cd my-api
bun install
bun run dev
Node.js
npm install hono @hono/node-server
// src/index.ts
import { serve } from "@hono/node-server"
import { Hono } from "hono"
const app = new Hono()
app.get("/", (c) => c.text("Hello Hono"))
serve({ fetch: app.fetch, port: 3000 }, (info) => {
console.log(`http://localhost:${info.port}`)
})
라우팅
import { Hono } from "hono"
const app = new Hono()
app.get("/", (c) => c.text("Home"))
app.get("/users/:id", (c) => {
const id = c.req.param("id") // 타입: string
return c.json({ id })
})
app.post("/users", async (c) => {
const body = await c.req.json<{ name: string }>()
return c.json({ created: body.name }, 201)
})
app.get("/search", (c) => {
const q = c.req.query("q") // string | undefined
const page = Number(c.req.query("page") ?? 1)
return c.json({ q, page })
})
// 그룹
const api = new Hono().basePath("/api/v1")
api.get("/health", (c) => c.text("ok"))
app.route("/", api)
export default app
미들웨어
import { cors } from "hono/cors"
import { logger } from "hono/logger"
import { secureHeaders } from "hono/secure-headers"
import { jwt } from "hono/jwt"
app.use("*", logger())
app.use("*", secureHeaders())
app.use("/api/*", cors({
origin: ["https://pkglog.com"],
credentials: true,
}))
// 인증이 필요한 서브트리
app.use("/api/private/*", jwt({
secret: process.env.JWT_SECRET!,
}))
app.get("/api/private/me", (c) => {
const payload = c.get("jwtPayload")
return c.json({ user: payload })
})
Koa 스타일의 next() 기반이라 “요청 전/후”를 자연스럽게 감쌀 수 있습니다.
검증: @hono/zod-validator / valibot
pnpm add zod @hono/zod-validator
import { z } from "zod"
import { zValidator } from "@hono/zod-validator"
const createUser = z.object({
email: z.string().email(),
age: z.number().int().positive(),
})
app.post("/users", zValidator("json", createUser), (c) => {
const { email, age } = c.req.valid("json") // 타입 완벽 추론
return c.json({ email, age }, 201)
})
zValidator는 400 응답을 자동 생성하고 c.req.valid("json")에서 완전히 타입이 좁혀진 객체를 반환합니다.
RPC: 서버 타입을 클라이언트로
// server/app.ts
const app = new Hono()
.get("/posts/:id", (c) => c.json({ id: c.req.param("id"), title: "Hello" }))
.post("/posts",
zValidator("json", z.object({ title: z.string() })),
(c) => c.json(c.req.valid("json"), 201))
export type AppType = typeof app
export default app
// client/api.ts (프런트)
import { hc } from "hono/client"
import type { AppType } from "../server/app"
export const api = hc<AppType>("https://api.pkglog.com")
// 사용
const res = await api.posts[":id"].$get({ param: { id: "abc" } })
const data = await res.json() // 서버의 반환 타입 완벽 추론
const created = await api.posts.$post({ json: { title: "New" } })
tRPC처럼 end-to-end 타입 공유가 됩니다. RPC 헬퍼는 fetch에 얇게 래핑돼 번들 증가가 거의 없습니다.
OpenAPI 생성: @hono/zod-openapi
import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi"
import { swaggerUI } from "@hono/swagger-ui"
const route = createRoute({
method: "get",
path: "/users/{id}",
request: {
params: z.object({ id: z.string() }),
},
responses: {
200: {
content: {
"application/json": {
schema: z.object({ id: z.string(), name: z.string() }),
},
},
description: "User",
},
},
})
const app = new OpenAPIHono()
app.openapi(route, (c) => {
const { id } = c.req.valid("param")
return c.json({ id, name: "Alice" })
})
app.doc("/doc", { openapi: "3.1.0", info: { title: "API", version: "1.0" } })
app.get("/swagger", swaggerUI({ url: "/doc" }))
export default app
API 스펙·Swagger UI·타입이 자동 동기화됩니다.
스트리밍 / SSE
import { streamSSE } from "hono/streaming"
app.get("/events", (c) =>
streamSSE(c, async (stream) => {
for (let i = 0; i < 10; i++) {
await stream.writeSSE({ data: `tick ${i}`, event: "tick", id: String(i) })
await stream.sleep(1000)
}
}),
)
// 일반 스트리밍 (LLM 응답 등)
import { stream } from "hono/streaming"
app.post("/chat", (c) =>
stream(c, async (s) => {
for await (const chunk of openaiStream(prompt)) {
await s.write(chunk.choices[0].delta.content ?? "")
}
}),
)
표준 Web Streams 기반이라 런타임 간 호환성이 완벽합니다.
Cloudflare Workers 환경 바인딩
type Bindings = {
DB: D1Database
KV: KVNamespace
BUCKET: R2Bucket
AI: Ai
OPENAI_KEY: string
}
const app = new Hono<{ Bindings: Bindings }>()
app.get("/posts", async (c) => {
const { results } = await c.env.DB.prepare("SELECT * FROM posts ORDER BY id DESC LIMIT 20").all()
return c.json(results)
})
app.post("/cache/:key", async (c) => {
const key = c.req.param("key")
const body = await c.req.text()
await c.env.KV.put(key, body, { expirationTtl: 3600 })
return c.text("cached")
})
app.post("/ai/embed", async (c) => {
const { text } = await c.req.json<{ text: string }>()
const res = await c.env.AI.run("@cf/baai/bge-base-en-v1.5", { text })
return c.json(res)
})
export default app
D1(SQLite), KV, R2(S3 호환), Workers AI까지 한 파일에서 자연스럽게.
인증 전체 플로우 예시
import { Hono } from "hono"
import { sign, verify } from "hono/jwt"
import { setCookie, getCookie, deleteCookie } from "hono/cookie"
import { z } from "zod"
import { zValidator } from "@hono/zod-validator"
const app = new Hono<{ Bindings: { JWT_SECRET: string } }>()
app.post("/auth/login",
zValidator("json", z.object({ email: z.string().email(), password: z.string() })),
async (c) => {
const { email, password } = c.req.valid("json")
// DB 조회·bcrypt 검증 생략
const token = await sign({ sub: email, exp: Math.floor(Date.now() / 1000) + 3600 },
c.env.JWT_SECRET)
setCookie(c, "token", token, {
httpOnly: true, secure: true, sameSite: "Lax", path: "/", maxAge: 3600,
})
return c.json({ ok: true })
})
app.use("/api/*", async (c, next) => {
const token = getCookie(c, "token")
if (!token) return c.json({ error: "unauthorized" }, 401)
try {
const payload = await verify(token, c.env.JWT_SECRET)
c.set("user", payload)
await next()
} catch {
return c.json({ error: "invalid token" }, 401)
}
})
app.get("/api/me", (c) => c.json({ user: c.get("user") }))
app.post("/auth/logout", (c) => {
deleteCookie(c, "token")
return c.json({ ok: true })
})
테스트
import { describe, it, expect } from "vitest"
import app from "./index"
describe("API", () => {
it("returns user", async () => {
const res = await app.request("/users/42")
expect(res.status).toBe(200)
const data = await res.json()
expect(data).toEqual({ id: "42" })
})
})
app.request()는 서버를 실제로 기동하지 않고 Fetch API 요청을 시뮬레이션합니다. 초고속 테스트.
Workers 바인딩이 있는 경우 @cloudflare/vitest-pool-workers로 Miniflare 기반 환경에서 테스트.
Lambda / Vercel / Netlify
// AWS Lambda (Lambda@Edge 포함)
import { handle } from "hono/aws-lambda"
import app from "./app"
export const handler = handle(app)
// Vercel (app router)
// app/api/[[...route]]/route.ts
import { handle } from "hono/vercel"
import app from "@/server"
export const GET = handle(app)
export const POST = handle(app)
// Netlify Edge Functions
// netlify/edge-functions/api.ts
import { handle } from "hono/netlify"
import app from "./app"
export default handle(app)
“서버 코드는 한 번 쓰고 인프라에 맞게 어댑터만” — 진정한 런타임 이식성.
라우터 선택
smart-router(기본): 자동 최적화reg-exp-router: 매우 빠름, 정적 분석 가능할 때trie-router: 와일드카드·동적 파라미터 많을 때linear-router: 경로가 매우 적을 때
대부분 기본값 사용으로 충분합니다.
베스트 프랙티스
- 최상위에 route 모듈 체이닝: 타입 추론이 풀리지 않도록
.get().post().put()체이닝 유지 - 핸들러는 얇게, 비즈니스 로직은 서비스 모듈에: 테스트·재사용성
- 환경변수는 Bindings 타입으로: 런타임 환경 타입 안전
- 에러 핸들러 중앙화:
app.onError((err, c) => { console.error(err) if (err instanceof HTTPException) return err.getResponse() return c.json({ error: "internal" }, 500) }) - 로그 + 추적:
hono/timing, Sentry 미들웨어, OpenTelemetry 연동
성능 팁
- Cold start 최소화: 외부 라이브러리를 top-level에서 import하지 말고 필요 시 lazy import
- 캐시 미들웨어: Cloudflare Cache API를 감싼
hono/cache활용 - Compression:
hono/compress(Node/Bun), Workers는 Cloudflare가 자동 처리 - Connection Pooling: D1/Neon 같은 HTTP 기반 DB가 Workers와 궁합 좋음
트러블슈팅
RPC 타입이 any로 풀림
createRoute체이닝이 끊기면 타입 정보가 유실.const app = new Hono().get(...).post(...).get(...)형태 유지- 라우트 분리 시
.route("/posts", postsApp)패턴으로 재조합
Workers에서 Node 모듈 에러
node:buffer·node:crypto등은compatibility_flags = ["nodejs_compat"]필요 (wrangler.toml)- 순수 Web API 대체가 가능하면 그것이 더 가벼움
CORS가 preflight에서 실패
cors()미들웨어를 라우트보다 먼저 등록- 복잡한 헤더는
allowHeaders명시
체크리스트
- 타깃 런타임(Workers/Deno/Bun/Node/Lambda) 결정
- zod/valibot 검증 일관 적용
- RPC로 클라이언트와 end-to-end 타입 공유
- JWT/쿠키/보안 헤더 미들웨어
- OpenAPI 자동 생성
- 에러/로깅 표준화
- Vitest 기반 테스트
- CI에서 런타임 여러 개 빌드 검증
마무리
Hono는 “어느 런타임에서나 돌아가는 작고 빠른 웹 프레임워크”의 현 시점 최고 답안입니다. Cloudflare Workers 같은 엣지 런타임이 주류로 올라오고, Bun·Deno가 생산성 경쟁에 가세한 2026년, 하나의 코드베이스로 모두를 커버할 수 있다는 가치는 점점 커지고 있습니다. BFF·API·서버리스 함수를 만들 계획이 있다면 Express/Fastify 대신 Hono로 시작해보세요 — 첫 배포까지의 속도가 놀라울 겁니다.
관련 글
- Cloudflare Workers 완벽 가이드
- Deno 완벽 가이드
- Bun 완벽 가이드
- 엣지 컴퓨팅 완벽 가이드