실무에서 Python 애플리케이션을 개발하다 보면 예상치 못한 요구사항을 마주할 때가 있습니다.
저 역시 그런 상황에서 "Python에서 JavaScript를 실행해야 한다"는 과제를 받게 되었습니다.
처음에는 막막했지만, V8 JavaScript 엔진을 임베딩해서 해결하는 과정에서 많은 것을 배울 수 있었습니다.
이번 글에서는 그 경험을 공유해보고자 합니다.
왜 Python에서 JavaScript를 실행해야 했을까?
실무에서 이런 요구사항이 생기는 경우는 생각보다 다양하다고 생각을 합니다.
- 기존 JavaScript 자산 재활용 : 이미 작성된 JavaScript 로직을 Python 환경에서 사용
- 동적 설정 처리 : 복잡한 조건부 로직이 포함된 설정 파일
- 비개발자 친환경 스크립팅 : 상대적으로 접근하기 쉬운 JavaScript 문법 활용
- 크로스 플랫폼 로직 공유 : 프론트엔드와 백엔드에서 동일한 비즈니스 로직 사용
저의 경우에는 세 번째 케이스였습니다. API 테스트나 설정 관리에서 비개발자도 쉽게 스크립트를 작성하고 수정할 수 있는 환경이 필요했습니다.
javaScript는 Python보다 상대적으로 접근하기 쉽고, 테스트 로직을 직관적으로 표현할 수 있어서 선택하게 되었습니다.
Python에서 JavaScript 실행 : 어떤 방법들이 있을까?
제가 실제로 시도했단 방법들만 소개해드리겠습니다.
1. Python으로 JavaScript 파싱 (실패)
처음에는 "JavaScript 코드를 Python으로 변환하면 되지 않을까?" 라고 생각했습니다.
# 초기 시도: JavaScript를 Python으로 변환
class NaiveJSParser:
def parse_pm_test(self, js_code):
# pm.test("name", function() {}) -> def test_name():
pattern = r'cp\.test\("(.+?)",\s*function\s*\(\)\s*{(.+?)}\)'
def replacer(match):
test_name = match.group(1)
test_body = match.group(2)
# JavaScript 코드를 Python으로 변환 시도
python_body = test_body.replace('pm.response', 'self.response')
return f"def test_{test_name}():\n {python_body}"
return re.sub(pattern, replacer, js_code, flags=re.DOTALL)
여기서 발생하는 문제점:
- JavaScript의 동적 타입, 프로토타입 체인, 클로저를 Python으로 변환 불가능
- 체이닝 API 구현 불가능
- 정규식으로는 복잡한 JavaScript 구문 파싱 한계
2. PyExecJS (부분 성공, 하지만 포기)
다음으로 시도한 것은 PyExecJS였습니다.
import execjs
class PyExecJSRunner:
def __init__(self):
# Node.js 런타임 사용
self.ctx = execjs.compile("""
var cp = {
test: function(name, fn) {
try {
fn();
return {success: true, name: name};
} catch(e) {
return {success: false, name: name, error: e.message};
}
}
};
""")
def run_test(self, script):
return self.ctx.eval(script)
여기서 발생하는 문제점:
- Node.js 설치 필수 (보안망, 기업망 등 기업 환경에서 제약 사항 발생)
- Python ↔ Node.js 간 데이터 직렬화 오버헤드
- 디버깅 어려움 (에러 위치 추적 불가)
- 실행 속도가 예상보다 느림
그래서 결국 제가 선택했던 방법은
3. py-mini-racer로 V8 엔진 직접 임베딩
from py_mini_racer import py_mini_racer
def run_js(script: str, res_obj: dict, response_time: int,
globals_dict: dict, url_string: str):
ctx = py_mini_racer.MiniRacer()
# 1단계: console 객체 구현
ctx.eval("""
var console = {
logs: [],
log: function () {
var msg = Array.prototype.slice.call(arguments).join(" ");
console.logs.push(msg);
}
};
""")
# 2단계: CP 환경 에뮬레이션...
이 방법이 성공한 이유는
- V8 엔진 네이티브 성능
- Python 프로세스 내에서 직접 실행
- 메모리 공유로 데이터 전달 효율적
하지만 하나 지금 생각이 난거지만 Js2Py를 먼저 알았다면 한번쯤이 시도해보지 않았을까 싶습니다.
JavsScript 코드를 Python함수로 변환을 하고, V8엔진이 없이도 순수 Python에서 실행이 가능합니다.
그로 인해 의존성도 적고 디버깅이 더 쉬웠을 수도... 있습니다.
지금 생각해보면 당시에는 py-mini-racer를 선택한 이유가 "V8 엔진 = 성능" 이라는 선입견 때문이었던 것 같습니다.
하지만 실제로는 Js2Py가 더 나은 선택이었을 수도 있겠다는 생각이 듭니다.
특히 디버깅과 유지보수 측면에서 말이죠.
V8 엔진을 선택한 이유
성능이 결정적이었습니다.
제가 맡은 작업에서는 수백 개의 스크립트를 연속으로 빠르게 실행해야 했습니다.
각 방법의 성능 테스트를 해본 결과:
- subprocess + Node.js: 프로세스 생성 오버헤드로 가장 느림
- PyExecJS: Node.js 의존성과 데이터 직렬화 비용
- py-mini-racer: 초기화 후에는 가장 빠른 실행 속도
메모리 호율성
V8엔진은 구글이 수년간 최적화해온 메모리 관리 시스템을 가지고 있습니다.
가비지 컬렉션이 효율적이고, 메모리 사용량도 예측 가능했습니다.
표준 준수
ECMScript 표준을 가장 잘 준수하는 엔진 중 하나여서, 기존 JavaScript 코드의 호환성이 뛰어났습니다.
py-mini-racer 기본 사용법
생각보다 사용법은 간단했습니다.
from py_mini_racer import py_mini_racer
ctx = py_mini_racer.MiniRacer()
result = ctx.eval('1 + 1') # 2
Python 데이터를 JavaScript로 전달할 때는 JSON을 활용했습니다.
import json
data = {'name': 'John', 'age': 30}
js_code = f"""
var data = {json.dumps(data)};
// JavaScript 로직 처리
data.age + 10;
"""
result = ctx.eval(js_code)
또한 한 번 정의한 함수를 여러 번 호출할 수 있어서 효율적이었습니다.
ctx.eval("""
function processData(input) {
// 복잡한 로직
return result;
}
""")
# 여러 번 호출
result1 = ctx.eval("processData('data1')")
result2 = ctx.eval("processData('data2')")
다만 실무에서 개발하다보니 맞닥뜨린 문제들이 있었습니다.
1. 메모리 누수와의 싸움
- 초기에는 하나의 컨텍스트를 게속 재사용했는데, 시간이 지나면서 메모리 사용량이 계속 증가했습니다.
해결한 방법으로는
- 적당한 주기로 컨텍스트를 새로 생성
- 메모리 사용량을 모니터링 하는 로직 추가
실제로 언제 재생성할지 정하는 것이 까다로웠습니다. 너무 자주 하면 성능 저하, 너무 늦으면 메모리 부족이 발생했기 때문입니다.
2. 비동기 코드는 포기
JavaScript의 `setTimeout`, `Promise` 같은 비동기 기능은 사용할 수 없었습니다.
V8엔진만 임베딩 했기 때문에 이벤트루트가 없었습니다.
이걸 우회하기 위해서 비동기가 필요했던 부분은 Python에서 처리를 진행하였고,
JavaScript는 순수 계산 로직만 담당했습니다.
3. 에러 메시지가 불친절
JavaScript에서 에러가 발생하면 스택 트레이스가 JavaScript 컨텍스트 기준으로만 나와서, 실제 어떤 스크립트에서 문제가 생겼는지 찾기 어려웠습니다.
그래서 저는 실행 전후로 디버깅용 로그를 추가하였고, try-catch로 에러 발생구간을 좁혀 나가면서 개발을 진행하였습니다.
해당 프로젝트를 하면서 아쉬웠던 부분
1. 제한적인 JavaScript 환경
브라우저나 Node.js에서 제공하는 다양한 API들을 사용할 수 없었습니다. 순수 ECMAScript 기능만 사용 가능했죠.
2. 디버깅
JavaScript 전용 디버거를 사용할 수 없어서, 복잡한 로직을 디버깅할 때 어려움이 있었습니다. 지금 생각해보니 Js2Py를 사용했다면 Python 디버거로 JavaScript 코드까지 디버깅할 수 있었을 텐데 하는 아쉬움이 있습니다.
3. 문서와 커뮤니티
py-mini-racer의 한국어 자료가 거의 없고, Stack Overflow에서도 관련 질답이 많지 않아서 문제 해결할 때 시간이 오래 걸렸습니다.
4. 플랫폼별 설치 이슈
Windows, macOS, Linux별로 설치 과정이 조금씩 달랐고, 가끔 컴파일 문제가 발생하기도 했습니다.
Python에서 JavaScript를 실행하는 것은 처음에는 이상하게 느껴졌지만, 실제로 사용해보니 꽤 실용적인 도구였습니다.
특히 기존 자산을 활용해야 하는 상황에서는 매우 유용했습니다.
다만 만능 해결책은 아니라는 점도 분명합니다. 프로젝트의 요구사항과 팀의 상황을 충분히 고려한 후 도입을 결정하시길 권합니다.
그리고 저처럼 Js2Py같은 다른 대안들도 미리 살펴보시기 바랍니다.
감사합니다.
'python' 카테고리의 다른 글
| [python] BeautifulSoup header 설정 방법 (0) | 2024.02.19 |
|---|---|
| python list element count (collections.Counter) (0) | 2023.11.15 |
| python list unique 값 확인 하는 방법 (0) | 2023.11.15 |
| python을 활용하여 json 데이터 받아오기 (df 변환) (0) | 2023.08.28 |
