Pyodide: WebAssembly로 브라우저에서 진짜 Python 실행하기
Pyodide는 CPython을 WebAssembly로 컴파일해 브라우저에서 NumPy와 pandas를 그대로 실행하게 해줍니다. 작동 원리와 적합한 사용처를 정리했습니다.

브라우저에서 Python을 돌리는 것은 2010년대 초반부터 많은 개발자들의 꿈이었습니다. 그런데 매번 같은 벽에 부딪혔죠. Python은 CPython의 C 런타임에 깊이 묶여 있어서 웹 환경으로 옮기기가 쉽지 않습니다. Transcrypt, Brython, Skulpt는 Python을 JavaScript로 다시 구현하는 방식으로 이 문제를 풀려고 했습니다. 단순한 스크립트에서는 잘 동작했지만, NumPy나 pandas, 그리고 Python을 쓸모 있게 만드는 C 확장 라이브러리가 필요해지는 순간 무너졌습니다.
Pyodide는 접근 방식이 다릅니다. 진짜 CPython 인터프리터를 WebAssembly로 컴파일합니다. 재구현이 아니라 실제 CPython 소스 코드를 Emscripten으로 컴파일해서 브라우저에서 돌리는 것이죠. 그래서 C 확장 모듈까지 포함해 언어 호환성이 거의 완벽합니다. import numpy를 쓰면 그냥 동작합니다. 컴파일된 Wasm 바이너리 안에 실제 NumPy의 C 코드가 들어 있기 때문입니다.
Pyodide 내부 동작 원리
Pyodide 빌드 과정은 CPython 소스 트리를 Emscripten으로 컴파일하는 것부터 시작합니다. Emscripten은 네이티브 기계어 대신 WebAssembly를 타깃으로 하는 컴파일러 툴체인입니다. 결과물은 Python 인터프리터를 구현한 .wasm 바이너리이고, 여기에 이 인터프리터를 브라우저 환경과 연결하는 JavaScript 글루 코드가 함께 붙습니다.
까다로운 부분은 C 확장 모듈입니다. 예를 들어 NumPy에는 10만 줄이 넘는 C와 Fortran 코드가 들어 있습니다. Pyodide는 NumPy, pandas, scikit-learn, matplotlib, scipy처럼 많이 쓰이는 패키지를 골라 미리 컴파일해서 Wasm 모듈로 제공하고, 필요할 때 불러올 수 있게 합니다. import numpy를 호출하면 Pyodide가 미리 컴파일된 NumPy Wasm 모듈을 내려받아 실행 중인 인터프리터에 링크하고 초기화합니다.
<!-- Minimal Pyodide example -->
<script src="https://cdn.jsdelivr.net/pyodide/v0.27.0/full/pyodide.js"></script>
<script>
async function main() {
// Load the Python interpreter (~10MB download)
const pyodide = await loadPyodide();
// Run Python code directly
pyodide.runPython(`
import sys
print(f"Python {sys.version} running in the browser!")
`);
// Load packages on demand
await pyodide.loadPackage('numpy');
// Use NumPy — the real NumPy, compiled to Wasm
const result = pyodide.runPython(`
import numpy as np
arr = np.random.randn(1000)
f"Mean: {arr.mean():.4f}, Std: {arr.std():.4f}"
`);
console.log(result); // "Mean: 0.0123, Std: 1.0045"
}
main();
</script>
JavaScript와 Python 연결 다리
Pyodide의 가장 큰 강점 중 하나는 Python과 JavaScript 사이의 매끄러운 상호 운용입니다. Python 객체는 자동으로 JavaScript 쪽에 프록시로 노출되고, 반대 방향도 마찬가지입니다. Python에서 JavaScript 함수를 호출하고 DOM을 직접 조작할 수 있으며, 수동 직렬화 없이 두 언어 사이에서 데이터를 주고받을 수 있습니다.
// JavaScript calling Python
const pyodide = await loadPyodide();
// Define a Python function
pyodide.runPython(`
def analyze(data):
import statistics
return {
'mean': statistics.mean(data),
'median': statistics.median(data),
'stdev': statistics.stdev(data)
}
`);
// Call it from JavaScript with JS data
const analyze = pyodide.globals.get('analyze');
const result = analyze([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
console.log(result.toJs()); // {mean: 5.5, median: 5.5, stdev: 3.03}
// Python accessing the DOM
pyodide.runPython(`
from js import document
element = document.getElementById('output')
element.textContent = 'Updated from Python!'
`);
프록시 시스템은 타입 변환도 자동으로 처리합니다. Python dict는 JavaScript 객체가 되고, list는 배열이, 숫자는 JavaScript 숫자가 됩니다. 예를 들어 백만 개 원소짜리 NumPy 배열을 JavaScript 시각화 라이브러리로 넘기는 것처럼 대용량 데이터를 전달할 때는 공유 메모리 버퍼를 사용해 복사를 피합니다. 성능을 위해서는 이 부분이 매우 중요합니다.
Pyodide가 빛을 발하는 곳
대화형 컴퓨팅 환경. Pyodide 기반의 JupyterLite는 Jupyter 노트북을 브라우저 안에서 완전히 실행합니다. 서버가 필요 없고, Python 커널이 Wasm으로 로컬에서 돌아갑니다. 교육 현장에서는 판도가 바뀝니다. 학생들은 아무것도 설치하지 않고 Python 노트북을 쓸 수 있고, 강사는 서버를 준비할 필요가 없습니다.
데이터 탐색 도구. 사용자가 데이터를 업로드하고 분석하는 웹 애플리케이션이라면, Pyodide로 모든 처리를 클라이언트에서 끝낼 수 있습니다. 데이터가 브라우저 밖으로 나가지 않으니 개인정보 문제가 줄고 서버 비용도 들지 않습니다. pandas 변환을 브라우저에서 돌리는 CSV 업로더는 데이터를 백엔드로 보내는 방식보다 배포가 단순하고 훨씬 안전합니다.
실행 가능한 예제가 담긴 문서. Python 라이브러리 문서에 실제로 실행되는 코드 블록을 넣을 수 있습니다. 정적인 출력만 보여주는 대신, 독자가 코드를 고치고 결과를 바로 확인할 수 있습니다. 학습 효과 면에서 정적 예제와는 비교가 안 될 만큼 좋습니다.
프로토타이핑과 실험. Python으로 생각하는 데이터 과학자라면 브라우저에서 바로 데이터 변환과 시각화를 프로토타입으로 만들고, 결과를 URL 하나로 공유할 수 있습니다. 환경 설정도, 의존성 관리도, '내 컴퓨터에선 되는데요' 같은 문제도 없습니다.
성능의 현실
Pyodide는 빠르지 않습니다. WebAssembly는 네이티브 코드에 비해 오버헤드가 있고, 연산량이 많은 작업에서는 보통 1.5~3배 느립니다. 게다가 Pyodide는 Wasm으로 컴파일된 CPython을 돌리는 것이라, 순수 Python 코드에서는 이미 C보다 100배 느린 CPython에 한 겹이 더 얹힌 셈입니다. 순수 Python 루프에서는 네이티브 CPython보다 대략 2~5배 느립니다.
하지만 핵심은 이겁니다. Pyodide가 유용한 워크로드는 대부분 순수 Python이 아니라 C 확장 호출이 지배합니다. np.dot(a, b)를 호출하면 실제 계산은 컴파일된 C 코드(지금은 Wasm으로 컴파일된)에서 일어납니다. 이 C 코드에 붙는 Wasm 오버헤드는 1.5~3배 수준이고, 대화형 사용 사례라면 충분히 괜찮은 수치입니다. 네이티브에서 10ms 걸리던 NumPy 행렬 곱이 Pyodide에서는 20ms가 됩니다. 노트북에서 '실행'을 누르는 사용자 입장에서는 거의 체감되지 않는 차이입니다.
Performance comparison (approximate):
Native CPython Pyodide (Wasm)
Pure Python loop: 1x 3-5x slower
NumPy operations: 1x 1.5-3x slower
Pandas groupby: 1x 2-3x slower
Startup time: 50ms 2-5 seconds
Package loading: instant (pip) 2-10s (download + init)
Startup is the real cost. Once loaded, interactive
performance is adequate for most use cases.
진짜 문제는 시작 비용입니다. Pyodide 코어 런타임을 불러오는 데만 10MB를 내려받아야 합니다. NumPy가 7MB를 더하고, pandas가 10MB를 더합니다. 빠르게 렌더링돼야 하는 일반 웹 페이지라면 5초짜리 초기화 지연은 치명적입니다. 그래서 Pyodide는 사용자가 환경이 올라오기를 기다릴 것으로 예상되는 애플리케이션, 즉 노트북, 데이터 도구, 대화형 튜토리얼에 가장 잘 맞습니다. 전통적인 웹 페이지에는 적합하지 않습니다.
Pyodide로 할 수 없는 것들
모든 Python 패키지가 Pyodide에서 돌아가지는 않습니다. 시스템 의존성이 있는 패키지, 예를 들면 데이터베이스 드라이버, GUI 라이브러리, OS 전용 API를 호출하는 패키지는 Wasm으로 컴파일되지 않습니다. psycopg2는 PostgreSQL 클라이언트 라이브러리가 필요하고, opencv는 시스템 그래픽 라이브러리가 필요합니다. 이런 의존성은 브라우저 환경에 존재하지 않습니다.
네트워킹도 제한적입니다. Python의 socket 모듈은 브라우저에서 동작하지 않습니다. Wasm에서는 원시 소켓에 접근할 수 없기 때문입니다. HTTP 요청은 Pyodide의 JavaScript 브리지를 통해 브라우저의 fetch() API로 나가므로 CORS 제약을 받습니다. 리슨할 소켓이 없으니 Pyodide에서 Flask 서버를 띄울 수 없고, 임의의 네트워크 연결도 만들 수 없습니다.
스레딩은 Web Worker를 통해 부분적으로 지원됩니다. 다만 Python의 GIL(Global Interpreter Lock)은 여전히 적용되고, 스레딩 모델도 네이티브 CPython과 다릅니다. CPU 바운드 병렬 처리는 스레드보다 멀티프로세싱이 더 잘 맞습니다. 각 워커가 자기만의 Wasm 인스턴스를 갖기 때문입니다.
Pyodide와 트랜스파일 방식 비교
Pyodide의 접근 방식(CPython을 Wasm으로 컴파일)을 대안인 트랜스파일 방식(Python을 JavaScript로 변환)과 비교해볼 만합니다. Transcrypt와 Brython 같은 프로젝트는 Python 문법을 JavaScript로 바꿔서, 브라우저의 JS 엔진에서 네이티브로 돌아가는 코드를 만듭니다.
트랜스파일 방식은 런타임이 더 빠릅니다. 생성된 JavaScript가 Wasm 기반 CPython 속도가 아니라 네이티브 JS 속도로 돌아가기 때문입니다. 내려받을 런타임이 없어 시작 비용도 사실상 0입니다. 대신 호환성을 희생합니다. 트랜스파일된 Python은 C 확장을 실행할 수 없고, 숫자 타입, 문자열 처리, 엣지 케이스 등에서 CPython과 미묘하게 다르게 동작하며, 표준 라이브러리도 일부만 지원합니다.
Pyodide의 강점은 진짜 CPython을 돌린다는 점입니다. Python 코드가 CPython에서 동작한다면 (시스템 수준 의존성만 없다면) Pyodide에서도 동작합니다. NumPy, pandas, scikit-learn에 의존하는 데이터 과학 워크플로라면 이 호환성은 타협할 수 없는 조건입니다. 반대로 C 확장이 필요 없는 단순한 스크립트라면 트랜스파일 방식이 더 나은 선택일 수 있습니다. Wasm과 네이티브 JS를 비교하는 다른 글들과 마찬가지로, 결국 올바른 도구는 워크로드에 따라 달라집니다.
앞으로의 방향
Pyodide는 활발히 개발되고 있고 계속 나아지고 있습니다. 최근 버전에서는 코어 런타임 크기가 줄었고, 스트리밍 컴파일(브라우저가 내려받는 동시에 Wasm을 컴파일하는 방식)로 시작 시간이 개선됐으며, 미리 컴파일된 패키지 목록도 늘었습니다. WebAssembly 생태계도 성숙해지고 있습니다. WASI가 표준화된 시스템 인터페이스를 제공하고 있고, 컴포넌트 모델은 Wasm 모듈 간 상호 운용을 더 좋게 만들어줄 것입니다.
더 큰 흐름은 브라우저가 범용 런타임이 되어가고 있다는 점입니다. JavaScript, Wasm, 그리고 Pyodide 같은 프로젝트 덕분에 거의 모든 언어로 쓴 코드를 브라우저에서 직접 실행할 수 있게 됐습니다. 이것이 서버 측 컴퓨팅을 대체하지는 않습니다. 서버가 필요 없는 연산을 클라이언트로 옮기면서 보완하는 것이죠. Python의 거대한 데이터 과학 생태계에서는 브라우저 기반 클라이언트 실행이 예전에는 불가능했던 사용 사례들을 열어주고 있습니다.


