Javascript

Hono 웹 프레임워크 소개

드리프트2 2024. 3. 6. 20:30

 

안녕하세요?

 

Cloudflare에서 밀고 있는 Hono에 대해 간단히 알아 보겠습니다.

 

공식 웹 사이트

 

Hono - Ultrafast web framework for the Edges

Hono is a small, simple, and ultrafast web framework for the Edges. It works on Cloudflare Workers, Fastly Compute, Deno, Bun, Vercel, Netlify, AWS Lambda, Lambda@Edge, and Node.js. Fast, but not only fast.

hono.dev

 

** 목 차 **


create-hono 명령어

먼저, Hono 프로젝트를 만들어보겠습니다.

 

이를 위해 create-hono라는 CLI 도구를 사용합니다.

 

몇 가지 템플릿이 있지만, 이번에는 다음 3가지를 사용하겠습니다.

  • cloudflare-workers - Cloudflare Workers
  • cloudflare-pages - Cloudflare Pages
  • x-basic - HonoX

자, 시작해보겠습니다.

Cloudflare Workers

먼저 Cloudflare Workers부터 시작하겠습니다.

3분 내에 배포하기

Hono 프로젝트를 만드는 것은 간단합니다.

 

또한 Cloudflare Workers로 배포하는 것도 순식간에 가능합니다.

 

먼저 create-hono를 실행해보겠습니다.

 

참고로 이번에는 Bun의 bun 명령어를 패키지 매니저로 사용합니다.

 

다른 패키지 매니저를 선호하시는 분들은 npm이나 yarn을 사용하셔도 됩니다.

 

자세한 내용은 기재하지 않겠습니다.

bun create hono my-app-okinawa
cd my-app-okinawa
bun run dev

 

이제 프로젝트가 초기화되었고 개발 서버가 실행되었습니다.

 

이제 배포해보겠습니다.

bun run deploy

 

성공했습니다!


응답 보내기, 요청 처리하기

이전에는 일종의 "Hello World"였으니, 이번에는 좀 더 다양한 방법으로 응답을 보내보겠습니다.

  1. JSON 응답: JSON 형식으로 응답을 보내보겠습니다.
  2. HTML 응답: HTML 페이지를 응답으로 보내보겠습니다.
  3. 리다이렉트: 다른 URL로 리다이렉트를 시도해보겠습니다.
  4. 원시 응답: 특정 텍스트를 그대로 응답으로 보내보겠습니다.
  5. 헤더 추가: 응답에 헤더를 추가해보겠습니다.

이제 요청을 처리하는 방법에 대해 알아보겠습니다.

 

c.req는 Web 표준 Request와는 다른 Hono 특유의 "HonoRequest" 객체입니다.

 

순수한 Request에 접근하려면 c.req.raw를 사용하시면 됩니다.

 

또한 Cloudflare의 경우 c.req.raw.cf를 통해 Cloudflare 특유의 속성에 접근할 수 있습니다.

 

HonoRequest를 다루는 방법은 다음과 같습니다:

  • 쿼리 매개변수 가져오기
  • 경로 매개변수 가져오기
  • 헤더 가져오기
  • 바디 가져오기 (JSON, 폼)

라우팅

라우팅에 대한 설명이 남아있었군요.

 

라우팅은 어떤 경로와 메서드가 요청되었을 때 어떤 핸들러를 실행할지 결정하는 것입니다.

 

Hono에서 기본적인 라우팅을 지원하며, 대부분의 경우 사용할 수 있습니다.

  • 기본 라우팅
  • 경로 매개변수
  • 정규식
  • 체인
  • 그룹, app.route()

미들웨어 사용하기

이제 Hono스러운 사용 방법을 알아보겠습니다.

 

미들웨어를 사용해보겠습니다.

 

미들웨어는 다음 3가지 유형이 있습니다.

  1. 내장 미들웨어
  2. 사용자 정의 미들웨어
  3. 타사 미들웨어

먼저 이해하기 쉬운 내장 미들웨어부터 시작하겠습니다.

Pretty JSON

이 미들웨어는 지루해 보일 수 있지만, 제가 좋아하는 미들웨어 중 하나입니다.

import { prettyJSON } from 'hono/pretty-json'

//...

app.use(prettyJSON())

 

이렇게 하면 URL에 ?pretty를 추가하면 응답이 자동으로 정렬됩니다.

 

조용한 미들웨어입니다.

 

그 외에도 내장된 미들웨어에는 다음과 같은 것들이 있습니다.

  • 기본 인증 (Basic Authentication)
  • 베어러 인증 (Bearer Authentication)
  • 캐시 (Cache)
  • 압축 (Compress)
  • CORS
  • CSRF 보호
  • ETag
  • JSX 렌더러
  • JWT
  • 타이밍
  • 로거
  • 보안 헤더

