Skip to content

Hono 스택

Hono는 쉬운 작업도, 어려운 작업도 모두 쉽게 처리할 수 있게 해준다. 단순히 JSON을 반환하는 용도로만 적합한 것이 아니다. REST API 서버와 클라이언트를 포함한 풀스택 애플리케이션을 구축하는 데도 뛰어나다.

RPC

Hono의 RPC 기능은 코드를 거의 변경하지 않고도 API 스펙을 공유할 수 있게 한다. hc로 생성된 클라이언트는 스펙을 읽어 엔드포인트에 타입 안전성(type-safety)을 보장하며 접근한다.

이 기능을 가능하게 하는 라이브러리들은 다음과 같다:

이러한 컴포넌트들을 통틀어 Hono 스택이라고 부른다. 이제 이 스택을 활용해 API 서버와 클라이언트를 만들어 보자.

API 작성하기

먼저, GET 요청을 받아 JSON을 반환하는 엔드포인트를 작성한다.

ts
import { Hono } from 'hono'

const app = new Hono()

app.get('/hello', (c) => {
  return c.json({
    message: `Hello!`,
  })
})

Zod를 사용한 검증

쿼리 파라미터의 값을 받기 위해 Zod로 검증한다.

SC

ts
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'

app.get(
  '/hello',
  zValidator(
    'query',
    z.object({
      name: z.string(),
    })
  ),
  (c) => {
    const { name } = c.req.valid('query')
    return c.json({
      message: `Hello! ${name}`,
    })
  }
)

타입 공유하기

엔드포인트 명세를 내보내려면 해당 타입을 추출하면 된다.

ts
const route = app.get(
  '/hello',
  zValidator(
    'query',
    z.object({
      name: z.string(),
    })
  ),
  (c) => {
    const { name } = c.req.valid('query')
    return c.json({
      message: `Hello! ${name}`,
    })
  }
)

export type AppType = typeof route

클라이언트

다음으로 클라이언트 측 구현을 살펴본다. hcAppType 타입을 제네릭으로 전달해 클라이언트 객체를 생성한다. 그러면 자동 완성 기능이 작동하며, 엔드포인트 경로와 요청 타입이 제안된다.

SC

ts
import { AppType } from './server'
import { hc } from 'hono/client'

const client = hc<AppType>('/api')
const res = await client.hello.$get({
  query: {
    name: 'Hono',
  },
})

Response는 fetch API와 호환되지만, json()으로 가져올 수 있는 데이터는 타입이 지정된다.

SC

ts
const data = await res.json()
console.log(`${data.message}`)

API 사양을 공유하면 서버 측 변경 사항을 실시간으로 파악할 수 있다.

SS

React와 함께 사용하기

React를 활용해 Cloudflare Pages에서 애플리케이션을 만들 수 있다.

API 서버

ts
// functions/api/[[route]].ts
import { Hono } from 'hono'
import { handle } from 'hono/cloudflare-pages'
import { z } from 'zod'
import { zValidator } from '@hono/zod-validator'

const app = new Hono()

const schema = z.object({
  id: z.string(),
  title: z.string(),
})

type Todo = z.infer<typeof schema>

const todos: Todo[] = []

const route = app
  .post('/todo', zValidator('form', schema), (c) => {
    const todo = c.req.valid('form')
    todos.push(todo)
    return c.json({
      message: 'created!',
    })
  })
  .get((c) => {
    return c.json({
      todos,
    })
  })

export type AppType = typeof route

export const onRequest = handle(app, '/api')

React와 React Query를 사용한 클라이언트

tsx
// src/App.tsx
import {
  useQuery,
  useMutation,
  QueryClient,
  QueryClientProvider,
} from '@tanstack/react-query'
import { AppType } from '../functions/api/[[route]]'
import { hc, InferResponseType, InferRequestType } from 'hono/client'

const queryClient = new QueryClient()
const client = hc<AppType>('/api')

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Todos />
    </QueryClientProvider>
  )
}

const Todos = () => {
  const query = useQuery({
    queryKey: ['todos'],
    queryFn: async () => {
      const res = await client.todo.$get()
      return await res.json()
    },
  })

  const $post = client.todo.$post

  const mutation = useMutation<
    InferResponseType<typeof $post>,
    Error,
    InferRequestType<typeof $post>['form']
  >(
    async (todo) => {
      const res = await $post({
        form: todo,
      })
      return await res.json()
    },
    {
      onSuccess: async () => {
        queryClient.invalidateQueries({ queryKey: ['todos'] })
      },
      onError: (error) => {
        console.log(error)
      },
    }
  )

  return (
    <div>
      <button
        onClick={() => {
          mutation.mutate({
            id: Date.now().toString(),
            title: 'Write code',
          })
        }}
      >
        Add Todo
      </button>

      <ul>
        {query.data?.todos.map((todo) => (
          <li key={todo.id}>{todo.title}</li>
        ))}
      </ul>
    </div>
  )
}

Released under the MIT License.