Ruby에서 C 언어를 통해 JavaScript 실행하기: QuickJS RB 프로젝트

[EN] Running JavaScript within Ruby / Kengo Hamasaki @hmsk

3줄 요약

  • QuickJS RB는 Ruby 환경에서 JavaScript 코드를 실행할 수 있도록 QuickJS를 래핑한 Ruby 젬입니다.
  • 이 프로젝트는 Ruby on Rails 백엔드에서 JavaScript 기반의 '커스텀 코드' 기능을 효율적으로 구현하기 위해 개발되었습니다.
  • QuickJS RB는 샌드박싱, 국제화 API 지원, Ruby 함수를 JavaScript에서 호출하는 기능 등 다양한 기능을 제공하며 실제 서비스에 적용되어 비용 절감 및 성능 향상에 기여하고 있습니다.

발표자는 RubyKaigi 2023에서 'Running JavaScript within Ruby by C-lang in Ruby'라는 주제로 QuickJS RB 프로젝트에 대해 설명합니다. 이 프로젝트는 Ruby 환경에서 JavaScript 코드를 안전하고 효율적으로 실행할 수 있도록 QuickJS 엔진을 Ruby 젬으로 래핑한 것입니다. 발표자는 이 젬을 개발하게 된 배경, 개발 과정, 주요 기능 및 실제 서비스 적용 사례를 상세히 공유합니다. 특히 기존 Function as a Service(FaaS) 방식의 한계를 극복하고 Ruby 백엔드 내에서 직접 JavaScript를 실행함으로써 얻는 이점을 강조합니다.

발표자의 회사인 Persona는 신원 확인을 위한 SaaS 플랫폼을 운영하며, Ruby on Rails 기반의 모놀리식 아키텍처를 사용합니다. 이 플랫폼의 핵심 기능 중 하나는 고객이 직접 JavaScript 코드를 작성하여 플랫폼의 기능을 확장할 수 있는 ‘커스텀 코드’입니다. 초기에는 이 기능을 Amazon Lambda와 같은 FaaS를 통해 구현했으나, 네트워크 지연, 환경 관리의 복잡성, 추가 유지보수 비용 등 여러 문제에 직면했습니다. 이러한 문제 해결을 위해 발표자는 Ruby 서버 내에서 직접 JavaScript를 실행하는 방안을 모색했고, 그 결과 QuickJS를 Ruby 젬으로 래핑한 QuickJS RB를 개발하게 되었습니다.

QuickJS는 Fabrice Bellard가 개발한 작고 빠르며 최신 ECMAScript 사양을 지원하는 JavaScript 엔진입니다. 발표자는 QuickJS를 Ruby에 바인딩하는 과정에서 C 언어와 Ruby의 상호작용에 대한 깊이 있는 이해와 C Ruby의 강력한 기능을 활용했습니다. 특히 Ruby로 테스트 코드를 작성하고 점진적으로 C 언어로 마이그레이션하는 방식을 통해 개발 효율성을 높였습니다. 이 과정에서 ‘Rubyist Walk Around the Seaside’와 같은 훌륭한 자료와 다른 언어의 QuickJS 바인딩 구현 사례를 참고했습니다.

QuickJS RB의 주요 기능은 다음과 같습니다. 1. 샌드박싱 (Sandboxing): QuickJS는 OS 리소스에 직접 접근할 수 있는 강력한 기능을 제공하지만, 보안을 위해 기본적으로 이러한 기능을 제한하고 필요시 옵션을 통해 활성화할 수 있도록 설계했습니다. 또한, JavaScript의 필수 기능 중 일부는 C-lang으로 구현된 Ruby 대체 기능을 제공하여 안전성을 확보했습니다. 2. 주변 기능 (Peripherals): QuickJS의 가장 큰 약점 중 하나인 국제화(Internationalization) API 부재를 해결하기 위해 기존 오픈소스 프로젝트의 국제화 번들을 QuickJS의 빌드 프로세스에 통합했습니다. 특히 JavaScript 코드를 C 바이너리로 컴파일하여 로딩 오버헤드를 줄이는 창의적인 방법을 사용했습니다. 3. 인터페이스 (Interfacing for Ruby): Ruby에서 JavaScript를 실행하기 위한 사용자 친화적인 인터페이스를 제공합니다. * 모듈 임포트 (Importing Feature): import 메서드를 통해 ES 모듈을 가져올 수 있으며, Ruby의 강력한 패턴 매칭 기능을 활용하여 다양한 임포트 구문을 지원합니다. * 콘솔 로그 (Console Log): JavaScript 내에서 console.log와 같은 함수를 호출하면 해당 로그가 Ruby 런타임 인스턴스에 기록되어 Ruby 측에서 접근할 수 있습니다. * 함수 정의 (Define Function): Ruby로 구현된 함수를 JavaScript 환경에서 호출할 수 있도록 define_function 기능을 제공합니다. 이를 통해 Ruby 백엔드의 HTTP 클라이언트와 같은 특정 기능을 JavaScript 커스텀 코드에서 활용할 수 있습니다. 예외 처리 또한 투명하게 이루어집니다.

QuickJS RB는 현재 Persona의 프로덕션 환경에서 하루 4백만 건 이상의 JavaScript 호출을 처리하며, FaaS 사용으로 인한 비용을 절감하고 HTTP 요청 감소로 인한 성능 향상을 이뤄냈습니다. 또한, 예상치 못하게 DSL(Domain Specific Language) 쿼리 파서와 같이 프론트엔드와 백엔드 모두에서 동일한 JavaScript 코드를 재사용하는 시나리오에도 활용되어 그 가능성을 확장하고 있습니다.

QuickJS RB 프로젝트는 Ruby 환경에서 JavaScript 코드를 내재화하여 기존 FaaS 기반 시스템의 한계를 극복하고 운영 효율성 및 비용 절감을 달성한 성공적인 사례입니다. C 언어와 Ruby의 상호운용성을 적극적으로 활용하고, 샌드박싱, 국제화 지원, Ruby-JS 함수 바인딩 등 실용적인 기능을 제공함으로써 실제 서비스 환경에서 안정적으로 운영되고 있음을 입증했습니다. 이 프로젝트는 Ruby 개발자들에게 네이티브 확장을 구축하는 즐거움과 가능성을 보여주며, 다양한 활용 사례를 통해 그 잠재력을 더욱 넓혀가고 있습니다.