사용자 정의 미들웨어

미들웨어를 직접 만들 수 있습니다.

 

이게 미들웨어의 매력이죠.

 

예를 들어, 응답 시간을 측정하는 미들웨어는 다음과 같습니다.

 

그런데 Cloudflare의 경우 Date 처리에 약간의 특이점이 있으므로 정확한 동작을 보장할 수 없습니다만, 일단 보시죠.

app.use(async (c, next) => {
  const start = Date.now()
  await next()
  const end = Date.now()
  c.res.headers.set('X-Response-Time', `${end - start}`)
})

 

next()는 핸들러로 생각하시면 됩니다.

 

이전에 요청을 처리하고, next() 이후에 응답을 조작합니다.

 

더 쉬운 예로 응답 헤더를 추가하는 것이 있습니다.

 

응용 프로그램에서는 Cloudflare 고유의 API인 HTMLRewriter를 사용한 것이 있습니다.

 

이를 통해 링크를 일괄적으로 변경하거나 링크 대상의 이동 등에 유용합니다.

app.get('/pages/*', async (c, next) => {
  await next()

  class AttributeRewriter {
    constructor(attributeName) {
      this.attributeName = attributeName
    }
    element(element) {
      const attribute = element.getAttribute(this.attributeName)
      if (attribute) {
        element.setAttribute(this.attributeName, attribute.replace('oldhost', 'newhost'))
      }
    }
  }
  const rewriter = new HTMLRewriter().on('a', new AttributeRewriter('href'))

  const contentType = c.res.headers.get('Content-Type')

  if (contentType!.startsWith('text/html')) {
    c.res = rewriter.transform(c.res)
  }
})

미들웨어 실행 순서

미들웨어를 사용하는 것은 매우 간단하지만, 실행 순서에 주의해야 합니다.

 

위에서 작성한 미들웨어가 먼저 실행됩니다.

 

정확히 말하자면, 위에서 작성한 미들웨어의 next() 이전이 먼저 실행되고, 그 이후가 마지막에 실행됩니다.

 

아래 예시를 보면 이해가 빠를 것입니다.

app.use(async (_, next) => {
  console.log('middleware 1 start')
  await next()
  console.log('middleware 1 end')
})
app.use(async (_, next) => {
  console.log('middleware 2 start')
  await next()
  console.log('middleware 2 end')
})
app.use(async (_, next) => {
  console.log('middleware 3 start')
  await next()
  console.log('middleware 3 end')
})

app.get('/', (c) => {
  console.log('handler')
  return c.text('Hello!')
})

 

위 코드를 실행하면 다음과 같은 출력이 나옵니다.

middleware 1 start
  middleware 2 start
    middleware 3 start
      handler
    middleware 3 end
  middleware 2 end
middleware 1 end

타사 미들웨어

이것은 이름 그대로 외부 라이브러리에 의존하는 미들웨어입니다.

 

Hono는 외부 라이브러리에 의존하지 않는 것이 원칙이며, 외부 라이브러리를 사용하면 코어에 포함되지 않습니다.

 

그러나 이것은 좋은 가이드라인이 될 수 있습니다.

 

타사 미들웨어에는 다음과 같은 것들이 있습니다.

 

codehex님의 Firebase Auth도 있군요.

  • GraphQL Server
  • Sentry
  • Firebase Auth
  • Zod Validator
  • Qwik City
  • tRPC Server
  • TypeBox Validator
  • Typia Validator
  • Valibot Validator
  • Zod OpenAPI
  • Clerk Auth
  • Swagger UI
  • esbuild Transpiler
  • Prometheus Metrics
  • Auth.js(Next Auth)
  • Zod Validator를 나중에 사용해보겠습니다.

헬퍼 사용하기

미들웨어 외에도 헬퍼(helper)가 있습니다.

 

Hono의 코어는 매우 작고, hono/tiny와 같은 가장 작은 프리셋은 12KB 미만입니다.

 

이는 정말 작습니다.

 

Express는 560KB 정도인데요.

 

그러나 이것으로는 기본적인 기능만 수행할 수 있습니다.

 

그래서 미들웨어와 헬퍼를 사용하여 기능을 확장합니다.

 

헬퍼에는 다음과 같은 것들이 있습니다.

  • Accepts
  • Adapter
  • Cookie
  • css
  • Dev
  • Factory
  • html
  • JWT
  • SSG
  • Streaming
  • Testing

Dev 헬퍼

Dev 헬퍼 중에서도 showRoutes()를 많이 사용합니다.

 

등록된 라우트가 한눈에 보이죠? 이게 편리합니다.

Streaming 헬퍼

