# Next.js + Hono: FE 개발자가 우아하게 풀스택 개발하기

## 개요

저는 프론트엔드 개발자입니다. 최근 회사에서 Next.js를 활용해 풀스택으로 개발을 하고 있습니다. 진행하는 프로젝트는 사내 내부 웹 서비스를 위한 REST API를 제공해야 했습니다.

Next.js 단독으로 API 개발을 진행하다보니 여러가지 어려움을 겪게 되었고, 해결하기위해 조사해보다가 Hono를 알게되었습니다.

이 글을 작성하면서 Next.js와 Hono를 잘 사용해 우아하게 풀스택을 개발할 수 있는 방법을 알아보려고 합니다.

## Hono

우선 Hono가 무엇인지 설명드리겠습니다. 공식 문서에 따르면 Hono는 “경량 웹 프레임워크로서 웹 표준에 기반해 설계되었으며, 다양한 JavaScript 런타임에서 동작”할 수 있습니다.

> Hono is a small, simple, and ultrafast web framework built on Web Standards. It works on any JavaScript runtime: Cloudflare Workers, Fastly Compute, Deno, Bun, Vercel, Netlify, AWS Lambda, Lambda@Edge, and Node.js.

Hono가 다양한 런타임에서 동작할 수 있는 이유는 기존 백엔드 프레임워크인 Express, Fastify, Nest.js 등 과 달리 서버리스 환경에 최적화 되어있기 때문입니다.

다양한 런타임 어댑터가 존재하고, 이 어댑터는 HTTP 요청을 처리하고 Hono에게 요청을 fetch 합니다.

### 어댑터 코드 살펴보기

```tsx
// AWS Lambda
// <https://hono.dev/docs/getting-started/aws-lambda>
const handle = <E extends Env = Env, S extends Schema = {}, BasePath extends string = '/'>(
  app: Hono<E, S, BasePath>
): ((event: LambdaEvent, lambdaContext?: LambdaContext) => Promise<APIGatewayProxyResult>) => {
  return async (event, lambdaContext?) => {
    // ...
    const res = await app.fetch(req, {
      event,
      requestContext,
      lambdaContext,
    })
    return processor.createResult(event, res)
  }
}

const app = new Hono()
export const handler = handle(app)

// ---

// Vercel
// <https://hono.dev/docs/getting-started/vercel>
const handle =
  (app: Hono<any, any, any>) =>
  (req: Request, requestContext: FetchEventLike): Response | Promise<Response> => {
    return app.fetch(req, {}, requestContext as any)
  }

const app = new Hono()
export const GET = handle(app)

// ---

// Node.js
// <https://hono.dev/docs/getting-started/nodejs>
const serve = (options: Options): ServerType => {
  const server = createAdaptorServer(options)
  // ...
}

const createAdaptorServer = (options: Options): ServerType => {
  const fetchCallback = options.fetch
  const requestListener = getRequestListener(fetchCallback, {})
  // ...
}

const getRequestListener = (fetchCallback: FetchCallback) => {
	// ...
  return async (
    incoming: IncomingMessage | Http2ServerRequest,
    outgoing: ServerResponse | Http2ServerResponse
  ) => {
     // ...
      res = fetchCallback(req, { incoming, outgoing } as HttpBindings) as
        | Response
        | Promise<Response>
		// ...
}

const app = new Hono()
serve(app)
```

위 코드들은 차례대로 FE개발자들에게 익숙한 환경인 AWS Lambda, Vercel, Node.js 환경의 어댑터 코드입니다. 코드를 잘 살펴보면 요청을 가공해 `app.fetch`로 넘겨주는것을 확인 할 수 있습니다.

Node.js 어댑터의 경우 지속적으로 유지되는 서버까지 생성하는 코드가 포함되어 있습니다.

## Next.js 단독으로 API 개발하면서 마주친 어려움

### 1\. 폴더 구조 기반 라우팅

폴더 구조 기반 라우팅의 장점은 직관적으로 이해하기 쉽다는 점입니다. 프론트엔드의 라우팅은 백엔드의 라우팅에 비교해서 비교적 단순하게 구성되기 때문에 이러한 장점이 더 돋보입니다.

하지만 백엔드에서 REST API의 라우팅은 API 설계가 복잡해질수록, 폴더 구조도 그만큼 복잡해집니다. 예를 들어, 게시글의 댓글 관련 API를 구현하면 아래와 같이 폴더 구조를 가지게 됩니다.

* `/api/posts/[postId]/comments/route.ts`
    
* `/api/posts/[postId]/comments/[commentId]/route.ts`
    

더욱 복잡한 리소스를 다루게 된다면 폴더 구조는 더욱 깊어지고, 코드의 가독성과 생산성을 저하시킬 수 있습니다.

