Scriptc: Vercel의 TypeScript‑to‑Native 컴파일러
Scriptc: Vercel의 TypeScript‑to‑Native 컴파일러
개요
Scriptc는 일반적인 TypeScript를 Node, V8 또는 JavaScript 엔진이 포함되지 않은 독립형 네이티브 바이너리로 컴파일합니다.
설치
npm을 사용하여 컴파일러를 전역으로 설치합니다:
npm install -g scriptc
clang이 필요합니다 (macOS의 경우 Xcode Command Line Tools에서 제공). macOS arm64가 기본 플랫폼이며, Linux 및 Windows 바이너리는 교차 컴파일(cross‑compilation)을 통해 빌드됩니다.
정적 컴파일 vs 동적 컴파일
기본적으로 scriptc는 코드를 네이티브 코드로 정적 컴파일합니다. scriptc coverage 명령어를 사용하면 무엇이 정적으로 컴파일될 수 있고 무엇이 동적으로 남는지 확인할 수 있습니다:
$ scriptc coverage app.ts
statements analyzed 4481
compile statically 4451 (99%)
blockers:
×2 functions with optional parameters as values SC1090
×1 Promise.reject SC2020
세 가지 계층이 명시되어 있습니다:
- 정적 컴파일(Compiled statically) – 네이티브 코드, 엔진 없음; 기본 모드입니다.
- 동적 실행(
--dynamic) – 임베디드된 QuickJS‑NG 엔진(~620 KB)이 정적으로 컴파일할 수 없는 코드(예: npm 의존성의 JS,any타입 코드)를 실행합니다. 정적 코드로 다시 넘어오는 값들은 런타임에 검증되며, 타입이 일치하지 않을 경우 포착 가능한TypeError를 발생시킵니다. - 거부됨(Rejected) – 특정 오류 코드, 코드 프레임과 함께 실패하며, 대개 재작성 힌트를 제공합니다. 아무것도 조용히 잘못 컴파일되지 않습니다.
정적 컴파일 대상
정적 컴파일 범위에는 다음이 포함됩니다:
- 언어 기능: 단일 상속과 진정한 동적 디스패치(검증 가능할 때 데버추얼라이즈됨)를 가진 클래스, JS 캡처 시맨틱을 가진 클로저, 제네릭(monomorphized), 태그된 값으로서의 판별된 유니온(discriminated unions), stackful fibers 위의
async/await,finally를 포함한 예외 처리, 구조 분해 할당, spread, 선택적/기본/rest 매개변수, getter/setter, string/array/Map/Set에 대한 반복자(iterators), 템플릿 리터럴, 정규 표현식(QuickJS의 ECMA-exact 바이트코드 인터프리터, regex 사용 시에만 링크됨). - 표준 라이브러리: UTF‑16‑exact 문자열, JS‑exact 정렬 및 식별성을 가진 array/Map/Set, 런타임 검증된 캐스트를 지원하는
JSON,Math, typed array 및Buffer, typedcatch를 지원하는Error계층 구조. - Node API 표면:
fs(sync 및 promises),path(byte‑exact),process, 파이프 스트림이 있는child_process,os,crypto,url/URL,zlib, 의존성 없는 이벤트 루프 위의 타이머 및 시그널 핸들러, 그리고 전체 서버 스택(net,http,https, vendored mbedTLS를 포함한tls),dgram,dns,fs.watch,readline. - fetch 및 WHATWG 웹 서브셋: 동일한 네이티브 net/TLS 스택을 사용하는 streams,
Headers,AbortSignal, 리다이렉트, gzip,AbortSignal.timeout, Node 형태의 error causes를 지원합니다. libcurl이나 시스템 HTTP 의존성은 없습니다. - npm 의존성 (
--dynamic사용 시): Node의 알고리즘을 통해 해결되고, 제공된.d.ts에 대해 타입 검사가 수행되며, 해당 JS는 빌드 시 바이너리에 임베디드됩니다. 바이너리는 런타임에node_modules를 읽지 않습니다.
타입 체크에는 TypeScript의 실제 es2025 lib(및 존재 시 @types/node)가 사용되며, 프로젝트의 tsconfig.json이 검사 엄격도를 제어합니다. lowering(하향 변환)이 불가능한 지점에 도달하면 정확한 진단(diagnostic)을 제공합니다.
정확성
모든 변경 사항에 대해 두 가지 강제 메커니즘이 실행됩니다:
- 차분 테스트(Differential testing): 800개 이상의 프로그램 코퍼스가 Node와 네이티브 바이너리 모두에서 실행됩니다. stdout, stderr 및 종료 코드는 바이트 단위로 일치해야 합니다. 숫자 포맷팅은 JS‑exact입니다(최단 왕복, 100만 개의 double에 대해 Node와 fuzz 검증 완료). 서버는 두 구현체 모두에 대해 라이브 클라이언트 드라이버로 테스트됩니다.
- 메모리 안전성 경로(Memory‑safety lane): 전체 코퍼스가 참조 횟수 감사(reference-count audit)와 함께 AddressSanitizer 하에서 재실행됩니다. 누수(leaks) 및 use-after-free는 빌드 실패를 유발합니다.
Node와의 의도적인 차이(타이밍 내부 및 error-object 속성에 관한 수십 개)는 문서화되고 번호가 매겨집니다. 아무것도 조용히 다르게 동작하지 않습니다.
성능
Apple M‑series에서 Node, Go, Rust, Zig(바이트 단위로 동일한 출력 검증됨)와 비교 측정되었습니다:
- 시작 시간(Startup): ~2.4 ms (Node ~47 ms; Zig와 대등하며 Go/Rust보다 빠름).
- 바이너리 크기: 정적 빌드의 경우 170–200 KB;
--dynamic및 임베디드 의존성 포함 시 ~3 MB (Go ~2 MB; Node SEA 60–100 MB). - 메모리 (RSS): 일반적인 경우 1–4 MB (Node 67–116 MB).
- 런타임: JS‑faithful f64 시맨틱; 대부분의 워크로드에서 시스템 언어와 경쟁 가능; 정수 추론 및 소유권 분석이 로드맵에 있습니다.
탈출구 (Escape Hatches)
comptime(() =>...): 컴파일러 내부의 격리된 VM에서 빌드 타임에 TypeScript를 실행하고 그 결과를 리터럴로 구워냅니다(bakes).- 네이티브 FFI (
--ffi): 시그니처 전용 TypeScript 선언을 직접적인 C ABI 호출에 바인딩하고, 매니페스트에 선언된 아카이브, 객체 및 시스템 라이브러리를 링크합니다. 경계는 명시적이며 길이 제한이 있습니다. --dynamic: npm 의존성 및any코드를 위해 QuickJS‑NG 엔진을 임베디드합니다.scriptc coverage --dynamic은 어떤 문장이 어디서 실행되는지, 어떤 차단 요소가 남아 있는지 정확히 보고합니다. 정적 모드가 기본이며, 바이너리가 조용히 엔진을 키우지 않습니다.- 체크된 캐스트(Checked casts):
JSON.parse(...) as Config는 잘못된 경로를 지칭하며 포착 가능한 에러를 던지는 런타임 검증을 삽입합니다(예:expected number at $.port, got string). TypeScript의as는 약속이지만, scriptc는 이를 검증합니다.
아키텍처
컴파일러 파이프라인은 다음과 같습니다:
TypeScript --(tsc: parse + typecheck)--> Lowering --> Typed IR --> C --> clang --> Native executable
packages/compiler: 프론트엔드 (tsc API → IR), 검증기/직렬화기가 포함된 IR, LLVM 및 C 백엔드. IR은 양 끝단 사이의 유일한 인터페이스입니다. LLVM은 투명한 C 폴백(fallback)을 가진 기본 코드 생성기입니다.packages/runtime: 사이클 컬렉터가 있는 참조 횟수 기반 값을 제공하는 C 런타임, stackful fibers 및 이벤트 루프(kqueue), 서버 스택, JS‑exact 숫자 포맷팅. 기능 유닛은 링크 게이트(link-gated) 방식입니다. 바이너리는 사용하는 기능에 대해서만 비용을 지불합니다.packages/cli:scriptc build | run | coverage를 구현합니다.
개발 워크플로우
pnpm install && pnpm build
pnpm test # 차분 코퍼스 + 진단 스냅샷
SCRIPTC_SAN=1 pnpm test # ASan + RC 감사가 적용된 동일 코퍼스
pnpm scriptc build x.ts --emit-ir #.scriptc/x.c 및 x.ir.json 유지
모든 기능은 차분 테스트와 함께 출시됩니다. 병합 기준을 충족하려면 두 경로 모두 통과해야 합니다.
커뮤니티 반응 (선택된 댓글)
- 실질적 채택에 대한 회의론: “이것을 사용할 만한 진지한 회사나 프로젝트가 떠오르지 않네요.” – @JoeDohn
- 이전 시도들과의 비교: “Porforr는 한동안 같은 목표를 향해 노력해 왔습니다... Vercel이 어떻게 이렇게 빨리 많은 진전을 이루었는지 다소 의기증연하네요.” – @acmnrs
- npm 생태계에 대한 우려: “대부분의 패키지는 타입 선언이 있는 타입 미지정 JavaScript만 제공합니다... npm 패키지를 사용하려면 여전히 JavaScript 엔진이 필요할 것입니다.” – @sheept
- 네이티브 JS에 대한 역사적 관점: “90년대에 GCJ가 있었습니다... GraalVM Native가 마침내 이 문제를 더 총체적으로 다루었습니다... 하지만 단순한 기존 애플리케이션을 네이티브에서 완벽하게 실행되게 만드는 것은 여전히 큰 고역입니다.” – @weinzierl
- 프로덕션 검증에 대한 갈망: “이 프로젝트 중 일부가 프로덕션에서 사용되는 것을 간절히 보고 싶습니다... 잘 된다면 그저 어thoughtless한 홍보용 쇼가 아니라는 것을 증명하는 셈이겠죠.” – @notsylver
- FFI 및 임베딩에 대한 관심: “매우 유망해 보입니다... 이것이 Android/iOS 타겟에서 실행되도록 기존 C++ 앱에 링크할 수 있는 정적 라이브러리로 컴파일될 수 있을지 궁금하네요.” – @elendilm
- 크로스 플랫폼 지원에 대한 질문: “C로 컴파일되나요? Wasm인가요?... Linux 및 Windows 바이너리는 교차 컴파일로 빌드된다고 하던데... 만약 그렇다면 좀 별로네요...” – @MrDrMcCoy
- 크기에 대한 경이로움: “178kb?! 거기에 뭘 넣은 건가요, JVM인가요?” – @aabhay
- 열광: “드디어 누군가 해냈군요” – @relug
- 일반적인 관심: “아이디어가 좋네요.” – @casper14
- JavaScript 서브셋에 대한 혼란: “JavaScript가 TypeScript의 유효한 서브셋이라면 어떻게 컴파일할 수 있다는 거죠? 혼란스럽네요.” – @xiaodai
- 유스케이스 추측: “이거 흥미롭네요, 그럼 이제 electron을 네이티브 앱으로 바꿀 수 있다는 건가요? 용도가 뭐죠, 내 node 프로젝트를 리버스 엔지니어링하기 어렵게 만드는 건가요?” – @zuzululu
- 유지보수 및 하이프에 대한 우려: “이 정도면 HN 첫 페이지를 유지하기에 충분하겠네요... 이건 성장 전략입니다... 유지보수가 될까요?” – @piterrro
- 프로덕션 준비성에 대한 의구심: “전에도 이런 이야기를 들은 적이 있습니다... 실제로 프로덕션에서 제대로 작동하는 사례를 하나라도 들 수 있나요? 단 하나도 없습니다.” – @ianberdin
- 단일 실행 파일 생성에 대한 문의: “이것은 내 JS 기반 백엔드를 Go처럼 단일 실행 파일로 만들어 쉽게 배포할 수 있다는 뜻인가요?” – @anta40
- 긍정적인 의견: “화려하진 않지만 작동하네요.” – @hnsmomdpvp
- 타겟이 불분명함: “C로 컴파일되나요? Wasm인가요? 리드미(readme)에서 찾을 수 없었습니다.” – @khalic
이 댓글들은 기술적 성취에 대한 흥분, 실제 사용성에 대한 우려, npm 생태계에 대한 의존성, 그리고 장기적인 유지보수 및 크로스 플랫폼 지원에 대한 질문이 섞여 있습니다.
SUMMARY: Scriptc는 대부분의 코드에는 정적 컴파일을 사용하고 동적 기능에는 선택적으로 QuickJS‑NG 엔진을 사용하여, 일반적인 TypeScript를 JavaScript 엔진 없이 작고 빠른 네이티브 실행 파일로 컴파일합니다.
TITLE: Scriptc: Vercel의 TypeScript‑to‑Native 컴파일러