Streaming 헬퍼도 현재 유행하는 기술입니다.

 

SSE를 사용하고 있지만, 이런 것들이 많이 있죠.

 

AI 시대에는 시간이 오래 걸리는 처리를 조금씩 조금씩 반환하는 경우가 많아서 이 헬퍼를 사용할 수 있습니다.

Zod Validator 사용하기

Zod Validator라는 미들웨어가 제일 좋습니다.

 

이를 사용하면 Zod를 사용하여 유효성 검사를 수행하고 타입을 확실히 지정할 수 있습니다.

 

게다가 RPC 모드도 사용할 수 있습니다.

Cloudflare의 Bindings 사용하기

Cloudflare의 미들웨어, 즉 KV, R2, D1 등을 사용하는 것을 "Bindings"라고 합니다.

 

그리고 변수도 마찬가지입니다.

 

Hono에서는 Bindings에 접근하는 것이 매우 간단합니다.

 

c.env.FOO와 같이 사용하면 됩니다.

 

그러나 이렇게 하면 TypeScript의 타입이 지정되지 않으므로 Hono를 인스턴스화할 때 제네릭을 전달하는 것이 좋습니다.

 

다음과 같이 말이죠.

type Bindings = {
  TOKEN: string
  MY_KV: KVNamespace
}

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

 

이렇게 하면 타입이 지정됩니다.


테스트하기

Hono를 사용하면 테스트를 간단하게 수행할 수 있습니다.

 

다음과 같은 방식으로 테스트할 수 있습니다.

it('should return 200 response', async () => {
  const res = await app.request('/')
  expect(res.status).toBe(200)
})

 

이 방법이 좋습니다.

 

Web Standard API는 서버 레이어를 블랙 박스화하므로 Request/Response 수준에서 테스트를 작성하면 됩니다.

 

Hono의 테스트 중 상당 부분이 이와 같은 방식으로 작성되어 있습니다.

 

이 레이어의 테스트만으로도 대부분의 문제를 발견할 수 있습니다.

최적의 방법

Hono 애플리케이션을 개발할 때 몇 가지 최적의 방법이 있습니다.

 

app.route()

 

가장 중요한 부분입니다.

 

큰 애플리케이션을 개발할 때는 어쩔 수 없이 Ruby on Rails 스타일로 경로와 컨트롤러를 분리해야 합니다.

const booksList = (c: Context) => {
  return c.json('list books')
}

app.get('/books', booksList)

 

이 방법도 괜찮지만, 타입 추론이 어려워질 수 있으므로 app.route()를 확장하는 스타일을 권장합니다.

// books.ts
import { Hono } from 'hono'

const app = new Hono()

app.get('/', (c) => c.json('list books'))
app.post('/', (c) => c.json('create a book', 201))
app.get('/:id', (c) => c.json(`get ${c.req.param('id')}`))

export default app
// index.ts
import { Hono } from 'hono'
import authors from './authors'
import books from './books'

const app = new Hono()

app.route('/authors', authors)
app.route('/books', books)

export default app

 

그래도 어쩔 수 없는 경우가 있을 때는 팩토리 헬퍼를 사용하면 좋습니다.

import { createFactory } from 'hono/factory'
import { logger } from 'hono/logger'

// ...

// 😃
const factory = createFactory()

const middleware = factory.createMiddleware(async (c, next) => {
  c.set('foo', 'bar')
  await next()
})

const handlers = factory.createHandlers(logger(), middleware, (c) => {
  return c.json(c.var.foo)
})

app.get('/api', ...handlers)

Cloudflare Pages

create-hono에서 cloudflare-pages를 선택해 보겠습니다.

JSX 사용하기

JSX를 사용해 보겠습니다.

 

Hono는 JSX를 사용할 수 있는 큰 장점이 있습니다.

 

확장자를 .tsx로 변경하면 바로 사용할 수 있습니다.

 

이 스타터 템플릿은 이미 지원하고 있습니다.

JSX 렌더러 미들웨어

JSX 렌더러 미들웨어도 제가 좋아하는 기능 중 하나입니다.

 

이를 사용하면 JSX 페이지를 쉽게 작성할 수 있습니다.

 

이 스타터 템플릿에는 기본적으로 포함되어 있습니다.

hono/css 사용하기

실제로 Hono는 JSX 외에도 CSS in JS를 구현하고 있습니다.

 

styled-components와 비슷한 방식으로 직접 구현한 것입니다.

 

아래는 기본적인 사용 방법입니다.

 

클래스 이름을 전달하는 것이 흥미롭습니다.

app.get('/', (c) => {
  const headerClass = css`
    background-color: orange;
    color: white;
    padding: 1rem;
  `
  return c.html(
    <html>
      <head>
        <Style />
      </head>
      <body>
        <h1 class={headerClass}>Hello!</h1>
      </body>
    </html>
  )
})

