Rust로 Node.js Native Addon 작성해보기
문제 상황
이미지 리사이징 API가 동시 요청이 많아지면서 응답 시간이 크게 늘어났다. Sharp 라이브러리를 사용 중이었지만, 추가로 워터마크 합성과 메타데이터 처리를 JS로 하면서 병목이 발생했다.
Rust + Neon 선택
C++로 작성할 수도 있었지만, Rust의 메모리 안정성과 최근 관심이 생긴 것도 있어서 neon-bindings를 선택했다.
[dependencies]
neon = "0.8"
image = "0.23"
구현
이미지 버퍼를 받아서 리사이징과 워터마크를 동시에 처리하는 함수를 작성했다.
use neon::prelude::*;
use image::{DynamicImage, imageops};
fn process_image(mut cx: FunctionContext) -> JsResult<JsBuffer> {
let buffer = cx.argument::<JsBuffer>(0)?;
let width = cx.argument::<JsNumber>(1)?.value(&mut cx) as u32;
let data = cx.borrow(&buffer, |data| {
data.as_slice::<u8>().to_vec()
});
let img = image::load_from_memory(&data)
.or_else(|e| cx.throw_error(e.to_string()))?;
let resized = img.resize(width, width, imageops::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)
}
register_module!(mut cx, {
cx.export_function("processImage", process_image)
});
결과
- 평균 응답 시간: 180ms → 45ms
- CPU 사용률 약 30% 감소
- 메모리 릭 없음
Rust의 러닝커브가 있었지만, 성능 크리티컬한 부분에 선택적으로 사용하기에 좋았다. 다만 빌드 환경 셋업이 복잡해서 CI/CD 파이프라인 구성에 시간이 좀 걸렸다.