본문 바로가기

카테고리 없음

Hono + Cloudflare Workers로 초경량 REST API 구축: 라우팅·미들웨어·배포 실무 가이드

반응형

왜 Hono와 Workers로 시작해야 할까

거대한 프레임워크로 API를 꾸리면 설정과 인프라가 복잡해지고, 작은 변경에도 배포가 부담스럽습니다. 간단한 CRUD나 웹훅 처리처럼 가벼운 요구라면, 빠른 라우팅과 저비용 엣지 배포가 핵심입니다.

Hono는 미니멀 라우팅과 미들웨어 구성이 쉬워 학습·유지 비용이 낮습니다. Cloudflare Workers는 서버 관리 없이 전 세계 엣지에 배포되어 지연시간을 줄일 수 있습니다.

이 글은 “라우팅을 어떻게 나누고, 미들웨어를 어디에 두며, 어떤 순서로 배포하는가”에 초점을 둡니다. 처음 적용한다면 버전 전략, 에러 처리, 환경변수 관리 같은 실무 체크포인트도 함께 정리합니다.

Hono와 Workers를 이해하는 가장 짧은 길

작은 팀이 주문 상태 조회 API를 만든다고 가정해 보겠습니다. Hono는 요청 경로별로 함수를 붙이는 간단한 라우팅 모델이라, /orders/:id 같은 패턴을 바로 구성하고, 응답은 JSON 헬퍼로 처리하면 끝입니다.

Cloudflare Workers는 이 함수들을 엣지 위치에서 실행해 가까운 데이터센터에서 응답합니다. 정적 서버 없이 배포되므로 웹훅처럼 짧고 빈번한 요청에 대기 시간을 줄이기 좋습니다.

