Hono 완벽 가이드 — Cloudflare Workers·Deno·Bun·Node에서 돌아가는 초경량 웹 프레임워크

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: 경로가 매우 적을 때

대부분 기본값 사용으로 충분합니다.

베스트 프랙티스

  1. 최상위에 route 모듈 체이닝: 타입 추론이 풀리지 않도록 .get().post().put() 체이닝 유지
  2. 핸들러는 얇게, 비즈니스 로직은 서비스 모듈에: 테스트·재사용성
  3. 환경변수는 Bindings 타입으로: 런타임 환경 타입 안전
  4. 에러 핸들러 중앙화:
    app.onError((err, c) => {
      console.error(err)
      if (err instanceof HTTPException) return err.getResponse()
      return c.json({ error: "internal" }, 500)
    })
  5. 로그 + 추적: 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 완벽 가이드
  • 엣지 컴퓨팅 완벽 가이드