Seize the day

POST : Backend study

Hono by BunJS study

Hono JS라는 걸 발견했는데 이게 꽤 쓸만해 보인다. 간결하고 있을 건 다 있다는 느낌이다.  https://hono.dev/ 여기서 관리하고 있고 https://thevalleyofcode.com/hono 여기에서 스펙을 좀 더 쉽게 확인할 수 있다.  middle ware, basic auth, compress, firebase auth 까지 지원을하고 logger,  ssl도 자체 지원한다. Hono는 Bun or Deno와 사용하면 최고의 성능을 내는 것 같다.  Nest js에 만족하고 있고, 극강의 퍼포먼스가 필요한 것은 아니라서 일단 관심만 두고 있다. Hono는 Node js도 지원하기 때문에 사이드 프로젝트가 있다면 흥미가 당긴다. 

https://medium.com/deno-the-complete-reference/performance-of-hono-framework-with-deno-170ea929c49e 
https://medium.com/deno-the-complete-reference/performance-of-hono-framework-with-bun-b6e592cc113a  
https://github.com/SaltyAom/bun-http-framework-benchmark 
https://hono.dev/docs/concepts/motivation 
https://github.com/honojs/hono  

Macmini m1에서 테스트 했고,  기본적인 가이드는 공식 사이트와 https://thevalleyofcode.com/hono 에서 확인 가능한데, 나는 내가 궁금한 것 위주로 테스트 했다. 

 

Hono 프로젝트 생성

bun , nodejs, deno, 등 여러 플랫폼에서 실행할 수 있는 기본 프로젝트를 생성해준다. 

dajkim76@Kims-Mac-mini ~/test % bun create hono hono_test
create-hono version 0.10.0
✔ Using target directory … hono_test
? Which template do you want to use? bun
✔ Cloning the template
? Do you want to install project dependencies? yes
? Which package manager do you want to use? bun
✔ Installing project dependencies
🎉 Copied project files
Get started with: cd hono_test
dajkim76@Kims-Mac-mini ~/test % cd hono_test
dajkim76@Kims-Mac-mini ~/test/hono_test % cat package.json
{
  "name": "hono_test",
  "scripts": {
    "dev": "bun run --hot src/index.ts"
  },
  "dependencies": {
    "hono": "^4.4.13"
  },
  "devDependencies": {
    "@types/bun": "latest"
  }
}%
dajkim76@Kims-Mac-mini ~/test/hono_test % bun dev
$ bun run --hot src/index.ts
Started server http://localhost:3000

bun으로 돌아가는 프로젝트로 생성된 기본 코드는

import { Hono } from 'hono'

const app = new Hono()

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

export default app

 

https (TLS) 지원

export default app을 아래로 바꾸면 tls 지원 끝

export default { 
  port: 3000, 
  fetch: app.fetch, 
  tls: {
    cert: Bun.file("/Users/dajkim76/test/server.crt"),
    key: Bun.file("/Users/dajkim76/test/server.key"),
  },
}

 

compression (gzip 지원)

