Rust로 Node.js Native Addon 작성하기

문제 상황

이미지 리사이징 API가 CPU 병목으로 응답 시간이 3초를 넘기는 경우가 잦았다. Sharp 라이브러리를 쓰고 있었지만, 추가 필터링 로직이 순수 JavaScript로 작성되어 있어 느렸다.

Rust + napi-rs 선택

C++로 작성하는 방법도 있었지만, 메모리 안전성과 러닝 커브를 고려해 Rust를 선택했다. napi-rs는 Rust 코드를 Node.js에서 바로 호출할 수 있게 해주는 프레임워크다.

구현

// lib.rs
use napi::bindgen_prelude::*;
use napi_derive::napi;

#[napi]
pub fn process_pixels(data: Vec<u8>, width: u32, height: u32) -> Result<Vec<u8>> {
  let mut result = Vec::with_capacity(data.len());
  
  for chunk in data.chunks(4) {
    let r = chunk[0];
    let g = chunk[1];
    let b = chunk[2];
    let a = chunk[3];
    
    // 커스텀 필터 로직
    result.extend_from_slice(&[r, g, b, a]);
  }
  
  Ok(result)
}
// Node.js에서 사용
const { processPixels } = require('./index.node');

const processed = processPixels(imageData, width, height);

결과

  • 기존 JavaScript 구현: 평균 2.8초
  • Rust Native Addon: 평균 0.4초
  • 약 7배 성능 향상

배운 점

빌드 환경 설정이 까다로웠다. GitHub Actions에서 Linux, macOS, Windows용 바이너리를 각각 빌드해야 했고, glibc 버전 호환성 문제도 있었다. 하지만 성능 개선 효과가 명확해서 충분히 가치가 있었다.

napi-rs의 타입 변환이 생각보다 직관적이고, 에러 핸들링도 Rust의 Result를 그대로 사용할 수 있어 편했다.