scriptc
TypeScript 와 JavaScript 를 LLVM 을 거쳐 네이티브 실행 파일로 뽑는 실험적 컴파일러
npm install -g scriptcTypeScript 와 JavaScript 를 네이티브 실행 파일과 WebAssembly 모듈로 컴파일합니다. 파싱과 타입 검사는 TypeScript 컴파일러가 맡고 그 결과를 LLVM IR 로 내보내 clang 이 컴파일합니다. 만들어진 실행 파일에는 작은 네이티브 런타임만 들어가고 Node 나 자바스크립트 엔진은 들어가지 않으며, 정적으로 컴파일할 수 없는 코드는 진단으로 보고합니다. 저장소가 실험 단계임을 명시합니다.
판단
이럴 때 씁니다
- CLI 도구를 배포하면서 사용자에게 Node 설치를 요구하고 싶지 않을 때
- 이미 타입을 갖춘 TypeScript 코드베이스를 그대로 두고 배포 형태만 바꾸고 싶을 때
- 같은 소스에서 네이티브 실행 파일과 WASI 모듈을 함께 뽑아야 할 때
- 내 코드가 정적 컴파일에 얼마나 들어맞는지 수치로 먼저 재 보고 싶을 때
이럴 땐 쓰지 마세요
- 프로덕션 안정성이 필요할 때. 저장소가 실험 단계라고 직접 밝힙니다
- 동적 자바스크립트에 크게 기대는 코드일 때. 정적으로 컴파일되지 않는 부분은 진단으로 막히거나 자바스크립트 엔진을 함께 넣어야 합니다
- 네트워크나 자식 프로세스, 시그널, 파일 감시가 필요한 WASI 빌드일 때. 링크 전에 실패합니다
- 빌드 환경에 clang 이나 Node 20 이상을 둘 수 없을 때
차별점
- 타입 검사를 자체 구현하지 않고 TypeScript 컴파일러를 그대로 쓴 뒤 LLVM IR 로 넘깁니다. 기존 타입 자산이 그대로 값을 합니다.
- 정적으로 컴파일되지 않는 지점을 숨기지 않고 코드가 붙은 진단으로 보고합니다. coverage 명령이 그 비율을 수치로 보여 줍니다.
- 동적 코드가 필요할 때만 명시적으로 자바스크립트 엔진을 묻어 넣습니다. 기본값이 아니라 선택입니다.
- 디버깅용으로 읽을 수 있는 C 백엔드를 함께 유지합니다.
- 테스트 코퍼스가 같은 프로그램을 Node 와 컴파일된 바이너리로 각각 돌려 표준 출력과 오류, 종료 코드를 바이트 단위로 비교합니다.
워크플로
- 01컴파일러를 전역 설치합니다. 컴파일에는 Node 20 이상과 clang 이 필요하지만 결과물 실행에는 필요 없습니다.
- 02scriptc coverage 로 내 프로그램이 정적으로 얼마나 컴파일되는지와 어디가 동적인지를 먼저 봅니다.
- 03scriptc run 으로 한 번에 컴파일하고 실행하거나 scriptc build 로 실행 파일을 떨어뜨립니다.
- 04npm 패키지나 any 타입 코드가 필요하면 --dynamic 을 붙여 자바스크립트 엔진을 실행 파일에 함께 넣습니다. 결과물은 실행 시점에 node_modules 를 읽지 않습니다.
- 05WebAssembly 가 필요하면 Zig 를 준비하고 대상과 컴파일러를 환경 변수로 지정해 빌드합니다.
주요 명령
| 명령 | 설명 |
|---|---|
npm install -g scriptc | 컴파일러를 설치합니다. Node 20 이상과 clang 이 필요합니다. |
scriptc coverage hello.ts | 정적 컴파일 비율과 동적 구간마다의 코드가 붙은 진단을 보여 줍니다. |
scriptc build hello.ts -o hello | 독립 실행 파일을 만듭니다. Node 없이 돕니다. |
scriptc build cli.ts --dynamic -o cli | npm 패키지의 자바스크립트를 실행 파일에 묻어 넣습니다. |
SCRIPTC_CC=zigcc SCRIPTC_TARGET=wasm32-wasi scriptc build hello.ts -o hello.wasm | WASI Preview 1 모듈을 만듭니다. Zig 가 필요합니다. |
함정
설치 전에 확인하세요
- 저장소가 실험 단계임을 첫 문단에 밝힙니다. 프로덕션 배포 기준으로 고를 대상이 아닙니다.
- 정적 빌드에도 작은 네이티브 런타임은 들어갑니다. 런타임이 아예 없는 것이 아니라 Node 와 자바스크립트 엔진이 없는 것입니다.
- WASI 대상에서는 네트워크와 fetch, 자식 프로세스, 시그널, 파일 감시가 링크 전에 SC3002 로 실패합니다. 새니타이저 빌드와 네이티브 FFI, 라이브러리 모드도 같은 계열의 진단 대상입니다.
- 크로스 타깃 빌드에는 Zig 가 따로 필요합니다.
- 컴파일 환경과 실행 환경의 요구 사항이 다릅니다. 컴파일에는 Node 20 이상과 clang 이 필요하지만 만들어진 실행 파일은 둘 다 필요 없습니다.
검토 메모
공식 문서·릴리스·공개 자료를 바탕으로 정리한 편집 메모입니다.
저는 이 프로젝트에서 가장 실용적인 부분이 coverage 명령이라고 봅니다. 네이티브 컴파일을 표방하는 도구는 많지만 대부분 되는지 안 되는지를 빌드해 봐야 압니다. 여기서는 문장 몇 개 중 몇 개가 정적으로 컴파일되는지, 안 되는 지점은 어디이고 왜인지를 코드가 붙은 진단으로 먼저 보여 줍니다. 도입 여부를 재는 비용이 크게 줄어드는 설계입니다.
설명에서 흔히 오해가 생기는 지점을 하나 짚겠습니다. 런타임이 없다는 말이 아니라 Node 와 자바스크립트 엔진이 없다는 말입니다. 정적 빌드에도 작은 네이티브 런타임은 들어갑니다. 그리고 동적 코드가 필요하면 엔진을 묻어 넣는 선택지가 따로 있는데, 이때는 결과물 크기와 성격이 달라집니다. 두 경로를 같은 것으로 보고 계획을 세우면 어긋납니다.
지금 권하는 용도는 좁습니다. 저장소가 실험 단계라고 스스로 밝혔고 WASI 대상에서는 네트워크와 자식 프로세스, 시그널, 파일 감시가 아예 링크 전에 막힙니다. 그래서 저는 사내 CLI 처럼 실패해도 되돌리기 쉬운 대상에서 먼저 재 보시기를 권합니다. 반대로 테스트 코퍼스가 같은 프로그램을 Node 와 네이티브 양쪽에서 돌려 출력과 종료 코드를 바이트 단위로 비교한다는 점은 이 단계 프로젝트치고 드물게 성실한 대목이라고 생각합니다.
도입을 검토하신다면 빌드 환경 요구 사항을 먼저 확인하시기 바랍니다. 컴파일에는 Node 20 이상과 clang 이 필요하고 WebAssembly 대상까지 가면 Zig 가 더 붙습니다. 만들어진 실행 파일은 이 가운데 아무것도 필요로 하지 않지만, 그 부담이 사라진 것이 아니라 배포 대상에서 빌드 환경으로 옮겨 간 것입니다. 지속적 통합 이미지를 새로 짜야 할 수도 있으니 이 이동을 계산에 넣고 값을 재는 편이 정확합니다.
비교
Bun 이나 Deno 의 컴파일 기능이 런타임을 통째로 실행 파일에 묶는 방식이라면, scriptc 는 자바스크립트 엔진 자체를 빼고 네이티브 코드로 내리는 방식입니다. 결과물이 훨씬 작아지는 대신 정적으로 컴파일되지 않는 코드는 진단으로 막힙니다.
최근 변경
v0.0.35는 개발 빌드 속도와 compiler correctness를 집중 개선했습니다. native build cache와 executable object cache를 추가하고 checker RPC를 줄였으며 compiler lazy-load, library program object 분할·translation unit cache로 반복 빌드 비용을 낮췄습니다. dynamic record equality·Array.from 경계 처리 등 compiler 오류도 수정됐고, 앞선 0.0.32~0.0.35 구간에서 FFI callback·aarch64 Linux musl 지원도 보강됐습니다.