Hono.js 경량 웹 프레임워크 입문 가이드 — Express 대안
Hono.js 설치, 라우팅/미들웨어, Cloudflare Workers·Vercel Edge·Deno Deploy 배포, Zod OpenAPI 통합, Express 마이그레이션.
한 줄 요약: Hono.js는 Cloudflare Workers·Deno·Bun·Node.js 등 어디서나 동작하는 초경량 엣지 웹 프레임워크로, Express보다 최대 20배 빠른 처리 속도와 4KB 미만의 번들 사이즈를 자랑한다.
- Express/Fastify를 쓰다가 엣지 런타임으로 이전을 고민하는 백엔드 개발자
- Cloudflare Workers 또는 Vercel Edge Functions에서 API를 구축하려는 개발자
- 번들 사이즈와 콜드스타트 지연을 줄이고 싶은 풀스택 개발자
- TypeScript 친화적인 경량 라우터를 찾고 있는 개발자
Hono.js란 무엇인가 — Express와 무엇이 다른가
Hono(炎, 일본어로 "불꽃")는 Yusuke Wada가 2021년 Cloudflare Workers를 위해 만든 웹 프레임워크다. 현재는 Cloudflare Workers, Deno, Bun, Vercel Edge, AWS Lambda, Node.js 등 7개 이상의 런타임을 하나의 API로 지원한다.
Express와의 핵심 차이는 세 가지다. 첫째, 런타임 독립성: Express는 Node.js에 종속되지만 Hono는 Web Standards API(Fetch API, Request/Response)를 기반으로 설계돼 어느 런타임에서나 동일하게 동작한다. 둘째, 번들 사이즈: hono 코어는 약 14KB(압축 시 ~5KB)로 Express(~2MB 의존성 포함)와 비교할 수 없을 만큼 작다. 셋째, 성능: Hono 공식 벤치마크 기준 초당 요청 처리량이 Express 대비 3~5배 높고, 엣지 런타임에서는 격차가 더 벌어진다.
단, Hono는 view 렌더링 엔진이나 ORM을 내장하지 않는다. API 서버·엣지 함수·BFF(Backend for Frontend) 레이어에 최적화된 프레임워크다.
설치 및 프로젝트 생성
Hono는 create-hono CLI로 타겟 런타임별 스캐폴드를 즉시 생성할 수 있다. 런타임을 먼저 정한 뒤 프로젝트를 만드는 것이 가장 빠르다.
create-hono CLI로 프로젝트 생성# npm npm create hono@latest my-app # pnpm pnpm create hono@latest my-app # bun bun create hono@latest my-app # 런타임 선택 프롬프트 예시 # ? Which template do you want to use? # cloudflare-workers # cloudflare-pages # vercel # deno # bun # nodejs # aws-lambda
Node.js 환경에서 기존 프로젝트에 추가 설치할 경우 아래와 같이 진행한다. @hono/node-server 어댑터가 Node.js HTTP 서버를 Web Standards 인터페이스로 감싸준다.
Node.js 환경 직접 설치npm install hono @hono/node-server # TypeScript 사용 시 npm install -D typescript @types/node ts-node npx tsc --init
Node.js 기본 서버 (src/index.ts)import { Hono } from 'hono' import { serve } from '@hono/node-server' const app = new Hono() app.get('/', (c) => c.text('Hello Hono!')) app.get('/json', (c) => c.json({ status: 'ok', runtime: 'node' })) serve({ fetch: app.fetch, port: 3000 }, (info) => { console.log(`Server running at http://localhost:${info.port}`) })
라우팅과 미들웨어 — 핵심 패턴 정리
Hono의 라우터는 RegExpRouter(기본)와 TrieRouter 두 가지를 제공한다. RegExpRouter는 모든 라우트를 하나의 정규식으로 컴파일해 O(1) 매칭을 구현한다. 라우팅 문법은 Express와 유사해 학습 비용이 낮다.
라우팅 패턴 — 경로 파라미터, 와일드카드, 그룹import { Hono } from 'hono' const app = new Hono() // 기본 HTTP 메서드 app.get('/posts', (c) => c.json({ posts: [] })) app.post('/posts', async (c) => { const body = await c.req.json() return c.json({ created: body }, 201) }) // 경로 파라미터 app.get('/posts/:id', (c) => { const id = c.req.param('id') return c.json({ id }) }) // 와일드카드 app.get('/files/*', (c) => c.text('file handler')) // 라우트 그룹 (Hono 인스턴스 중첩) const api = new Hono().basePath('/api') api.get('/users', (c) => c.json({ users: [] })) api.get('/users/:id', (c) => c.json({ id: c.req.param('id') })) app.route('/', api)
미들웨어는 app.use()로 등록하며, next()를 await해 체인을 이어간다. 공식 미들웨어 패키지는 hono/middleware에서 임포트할 수 있다.
미들웨어 — 로깅, 요청 시간, 커스텀import { Hono } from 'hono' import { logger } from 'hono/logger' import { timing } from 'hono/timing' import { cors } from 'hono/cors' const app = new Hono() // 글로벌 미들웨어 app.use('*', logger()) app.use('*', timing()) app.use('/api/*', cors({ origin: 'https://example.com' })) // 커스텀 미들웨어 app.use('*', async (c, next) => { const start = Date.now() await next() const elapsed = Date.now() - start c.res.headers.set('X-Response-Time', `${elapsed}ms`) }) app.get('/', (c) => c.text('OK'))
Cloudflare Workers · Vercel Edge · Deno Deploy 배포
Hono의 가장 큰 강점은 런타임별 어댑터 교체만으로 동일한 코드를 다른 엣지 플랫폼에 배포할 수 있다는 점이다. 아래 세 플랫폼의 배포 방법을 각각 정리한다.
Cloudflare Workers 배포 (wrangler)# 1. wrangler 설치 npm install -D wrangler # 2. wrangler.toml 예시 # name = "my-hono-app" # main = "src/index.ts" # compatibility_date = "2024-01-01" # 3. src/index.ts — CF Workers는 default export fetch 사용 import { Hono } from 'hono' const app = new Hono() app.get('/', (c) => c.text('Edge!')) export default app # 4. 로컬 개발 npx wrangler dev # 5. 배포 npx wrangler deploy
Vercel Edge Functions 배포# vercel.json { "functions": { "api/**/*.ts": { "runtime": "edge" } } } # api/index.ts import { Hono } from 'hono' import { handle } from 'hono/vercel' export const runtime = 'edge' const app = new Hono().basePath('/api') app.get('/hello', (c) => c.json({ message: 'Hello from Vercel Edge' })) export const GET = handle(app) export const POST = handle(app)
Deno Deploy 배포# Deno는 별도 어댑터 없이 app.fetch 직접 사용 import { Hono } from 'npm:hono' const app = new Hono() app.get('/', (c) => c.text('Hello from Deno Deploy!')) Deno.serve(app.fetch)
Zod OpenAPI 통합 및 JWT·CORS 미들웨어
Hono는 @hono/zod-openapi 패키지로 Zod 스키마에서 OpenAPI 3.1 문서를 자동 생성하고, 입력 유효성 검사를 라우트 단계에서 처리할 수 있다. JWT 인증과 CORS는 공식 미들웨어로 빠르게 추가할 수 있다.
Zod OpenAPI — 스키마 기반 라우트 정의npm install @hono/zod-openapi zod # src/app.ts import { OpenAPIHono, createRoute, z } from '@hono/zod-openapi' const app = new OpenAPIHono() const UserSchema = z.object({ id: z.string().openapi({ example: 'user-123' }), name: z.string().openapi({ example: 'Alice' }), }) const getUserRoute = createRoute({ method: 'get', path: '/users/{id}', request: { params: z.object({ id: z.string() }), }, responses: { 200: { content: { 'application/json': { schema: UserSchema } }, description: 'User found' }, 404: { description: 'User not found' }, }, }) app.openapi(getUserRoute, (c) => { const { id } = c.req.valid('param') return c.json({ id, name: 'Alice' }, 200) }) // /doc 에서 OpenAPI JSON 제공 app.doc('/doc', { openapi: '3.1.0', info: { title: 'My API', version: '1.0.0' } })
JWT 인증 미들웨어import { Hono } from 'hono' import { jwt, sign, verify } from 'hono/jwt' const app = new Hono() const SECRET = process.env.JWT_SECRET ?? 'change-this-in-production' // 토큰 발급 엔드포인트 app.post('/auth/login', async (c) => { const { username } = await c.req.json() const token = await sign({ sub: username, exp: Math.floor(Date.now() / 1000) + 3600 }, SECRET) return c.json({ token }) }) // 보호 라우트 app.use('/protected/*', jwt({ secret: SECRET })) app.get('/protected/profile', (c) => { const payload = c.get('jwtPayload') return c.json({ user: payload.sub }) })
Express에서 Hono로 마이그레이션하는 법
Express 코드베이스를 Hono로 이전할 때 가장 자주 마주치는 패턴 차이를 정리한다. 대부분은 req/res를 Hono의 Context(c)로 교체하는 작업이다.
단계별 마이그레이션 전략: 전체를 한 번에 교체하는 대신, Express 앞에 Hono를 BFF 레이어로 배치하고 라우트를 하나씩 이전하는 방식이 안전하다. Hono의 app.mount()를 이용하면 Express 앱을 Hono 라우터에 마운트해 단계적으로 교체할 수 있다.
✔
req.body → await c.req.json() (bodyParser 미들웨어 제거)✔ 에러 핸들러:
app.onError((err, c) => c.json({ error: err.message }, 500))✔ 404 핸들러:
app.notFound((c) => c.json({ error: 'Not Found' }, 404))✔ helmet →
hono/secure-headers로 대체✔ morgan →
hono/logger로 대체