Hono를 사용한다면 Next.js의 [Catch-all Segments](https://nextjs.org/docs/pages/building-your-application/routing/dynamic-routes#catch-all-segments)를 이용해 API를 구성할 수 있습니다.

```tsx
// @/app/api/[...routes]/route.ts
import { Hono } from 'hono'
import { handle } from 'hono/vercel'

const app = new Hono().basePath('/api')

app.get('/api/posts/:postId/comments', async (ctx) => {})
app.patch('/api/posts/:postId/comments/:commentId', async (ctx) => {})

export const GET = handle(app)
export const PATCH = handle(app)
```

### 2\. Swagger 문서 작성

Next.js에서 Swgger 문서를 작성하기 위해서는 next-swagger-doc 이라는 라이브러리를 사용해 작성할 수 있습니다. 이 라이브러리는 JSDoc을 활용해 문서를 생성하는 방식이기 때문에 주석으로 문서를 작성하면서 휴먼 에러가 발생할 확률이 매우 높고, IDE의 자동완성 기능에 도움을 받기 어렵습니다.

Hono를 사용한다면 hono의 middleware를 사용해 코드로 문서를 관리할 수 있습니다. 코드로 작성한 query, params, body 등 요청을 자동으로 검증하고 안전한 타입으로 개발을 진행할 수 있습니다.

```tsx
// @/app/api/[...routes]/route.ts
import { swaggerUI } from '@hono/swagger-ui'
import { OpenAPIHono, createRoute, z } from '@hono/zod-openapi'
import { handle } from 'hono/vercel'

const app = new OpenAPIHono().basePath('/api')

const getPostCommentsRoute = createRoute({
	method: 'get',
	path: '/posts/{postId}/comments',
	request: {
		params: z.object({ postId: z.string().min(2) })
	},
	responses: {
    200: {
      content: {
        'application/json': {
          schema: z.object({
	          postId: z.string(),
	          comments: z.string().array()
          }),
        },
      },
    },
  },
})

app.openapi(getPostRoute,	async (ctx) => {
	const { postId } = c.req.valid('param') // postId: string
	return ctx.json({ postId, comments: [] })
})

app.doc('/open-api.json', {
  openapi: '3.0.0',
  info: {
    version: '1.0.0',
    title: 'My API',
  },
})

app.get('/swagger', swaggerUI({ url: '/api/open-api.json' }))

export const GET = handle(app)
```

### 3\. 브라우저 환경을 위한 API 호출 함수 개발

API를 개발하고 사용하기 위해서는 클라이언트에서 fetch 혹은 axios를 활용해 API를 호출하는 함수를 개발해야 합니다. 이는 API 개발과 API 호출 함수의 코드 중복이 발생할 수 있습니다. 또 API 코드가 변경되면 클라이언트에서 확인하기 어렵습니다.

Hono를 사용하면 RPC 기능을 통해 서버와 클라이언트 사이의 API 스펙을 손쉽게 공유할 수 있습니다.

```tsx
// @/app/api/[...routes]/route.ts
import { swaggerUI } from '@hono/swagger-ui'
import { OpenAPIHono, createRoute, z } from '@hono/zod-openapi'
import { handle } from 'hono/vercel'

const app = new OpenAPIHono()
	.basePath('/api')
	.doc('/open-api.json', {
	  openapi: '3.0.0',
	  info: {
	    version: '1.0.0',
	    title: 'My API',
	  },
	})
	.get('/swagger', swaggerUI({ url: '/api/open-api.json' }))
	.openapi(getPostCommentsRoute,	async (ctx) => {
		const { postId } = c.req.valid('param') // postId: string
		return ctx.json({ id: postId, content: 'post', comments: [] })
	})

export type App = typeof app
export const GET = handle(app)

// client side
import { hc } from 'hono/client'
import type { App } from '@/app/api/[...routes]/route'

const client = hc<App>('<http://localhost:3000>')

const response = await client.posts[':postId'].comments.$get({
  param: { postId: '123' },
})
```

## 마무리

* [https://github.com/wooseongDev/nextjs-hono-example](https://github.com/wooseongDev/nextjs-hono-example)
    
* [https://nextjs-hono-example.vercel.app/](https://nextjs-hono-example.vercel.app/)
    

간단하게 Todo 예제 앱을 만들어 봤습니다. 실제로 사용해보니 장점도 있었고, 크진 않지만 단점도 있었습니다.

단점으로 RPC의 타입추론을 잘 사용하기 위해서는 핸들러들을 체이닝 하는 부분이 필요하다는 점입니다. RPC의 Known issue인 타입추론에 의해 [IDE 퍼포먼스 저하 이슈](https://hono.dev/docs/guides/rpc#ide-performance)가 있습니다.

장점으로는 앞서 설명한 어려움들이 크게 해소 된다는 점이고, Next.js 단독으로 사용할때 보다 확실히 Hono와 같이 사용할 때 생산성이 올라간다는 점 입니다.
