때는 한가로운 주말 오늘은 뭐하지 하면서 개인프로젝트들을 하나 만들어볼까 하는 도중에 ..
bun어플도 그냥 build해서 내보내고싶다는 마인드로
bun build를 사용하여 프로젝트를 내보내어 bun으로실행하니 나타나는 오류 ..
어이 없던 나는
이거 dev에서 gzip제대로 안 돌아가는 거 아니야 ?
바로 실험에 돌입 해보았는데 .. dev는 또 잘 돌아간다
현재 코드는 아래와 같다
typescriptimport { MiddlewareHandler } from 'hono'
const COMPRESSIBLE_CONTENT_TYPE_REGEX =
/^\s*(?:text\/[^;\s]+|application\/(?:javascript|json|xml|xml-dtd|ecmascript|dart|postscript|rtf|tar|toml|vnd\.dart|vnd\.ms-fontobject|vnd\.ms-opentype|wasm|x-httpd-php|x-javascript|x-ns-proxy-autoconfig|x-sh|x-tar|x-virtualbox-hdd|x-virtualbox-ova|x-virtualbox-ovf|x-virtualbox-vbox|x-virtualbox-vdi|x-virtualbox-vhd|x-virtualbox-vmdk|x-www-form-urlencoded)|font\/(?:otf|ttf)|image\/(?:bmp|vnd\.adobe\.photoshop|vnd\.microsoft\.icon|vnd\.ms-dds|x-icon|x-ms-bmp)|message\/rfc822|model\/gltf-binary|x-shader\/x-fragment|x-shader\/x-vertex|[^;\s]+?\+(?:json|text|xml|yaml))(?:[;\s]|$)/i
const ENCODING_TYPES = ['gzip', 'deflate'] as const
const cacheControlNoTransformRegExp = /(?:^|,)\s*?no-transform\s*?(?:,|$)/i
interface CompressionOptions {
encoding?: (typeof ENCODING_TYPES)[number]
threshold?: number
}
export const compress = (options?: CompressionOptions): MiddlewareHandler => {
const threshold = options?.threshold ?? 1024
return async function compress(ctx, next) {
await next()
const { createGzip, createDeflate } = await import('zlib')
const contentLength = ctx.res.headers.get('Content-Length')
if (
ctx.res.headers.has('Content-Encoding') ||
ctx.req.method === 'HEAD' ||
(contentLength && Number(contentLength) < threshold) ||
!shouldCompress(ctx.res) ||
!shouldTransform(ctx.res)
) {
return
}
const accepted = ctx.req.header('Accept-Encoding')
const encoding = options?.encoding ?? ENCODING_TYPES.find((e) => accepted?.includes(e))
if (!encoding || !ctx.res.body) {
return
}
let compressedStream
if (encoding === 'gzip') {
compressedStream = createGzip()
} else if (encoding === 'deflate') {
compressedStream = createDeflate()
} else {
return
}
const nodeToWebReadable = (nodeStream: NodeJS.ReadableStream): ReadableStream => {
return new ReadableStream({
start(controller) {
nodeStream.on('data', (chunk) => controller.enqueue(chunk))
nodeStream.on('end', () => controller.close())
nodeStream.on('error', (err) => controller.error(err))
},
cancel() {
// @ts-ignore
nodeStream.destroy()
},
})
}
const reader = ctx.res.body?.getReader()
reader?.read().then(function process({ done, value }) {
if (done) {
compressedStream.end()
return
}
compressedStream.write(value)
reader.read().then(process)
})
ctx.res = new Response(nodeToWebReadable(compressedStream), ctx.res)
ctx.res.headers.delete('Content-Length')
ctx.res.headers.set('Content-Encoding', encoding)
}
}
const shouldCompress = (res: Response) => {
const type = res.headers.get('Content-Type')
return type && COMPRESSIBLE_CONTENT_TYPE_REGEX.test(type)
}
const shouldTransform = (res: Response) => {
const cacheControl = res.headers.get('Cache-Control')
return !cacheControl || !cacheControlNoTransformRegExp.test(cacheControl)
}
그냥 body 가져와서 reader에 buffer로 던지고 Response에 태워 내보내는 형식이다
우리가 봐야 할 부분은
compressedStream, nodeToWebReadable, reader
이 세가지의 부분은 바꾸면 된다
brotli나 deflate등 추후에 구현하면 되는데 .. 굳이 하나하나 늘릴 필요가 있긴한가 싶다
(나중에 port - adapter형식으로 encoder를 넣을 수 있게 하면 재밌을듯 하긴 하다)
여하튼, 이부분을 조금 더 쉽게 만들어보자
먼저 우리가 해야할 목표인
바디 긁기 -> 압축된 버퍼작성 -> 읽기 스트림으로 변환 -> Response 이순서를 그대로 구현하면 된다
먼저 body에서 buffer를 땡겨온다
typescript// 참고는 다음 링크를 보자 https://hono.dev/docs/api/request#arraybuffer
const rawBody = await ctx.res.arrayBuffer()
그리고 Bun에서 제공하는 gzip기능을 가져오자
typescript// gzip 도큐는 여기 https://bun.sh/docs/api/utils#bun-gzipsync
const compressedBuffer = Bun.gzipSync(new Uint8Array(rawBody))
그다음 스탭은 기존 코드와 같다. ReadableStream을 이용해서
작성한 압축된 buffer를 스트림에 넣고 response해주면 끝이다
typescriptconst stream = new ReadableStream({
start(controller) {
controller.enqueue(compressedBuffer)
controller.close()
},
cancel() {
stream.cancel()
},
})
ctx.res = new Response(stream, ctx.res)
ctx.res.headers.delete('Content-Length')
ctx.res.headers.set('Content-Encoding', encoding)
그리고 수정된 코드와 테스팅 페이지를 주섬주섬 준비해보자
import { compress } from 'compress'
import { Hono } from 'hono'
import { TestCompress } from './page/testingpage'
const app = new Hono()
app.use('*', compress())
app.get('/', TestCompress)
app.get('/api/gzip', (c) => {
const payload = {
message: 'Hello world! '.repeat(1000),
description: 'Compression test sample'.repeat(800),
lines: Array.from({ length: 500 }, (_, i) => `Line ${i} - this is a test line that repeats.\n`),
}
return c.json(payload)
})
마지막으로 빌드하고 실행해보면 ..
퍼~~펙트하다
이렇게 기존에 사용하던 zlib라이브러리를 지우고 Bun메서드를 이용해서 더욱더 안전한.. 코드 작성이 끝났다
주말에 오랜만에 페이지 짜는게 아니라 기술적인 무언가를 한 기분이다
담주도 화이팅하자