Scriptc - TypeScript를 네이티브 바이너리로 컴파일하기
Vercel Labs가 Scriptc를 공개했습니다. TypeScript를 Node.js도 V8도 없이 단독 네이티브 실행파일로 컴파일하는 도구입니다. 2026년 7월 26일 Hacker News에서 262포인트에 댓글 151개가 달렸고, GitHub 별은 1.6k, 라이선스는 Apache-2.0입니다.
숫자부터 보면 기동 시간 평균 약 2.4ms, 바이너리 크기 170KB에서 200KB(정적), 의존성을 임베드하면 약 3MB입니다.
TypeScript를 컴파일한다는 말의 난점
TypeScript는 타입을 지우면 JavaScript가 되는 언어입니다. 이 설계가 도입 장벽을 낮췄지만, 컴파일러를 만들려는 쪽에는 정확히 그 지점이 벽입니다.
number라고 적혀 있어도 런타임 표현이 정해지지 않습니다. JS의 number는 IEEE 754 배정밀도 부동소수점 하나뿐이고, 엔진은 상황에 따라 정수로 최적화하지만 그건 언어 의미론이 아니라 구현 세부입니다. 객체의 프로퍼티도 마찬가지입니다. 타입이 {a: number}여도 런타임에는 임의의 키가 추가될 수 있습니다.
즉 TypeScript의 타입은 검사용 주석이지 메모리 레이아웃 지시가 아닙니다. C나 Rust처럼 타입에서 표현을 유도하는 컴파일 전략을 그대로 쓸 수 없습니다.
3티어 구조
Scriptc가 택한 답은 그 간극을 없애려 하지 않고 인정하는 것입니다. 코드를 세 등급으로 나눕니다.
기본은 정적 네이티브 컴파일입니다. 파이프라인은 TypeScript를 tsc로 파싱하고 타입체크한 뒤, 중간 표현(IR)으로 내리고, C 또는 LLVM 코드를 생성하고, clang으로 네이티브 바이너리를 만듭니다.
정적으로 다룰 수 없는 코드는 임베드된 JavaScript 엔진으로 넘깁니다. quickjs-ng를 쓰고 크기는 약 620KB입니다. 바이너리가 3MB까지 커지는 경우가 여기입니다.
그래도 처리할 수 없으면 컴파일을 거부하되 구체적인 진단을 냅니다. 조용히 느려지는 대신 무엇이 문제인지 알려주는 쪽을 택했습니다.
정적으로 커버하는 범위가 좁지 않습니다. 단일 상속 클래스, 클로저, 제네릭, async/await, 구조분해가 들어가고, 표준 라이브러리 쪽으로는 fs, http/https, crypto 같은 Node API와 fetch를 비롯한 WHATWG 웹 표준까지 포함합니다. 프로젝트 설명은 실제 프로그램이 쓰는 언어와 표준 라이브러리를 정적 표면이 덮는다고 적고 있습니다.
이 부분 정적화 패턴은 새로운 발명이 아닙니다. 파이썬의 mypyc가 타입 주석이 붙은 부분만 C로 컴파일하고 나머지는 CPython 객체로 남기는 방식을 씁니다. 루비의 YJIT도 실행 중 관측된 타입에 대해서만 특화된 기계어를 만들고 벗어나면 인터프리터로 되돌아갑니다. 동적 언어를 정적으로 다루려는 시도는 결국 어디까지 정적으로 볼 것인가의 경계를 어디에 그을 것이냐로 수렴합니다.
런타임을 직접 구현했습니다
엔진을 들어내면 엔진이 해주던 일을 대신해야 합니다. Scriptc가 손수 만든 부분이 여기입니다.
메모리 관리는 참조계수에 사이클 수집을 붙였습니다. V8은 세대별 추적 GC를 쓰는데, 참조계수를 고른 것은 지연 특성 때문으로 읽힙니다. 추적 GC는 처리량이 좋지만 수집이 언제 일어날지 예측하기 어렵고, 그때 멈춤이 생깁니다. 참조계수는 처리량에서 손해를 보는 대신 해제 시점이 결정론적입니다. 짧게 돌고 끝나는 CLI나 서버리스 함수에서는 이쪽이 맞습니다. 순환 참조는 참조계수만으로 못 잡으므로 사이클 수집기를 따로 붙였습니다.
비동기는 stackful fiber와 kqueue 이벤트 루프로 구현했습니다. kqueue는 BSD 계열 이벤트 통지 인터페이스이므로 macOS와 BSD가 1차 대상입니다.
그리고 JS와 정확히 같은 시맨틱을 재현해야 하는 지점들이 있습니다. 숫자 포맷팅과 자료구조 순회 순서가 대표적입니다. 0.1 + 0.2를 문자열로 만들면 0.30000000000000004가 나와야 하고, 객체 키 순회 순서도 정수 키가 오름차순으로 먼저, 그다음 문자열 키가 삽입 순서로 나와야 합니다. 이걸 어기면 기존 코드가 조용히 다르게 동작합니다.
2.4ms가 의미하는 것
기동 2.4ms가 이 도구의 존재 이유입니다.
Node 프로세스 기동은 통상 수십에서 수백 밀리초입니다. V8 초기화, 스냅숏 역직렬화, 모듈 해석이 매번 일어납니다. 오래 살아 있는 서버에서는 한 번 치르고 끝이라 문제가 아닙니다.
문제가 되는 곳은 두 군데입니다. 첫째는 서버리스입니다. 콜드 스타트가 요청 지연에 그대로 더해집니다. 둘째는 CLI입니다. 사용자가 명령 하나 칠 때마다 그 비용을 냅니다. git처럼 자주 쓰는 도구가 200ms씩 걸리면 체감이 확 다릅니다.
바이너리 170KB에서 200KB라는 크기도 같은 맥락입니다. 배포 아티팩트가 작으면 컨테이너 이미지가 가벼워지고 콜드 스타트에서 코드를 끌어오는 시간도 줄어듭니다.
정리
Scriptc는 TypeScript를 완전히 정적으로 만들겠다고 하지 않습니다. 정적으로 표현 가능한 부분만 네이티브로 내리고, 나머지는 620KB짜리 엔진에 넘기고, 그것도 안 되면 거부합니다. 경계를 명확히 긋고 각 구간에서 다른 전략을 쓰는 설계입니다.
kqueue 기반이라 리눅스 지원 범위와 성숙도는 확인이 필요합니다. 그리고 정적 티어에서 벗어나는 코드가 얼마나 자주 나오는지는 실제 프로젝트에 붙여봐야 알 수 있습니다. 정적으로 잡히는 비율이 낮으면 3MB 바이너리에 quickjs를 안고 다니는 셈이 되고, 그러면 기동 이득도 줄어듭니다.
저장소는 github.com/vercel-labs/scriptc이고, 논의는 Hacker News 스레드에서 볼 수 있습니다.