미들웨어는 인증, 로깅, CORS 같은 공통 처리를 라우트 전에 한 번만 통과시키는 필터입니다. 예를 들어 관리자 전용 /admin/* 경로에만 토큰 검증 미들웨어를 묶어 위험 구간을 좁힐 수 있습니다.

실무에서는 세 가지 축으로 개념을 정리하면 혼선이 줄어듭니다.
- 라우팅: 리소스 단위로 경로를 나누고 버전(/v1)으로 변화 범위를 관리
- 미들웨어: 전역(CORS, 로깅)과 구간별(인증, 속도 제한)로 층을 구분
- 배포·런타임: Workers 바인딩으로 비밀키·KV·D1 같은 외부 자원을 주입

Hono로 라우팅·미들웨어·배포 빠르게 정리

프로젝트는 npm create hono@latest my-api → Cloudflare 선택 → TypeScript 권장 순으로 시작합니다. 환경변수는 wrangler.toml에 선언하고, 민감값은 wrangler secret로 분리합니다.

라우팅은 리소스 단위(/v1/orders, /v1/users)로 그룹화하고 id는 /:id로 제한합니다. 버전 전환은 /v1·/v2를 일정 기간 병행해 안정적으로 진행합니다.

미들웨어는 전역(CORS, 로깅) → 구간(인증, 레이트 리밋) → 라우트 순서로 둡니다. 배포 전 wrangler dev로 200/401/404를 확인하고, 에러 응답은 JSON으로 통일하세요.

관리자 전용 경로에는 경로 그룹 미들웨어로 인증을 붙입니다. 공통 처리와 위험 구간을 분리하면 원인 파악과 롤백이 간결해집니다.

아래 코드는 구조를 보여주는 예시입니다. 전역 CORS, 관리자 베이스 경로, 일관된 JSON 응답 형태를 담았습니다.

import { Hono } from 'hono'
import { cors } from 'hono/cors'

const app = new Hono()

app.use('*', cors()) // 전역 CORS
const admin = app.basePath('/v1/admin')

// Bearer 토큰 검증(구조용)
admin.use('*', async (c, next) => {
  const token = c.req.header('authorization')?.replace('Bearer ', '')
  if (!token) return c.json({ error: 'unauthorized' }, 401)
  await next()
})

app.get('/v1/orders/:id', (c) => c.json({ id: c.req.param('id') }))
admin.get('/users', (c) => c.json([{ id: 1 }]))

export default app

배포 체크리스트
- wrangler publish로 배포
- wrangler.toml: name, routes/worker-namespace, vars
- 시크릿: wrangler secret put KEY
- 롤백: 이전 태그, 큰 변경은 /v1·/v2 병행 기간 확보

흔한 실수와 선택 기준 한 번에 정리

버전 없이 경로를 늘리면 변경 영향 범위를 통제하기 어렵습니다. /v1로 고정하고 파라미터 스키마가 바뀌면 /v2를 따로 열어 병행 운영하는 편이 안전합니다.

미들웨어는 전역에 과하게 걸면 성능과 디버깅 모두 불리합니다. 인증·레이트리밋은 경로 그룹에만 붙이고, 전역에는 CORS·로깅처럼 반드시 필요한 것만 두세요.

에러 응답이 텍스트/JSON이 섞이면 클라이언트 처리가 흔들립니다. 상태코드와 메시지 포맷을 { error, code, traceId } 같은 JSON으로 고정하고, Hono onError 훅에서 공통 처리로 합치는 것이 좋습니다.

Workers 선택 시 KV, D1, Durable Objects 중 일관성·지연·쓰기 패턴을 먼저 봅니다. 조회 위주 캐시형이면 KV, 트랜잭션·조인이 필요하면 D1, 동시성 제어가 핵심이면 Durable Objects를 고려하세요.

비교 관점은 “규모·지연·상태 관리” 세 가지로 단순화합니다. 요청량이 작고 지연 민감하면 Hono+Workers, 지역 상태 공유가 필수이거나 복잡한 배치 작업이 많으면 컨테이너/서버 프레임워크가 유리합니다.

체크리스트:
- 라우트 설계: 리소스 단위 + 고정 버전(/v1) + 명확한 파라미터
- 미들웨어: 전역 최소화, 위험 구간 별도 그룹
- 에러/로그: JSON 포맷 통일, traceId 포함
- 배포: wrangler dev로 지역/프리뷰 테스트 → 최소 권한 바인딩만 노출
- 데이터: KV/D1/DO 중 일관성 요구사항으로 선택, TTL·재시도 정책 명시

지금 바로 적용할 실행 요약

이 글의 핵심은 가볍게 시작하고 명확하게 분리하는 것입니다. 리소스·버전·미들웨어 층을 나눠두면 라우팅과 배포가 단순해집니다.

바로 시도할 순서는 다음과 같습니다.
- 프로젝트 생성: npm create hono@latest → Cloudflare/TypeScript 선택
- 환경 구성: wrangler.toml에 바인딩, 민감값은 wrangler secret
- 라우팅 규칙: /v1/{resource}/:id 패턴 고정, 변경 시 /v2 병행
- 미들웨어 배치: 전역(CORS/로깅) → 그룹(인증/레이트리밋) → 라우트
- 에러 정책: onError에서 { error, code, traceId } JSON 통일
- 로컬 점검: wrangler dev로 200/401/404 확인 후 wrangler deploy

간단한 상태 조회나 웹훅부터 올려 보세요. 작은 범위를 안정화한 뒤, KV/D1/Durable Objects 선택과 캐시 전략을 단계적으로 추가하는 편이 안전합니다.

onError 훅으로 에러 응답 표준화

API 에러 형식이 제각각이면 클라이언트 분기와 디버깅 비용이 커집니다. onError 훅에서 상태코드·메시지를 매핑해 고정 포맷(JSON)으로 통일하면 로깅과 분석이 간결해집니다.

아래 예시는 Hono 전역 에러를 { error, code, traceId }로 변환하고, 알려진 예외를 의미 있는 상태코드로 매핑합니다.

import { Hono } from 'hono'

type Bindings = { ENV: string }
const app = new Hono<{ Bindings: Bindings }>()

class NotFoundError extends Error {}
class AuthError extends Error {}

app.onError((err, c) => {
  const traceId = crypto.randomUUID()
  const map: Record<string, number> = {
    [NotFoundError.name]: 404,
    [AuthError.name]: 401,
  }
  const status = map[err.name] ?? 500
  return c.json(
    { error: err.message || 'Unexpected error', code: status, traceId },
    status
  )
})

app.get('/v1/orders/:id', (c) => {
  const id = c.req.param('id')
  if (!id) throw new NotFoundError('order not found')
  return c.json({ id })
})

export default app

핵심은 두 가지입니다. 가벼운 예외 클래스로 의미→코드 매핑을 분리하고, traceId로 로그와 클라이언트 리포트를 연결합니다. 운영에서는 스택 노출을 제한하고 상세 원인은 서버 로그에서만 확인하도록 구성하는 편이 안전합니다.

반응형