hono/compress가 bun에서는 아직 미지원이다. 
(https://hono.dev/docs/middleware/builtin/compress 
https://github.com/oven-sh/bun/issues/1723  )

https://www.npmjs.com/package/bun-compression 를 이용해서 gzip을 지원할 수 있을 듯..

 bun add bun-compression

import { compress } from 'bun-compression'

const app = new Hono()

app.use("*", compress())

Postman으로 응답 해서 Content-Encoding gzip 확인

근데 테스트 결과.. bun-compression 미들웨어를 사용하면 httpCode 응답이 200으로 고정되는 문제가 있다.  404 not found 요청도 마찬가지.  c.status(500) 이런게 클라이언트까지 전달되지 않았다. 이유는 모름.

 

https://github.com/oven-sh/bun/issues/1723#issuecomment-1774174194  이쪽 코드를 참고하여 해결했다. 

import { compress } from 'hono/compress' 를 그대로 사용하면서,  미 구현된 CompressionStream과 DecompressionStream 을 핵으로 구현한다. 

import { compress } from 'hono/compress'





// @bun

/*! MIT License. Jimmy Wärting <https://jimmy.warting.se/opensource> */
import zlib from 'node:zlib'

// fyi, Byte streams aren't really implemented anywhere yet
// It only exist as a issue: https://github.com/WICG/compression/issues/31

const make = (ctx, handle) => Object.assign(ctx, {
  writable: new WritableStream({
    write: chunk => handle.write(chunk),
    close: () => handle.end()
  }),
  readable: new ReadableStream({
    type: 'bytes',
    start (ctrl) {
      handle.on('data', chunk => ctrl.enqueue(chunk))
      handle.once('end', () => ctrl.close())
    }
  })
})

globalThis.CompressionStream ??= class CompressionStream {
  constructor(format) {
    make(this, format === 'deflate' ? zlib.createDeflate() :
    format === 'gzip' ? zlib.createGzip() : zlib.createDeflateRaw())
  }
}

globalThis.DecompressionStream ??= class DecompressionStream {
  constructor(format) {
    make(this, format === 'deflate' ? zlib.createInflate() :
    format === 'gzip' ? zlib.createGunzip() :
    zlib.createInflateRaw())
  }
}






const app = new Hono()
app.use(compress())

httpCode도 정상 동작하고 헤더에 gzip도 보인다. 요청시 gzip 인코딩도 될 것 같은데 테스트는 안 해봄.

 

Get Remote IP from HonoJS with Bun

의외로 이게 JS 런타임마다 일관적이지 않다.  뭐 당연하겠지만 그래서 프레임웍마다 구현 가이드가 다 다르고 친절하지도 않다. 

https://bun.sh/blog/bun-v1.0.4#implement-server-requestip  여기를 참고하면 requestIP라는 함수가 제공되는 것을 알 수 있고, Bun 런타임에서의 request 객체를 파라미터로 받는다. 따라서  Hono 어딘가에 requestIP 함수가 있을 것이고 어딘가에 rawRequest 같은게 있어야 한다. 

app.get('/', (c) => {
  console.log(c);
  console.log(c.req);

로 두 가지를 찾아보면 

Context {
  env: DebugHTTPSServer {
    address: {
      address: "::",
      family: "IPv6",
      port: 3000,
    },
    development: true,
    fetch: [Function: fetch],
    hostname: "localhost",
    id: "[http]-tcp:localhost:3000",
    pendingRequests: 1,
    pendingWebSockets: 0,
    port: 3000,
    protocol: "https",
    publish: [Function: publish],
    ref: [Function: ref],
    reload: [Function: reload],
    requestIP: [Function: requestIP], <<<------------------------------- requestIP 함수
    
    
    
HonoRequest {
  raw: Request (0 KB) {  <<<-------------------------------- rawRequest 객체
    method: "GET",

 

따라서 아래처럼 호출해보면

app.get('/', (c) => {
  //console.log(c.req);
  const ip = c.env.requestIP(c.req.raw);  
  console.log(ip);
  console.log(ip.address);
  return c.text('Hello Hono! :' + ip.address)
})


이렇게 응답함..

{
  address: "::ffff:192.168.0.191",
  family: "IPv6",
  port: 57296,
}
::ffff:192.168.0.191

 

Logger

기본 logger, 별로 기능 없는 

import { logger } from "hono/logger"

const app = new Hono()

app.use("*", logger())

실행 화면

  <-- GET /hello
  --> GET /hello 200 2ms
  <-- GET /hello
  --> GET /hello 200 2ms

https://github.com/honojs/hono/blob/main/src/middleware/logger/index.ts logger의 실제 구현부가 매우 간단하고. 날짜, IP, user-Agent 표시도 없고 당연히 file로 쓰는 옵션도 없다.  코드를 참고해서 직접 구현해야 한다..

custom-logger.ts

import { MiddlewareHandler } from "hono/types"

export const customLogger = (): MiddlewareHandler => {
    return async function logger(c, next) {
      const { method, path } = c.req
      const userAgent = c.req.header('user-agent')
      const ip = c.env.requestIP(c.req.raw).address      
      const datetime = new Date().toLocaleString()
      
      await next()        
      console.log(datetime, method, path, c.res.status, ip, userAgent)
    }
  }

 

index.ts

import { customLogger } from './custom-logger';
app.use(customLogger()

 

동작확인

7/13/2024, 12:28:14 AM GET / 200 ::1 PostmanRuntime/7.37.3
7/13/2024, 12:28:16 AM GET / 200 ::1 PostmanRuntime/7.37.3
7/13/2024, 12:28:16 AM GET / 200 ::1 PostmanRuntime/7.37.3
7/13/2024, 12:28:20 AM GET / 200 ::ffff:192.168.0.191 Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Mobile Safari/537.36

파일로 쓰기 구현은 설계부터 하자. 당분간 리다이렉트로 땜빵..  

 

기본 인증 basicAuth

import { basicAuth } from 'hono/basic-auth'

const app = new Hono()

app.use('/admin/*', basicAuth({ 
  username: 'test1', 
  password: 'test1'
}));


app.get('/admin', (c) => { 
  //console.log(c.req);
  return c.text('Logged in') 
});

잘 동작 함

 

Rate limit

https://github.com/rhinobase/hono-rate-limiter  

bun add hono-rate-limiter 로 6초에 10번의 요청시 에러 응답

import { rateLimiter } from "hono-rate-limiter";

const app = new Hono()

const limiter = rateLimiter({
  windowMs: 6 * 1000, 
  limit: 10,
  standardHeaders: "draft-6",
  keyGenerator: (c) => { 
    const ip = c.env.requestIP(c.req.raw).address;
    console.log("reqIP:"+ip);
    return ip;
  }, // Method to generate custom identifiers for clients.
});

app.use(limiter);

 

테스트 결과 Limit에 도달시  응답 body에 text로 나간다. 

Too many requests, please try again later.

http 코드는 429가 내려온다. 하지만 타이밍이 좀 이상하다. 응답을 서버가 지연시키고 있는 듯한 느낌.  https://github.com/rhinobase/hono-rate-limiter/blob/main/packages/core/src/core.ts. 코드를 보니  options.message 와 options.handler로 적당히 커스터마이징도 가능할 듯 하다. 

  message: async (c) => {
    return {code: 429, message: "too many requests"};
  }, 
  
  이렇게 하거나 handler를 직접 넣어주면 휠씬 다양하게 대응 가능
  
  handler: async (c, _, options) => {
    // TODO: 이겨서 ip를 저장해 두었다가 1시간 동안 request 블럭시키는 것도 가능할 듯.
    
    // 로그가 안 찍혀서 직접 찍는다. 근데 logger에서 왜 안 찍히지.
    const { method, path } = c.req
    const userAgent = c.req.header('user-agent')
    const ip = c.env.requestIP(c.req.raw).address      
    const datetime = new Date().toLocaleString()    
    console.log(datetime, method, path, options.statusCode, ip, userAgent)

    // Json으로 응답하자
    return c.json({code: 429, message: "block too many requests"}, options.statusCode)
  }

 

Error Handling

https://hono.dev/docs/api/exception 

// 나만의 에러 정의..
class ErrorCodeException extends Error {
  readonly code: number;
  
  constructor(code: number, message: string) {
    super(message)
    this.code = code;
  }
}


app.onError((err, c) => {
  if (err instanceof ErrorCodeException) {
    console.log(`ErrorCodeException: ${err.code} ${err.message}`); 
    return c.json({code: err.code, message: err.message}, 500);
  }
  if (err instanceof HTTPException) {
    // Get the custom response
    console.log(`HTTPException: ${err.message}`); 
    return c.json({code: err.status, message: err.message}, err.status);
  }
  //...
})


어딘가에서 에러를 던지면.. 
throw new ErrorCodeException(1234, `error remote ip: ${ip}`);

HttpCode 500에 json body로 응답한다. 
{
    "code": 1234,
    "message": "error remote ip ::1"
}

HTTPException과 ErrorCodeException과 UnhandledException (이건 nodejs와 동일 할 듯) 처리하고 로그 남길 수 있다면  큰 문제 없을 듯. 중대한 오류일 때는 반드시 파일로 남기거나 Discord로 알리는 코드는 nestjs와 다르지 않을 듯..

 

Firebase IdToken Auth

https://github.com/honojs/middleware/tree/main/packages/firebase-auth   

테스트 안 해봄. 되겠지 뭐.. 

 

Cron

가벼운 프레임웍이라서 cron job 실행은 지원하지 않는다.  별도의 process로 구현하던지 Linux  cron을 이용하던지.. 

 

소감

전체적 느낌적으로 Elysia보다 나은 것 같다.  node, deno에서도 동작하고 여러 플랫폼에서 다양하게 지원되는 가벼운 프레임웍이면서 커스터 마이징도 쉽다. bun과 사용되면 성능도 상위권이다. ( node를 사용한다면  NestJS with fastify 가 나을 수 있다. Elysia도 bun에서는 상위권 성능이지만 Hono와는 대동소이하다.)  https://github.com/honojs/hono 에서 확인해보니 Github Star와 Fork 카운트가 Hono가 두 배 더 많다.  릴리스도 훨씬 더 많이 했다. 코드 기여자도 더 많다. 따라서 정성적, 정량적 모두에서 Hono에 호감이 더 간다. 

 

top

posted at

2024. 7. 12. 23:31


CONTENTS

Seize the day
BLOG main image
김대정의 앱 개발 노트와 사는 이야기
RSS 2.0Tattertools
공지
아카이브
최근 글 최근 댓글
카테고리 태그 구름사이트 링크