Pages로 배포하기

Cloudflare Pages로 배포하는 것도 매우 간단합니다.

HonoX

이제 HonoX로 넘어갑니다.

기본 라우팅

기본 라우팅은 다음과 같습니다.

 

_error.tsx, _404.tsx 및 _renderer.tsx가 보존되어 있습니다.

라우트와 핸들러

각 라우트 파일에는 어떤 내용을 작성해야 할까요?

 

응답을 반환하는 방법은 다음과 같이 3가지 유형이 있습니다.

  1. createRoute()
  2. Hono 인스턴스 (클래식 스타일)
  3. 함수

이 부분은 이 문서가 자세히 설명하고 있습니다.

 

그렇습니다, 여기에도 적혀 있듯이 HonoX는 아직 알파 상태이므로 갑작스런 변경 사항이 발생할 수 있고, 아직 미숙한 부분이 있습니다.

MDX 사용하기

HonoX에서는 .mdx 확장자를 가진 MDX 파일도 지원됩니다.

 

MDX가 무엇인지 궁금하시다면 Markdown의 확장이라고 생각하시면 됩니다.

 

의존성 라이브러리를 설치하고 vite.config.ts 파일을 수정해 보겠습니다.

bun add -D @mdx-js/rollup remark-frontmatter remark-mdx-frontmatter

vite.config.ts

import devServer from '@hono/vite-dev-server'
import mdx from '@mdx-js/rollup'
import honox from 'honox/vite'
import remarkFrontmatter from 'remark-frontmatter'
import remarkMdxFrontmatter from 'remark-mdx-frontmatter'
import { defineConfig } from 'vite'

const entry = './app/server.ts'

export default defineConfig(() => {
  return {
    plugins: [
      honox(),
      devServer({ entry }),
      mdx({
        jsxImportSource: 'hono/jsx',
        remarkPlugins: [remarkFrontmatter, remarkMdxFrontmatter],
      }),
    ],
  }
})

 

이제 app/routes/foo.mdx와 같은 파일을 만들면 /foo에 접속했을 때 자동으로 렌더링됩니다.

 

이 기능은 정말 편리합니다.

 

또한 프론트매터도 지원되므로 제목이나 메타데이터를 쉽게 추가할 수 있습니다.

 

게시물 목록도 다음과 같이 작성할 수 있습니다.

 

Meta는 메타데이터를 작성하면 됩니다.

import type { Meta } from '../types'

export default function Top() {
  const posts = import.meta.glob<{ frontmatter: Meta }>('./posts/*.mdx', {
    eager: true,
  })
  return (
    <div>
      <h2>Posts</h2>
      <ul class='article-list'>
        {Object.entries(posts).map(([id, module]) => {
          if (module.frontmatter) {
            return (
              <li>
                <a href={`${id.replace(/\.mdx$/, '')}`}>{module.frontmatter.title}</a>
              </li>
            )
          }
        })}
      </ul>
    </div>
  )
}

SSG 헬퍼 사용하기

지금까지 SSR(서버 사이드 렌더링) 또는 프론트엔드 개발자들이 말하는 "하이드레이트" 방식으로 동적 렌더링을 수행했습니다.

 

그러나 v4부터 SSG(정적 사이트 생성) 헬퍼가 도입되었습니다.

 

이를 사용하면 Hono 애플리케이션을 정적 HTML로 렌더링할 수 있습니다.

 

예를 들어, 블로그를 만들고 Cloudflare Pages에 배포하려면 컨텐츠를 전체 번들로 묶어야 하는 문제가 있습니다.

 

이는 꽤 제한적입니다.

 

따라서 이 SSG 기능은 정말 편리합니다.

 

구현 방법은 빌드용 파일을 직접 만들고 실행해도 좋지만, Vite 플러그인을 사용하면 편리합니다.

블로그 만들기

이제 HonoX를 사용하여 블로그를 만들 수 있습니다.

 

hono/css와 함께 사용해도 좋습니다.

클라이언트 컴포넌트

HonoX는 클라이언트 컴포넌트를 Island에 배치할 수 있습니다.

 

React와 상당히 호환되므로 친숙한 느낌일 것입니다.

 

그러나 HonoX의 경우 목표가 명확합니다.

 

바로 Island을 지향하며 MPA 페이지의 일부분에 상호작용을 추가하는 것입니다.

 

따라서 너무 많이 사용하지는 않습니다.

 

그 대신, Island이 없는 페이지에서는 JavaScript가 제공되지 않습니다.

 

따라서 성능이 좋을 것입니다.

요약

이렇게 Hono에 대해 빠르게 설명해 보았습니다.

 

끝.