Rust로 Node.js 네이티브 모듈 작성하기 - neon 사용기
배경
대용량 이미지 리사이징 API의 응답 속도가 느려서 프로파일링을 돌려봤다. sharp 라이브러리를 쓰고 있었지만, 동시 요청이 많을 때 CPU 병목이 심했다. 이참에 Rust로 직접 작성해보기로 했다.
neon 설정
npm install -g neon-cli
neon new image-processor
neon-cli가 프로젝트 구조를 자동으로 만들어준다. native/src/lib.rs에 Rust 코드를 작성하고, npm run build하면 Node 모듈로 컴파일된다.
Rust 코드 작성
use neon::prelude::*;
use image::imageops::FilterType;
fn resize_image(mut cx: FunctionContext) -> JsResult<JsBuffer> {
let buffer = cx.argument::<JsBuffer>(0)?;
let width = cx.argument::<JsNumber>(1)?.value(&mut cx) as u32;
let height = cx.argument::<JsNumber>(2)?.value(&mut cx) as u32;
let data = cx.borrow(&buffer, |data| data.as_slice().to_vec());
let img = image::load_from_memory(&data)
.or_else(|e| cx.throw_error(e.to_string()))?;
let resized = img.resize(width, height, FilterType::Lanczos3);
let mut output = Vec::new();
resized.write_to(&mut output, image::ImageOutputFormat::Jpeg(90))
.or_else(|e| cx.throw_error(e.to_string()))?;
let mut result = cx.buffer(output.len() as u32)?;
cx.borrow_mut(&mut result, |data| {
data.as_mut_slice().copy_from_slice(&output);
});
Ok(result)
}
#[neon::main]
fn main(mut cx: ModuleContext) -> NeonResult<()> {
cx.export_function("resizeImage", resize_image)?;
Ok(())
}
Node.js에서 사용
const { resizeImage } = require('../native');
app.post('/resize', async (req, res) => {
const buffer = req.file.buffer;
const resized = resizeImage(buffer, 800, 600);
res.type('image/jpeg').send(resized);
});
결과
동일한 이미지 처리 작업 기준으로 약 40% 성능 향상이 있었다. sharp도 네이티브 바인딩을 쓰지만, 우리 유즈케이스에 맞게 최적화하니 더 빨랐다.
Rust의 메모리 안전성 덕분에 segfault 걱정 없이 네이티브 모듈을 작성할 수 있다는 점도 좋았다. C++로 작성했다면 디버깅에 시간을 더 썼을 것 같다.