원문 캡처 · flaviocopes.com
원문 캡처 · flaviocopes.com

tsrs는 TypeScript 컴파일러를 Rust로 옮기는 실험적 프로젝트다. TypeScript 코드를 검사하고 JavaScript와 선언 파일을 만들 수 있으며, WebAssembly 빌드는 브라우저에서도 실행된다. Cloudflare와 VoidZero는 2026년 10월 20일 프로젝트를 발표했다. 컴파일러 포트를 만든 Owner.com의 맥스 슈웽크(Max Schwenk)가 TypeScript 팀과 협력하며, 프로젝트는 Oxc 도구 체인에 합류한다.

프로젝트가 내세운 수치는 검사기 수를 같게 맞췄을 때 TypeScript 7.0보다 검사 속도가 최대 8배 빠르고 메모리를 약 70% 덜 쓴다는 것이다. 이는 초기 업체 벤치마크이며, 결과는 작업 부하에 따라 달라진다. 아래의 대규모 프로젝트 성능 수치도 프로젝트 측 측정치다.

tsrs가 바꾸는 것과 바꾸지 않는 것

TypeScript를 사용할 때는 언어, 컴파일러, 런타임을 구분할 필요가 있다. 언어는 타입 주석과 제네릭, 타입 추론, 허용되는 대입 등 프로그램의 의미를 정한다. 컴파일러는 파일과 가져오기를 읽고 타입을 검사하며 JavaScript를 만들 수 있다. 런타임은 JavaScript를 실행한다. 웹에서는 브라우저의 JavaScript 엔진이고, 서버에서는 Node.js나 Bun일 수 있다.

function greet(name: string) {
 return `Hello, ${name}`
}

console.log(greet('Flavio'))

컴파일러는 name: string을 이용해 greet 호출을 검사한다. JavaScript 런타임에 전달되는 함수에는 이 주석이 없다. 컴파일러를 구현한 언어가 바뀌어도 작성하는 언어는 바뀌지 않는다. tsrs는 Rust로 TypeScript 규칙을 구현하므로 계속 .ts 파일을 작성한다.

컴파일러가 더 빨리 끝난다고 애플리케이션의 JavaScript 실행 속도가 저절로 빨라지는 것도 아니다. 개발과 빌드 중 기다리는 시간이 줄어들 수 있지만, 애플리케이션 런타임 성능은 별도로 측정해야 한다.

도구 또는 단계역할결과
타입 검사기타입이 맞는지 검사오류 또는 검사 성공
TypeScript 출력TypeScript를 JavaScript로 변환JavaScript, 선택적 선언 파일과 소스 맵
번들러애플리케이션 모듈을 찾아 묶음배포할 파일
JavaScript 런타임프로그램 실행애플리케이션 동작

일부 도구는 이 가운데 여러 작업을 맡는다. 주석을 빨리 제거해도 타입 검사는 다른 명령에 맡길 수 있으므로, 도구를 비교할 때 각 작업을 구분해야 한다.

프로젝트의 기반과 호환성

마이크로소프트는 TypeScript 컴파일러를 TypeScript에서 Go로 옮겼다. tsrs는 이 네이티브 구현에서 출발한다. 호환성 문서는 tsrs가 Go 구현을 기능별로 포팅하며, 기준으로 상위 TypeScript의 동작을 삼는다고 설명한다.

언어 규칙을 정하는 일은 계속 TypeScript 유지관리자에게 달려 있다. tsrs 팀은 그 결정을 따라 Rust로 동작을 재현해야 한다. 예를 들어 의존성이 복잡한 조건부 타입을 쓴다면, 한 컴파일러는 받아들이고 다른 컴파일러는 거부하는 차이가 애플리케이션 코드 변경 없이도 호환성 문제를 일으킬 수 있다.

오류 메시지와 위치도 중요하다. 편집기는 위치를 이용해 코드에 밑줄을 긋고, 빌드 도구는 종료 코드를 확인하며, 테스트는 진단이나 생성된 선언 파일을 비교할 수 있다. 프로젝트는 진단 내용과 메시지 문구, 종료 코드의 차이도 버그로 취급한다. 차이가 독립적으로는 더 나아 보이는 경우에도 마찬가지다.

저장소는 상위 프로젝트의 개발을 따라가며 TypeScript 7.1을 기준으로 완전한 적합성을 갖춘 안정 버전을 목표로 한다. 현재 패키지는 실험 단계다. 한 프로젝트에서 검사에 성공했다는 사실만으로 기존 검사를 대체하기에 충분한지 판단할 수는 없다.

컴파일러는 설정을 읽어 include, exclude, files 등을 바탕으로 시작 파일을 정한다. 가져온 파일과 설치된 패키지의 선언 파일도 프로그램에 포함된다. 이어 소스를 구문 구조로 분석하고, 이름을 선언에 연결한 뒤 타입을 검사한다. 예를 들어 greet(42)에서는 문자열을 요구하는 매개변수에 숫자가 전달된다는 점을 찾아낸다.

출력이 켜져 있으면 컴파일러는 소스를 변환해 JavaScript, 내보낸 타입을 설명하는 선언 파일, 생성 코드와 원본을 연결하는 소스 맵 등을 만들 수 있다. --noEmit은 JavaScript나 선언 파일을 쓰지 않고 검사만 하도록 한다. Vite나 프레임워크가 애플리케이션 빌드를 맡는 프로젝트에 쓸 수 있다. 다만 증분 검사가 켜져 있으면 --noEmit도 .tsbuildinfo 캐시를 기록할 수 있다. 옵션의 상호작용은 출력 문서에 설명돼 있다.

Rust 구현과 성능 수치를 읽는 법

Rust를 택했다고 속도 향상이 보장되는 것은 아니다. tsrs 저장소는 Go 문법을 Rust 문법으로 옮기는 데 그치지 않고 메모리 배치, 검사기 캐시, 스레드 간 검사 작업 분할 등을 최적화했다고 설명한다.

대형 프로젝트에는 구문 노드, 선언을 나타내는 심볼, 표현식과 제네릭을 해석하며 생기는 타입 객체가 많다. 이 객체들이 서로 참조하므로 참조 공간도 커진다. 프로젝트의 포인터 압축 기록은 지원 플랫폼에서 아레나에 저장된 객체를 작은 핸들로 가리키는 방식을 설명한다. 아레나는 관련 객체를 모아 할당하는 메모리 영역이다. 핸들을 주소로 되돌리는 작업이 필요하므로 메모리 절감과 접근 비용 사이의 균형을 측정해야 하며, 기록에는 그 측정과 절충점이 담겼다.

말단 파일 메모리 기록은 다른 파일에서 가져오지 않는 테스트 파일을 검사한 뒤, 조건이 맞으면 구문 데이터를 해제하는 최적화를 다룬다. 선언 병합이나 가져오기, 공유 컴파일러 상태 때문에 데이터가 계속 필요할 수 있으므로 모든 파일의 검사가 끝나자마자 데이터를 해제할 수 있는 것은 아니다.

릴리스 빌드는 프로파일 기반 최적화도 사용한다. 대표 작업을 실행해 어떤 경로가 실행되는지 기록하고, 그 프로파일을 사용해 컴파일러 바이너리를 만든다. 따라서 소스에서 만든 개발 빌드와 공개 벤치마크 바이너리의 동작은 다를 수 있다. 현재 구현 선택은 Rust 엔지니어링 안내서에서 볼 수 있다. Rust라는 언어만으로 성능을 추정하기보다 프로젝트 측 벤치마크를 확인해야 한다.

컴파일러 벤치마크는 프로젝트, 컴파일러 리비전, 검사기 스레드 수에 따라 달라진다. Cloudflare는 발표에서 비교 설정을 맞추기 위해 TypeScript 팀과 협력했다고 밝혔다. 새 TypeScript 베타에는 tsrs에서 상위 프로젝트에 기여한 내용을 포함해 개선 사항이 반영돼 있다고도 설명했다.

저장소에는 별도의 과거 벤치마크 보고서가 있다. 보고서의 세 작업 부하는 TypeScript 7.0과 이후 TypeScript 개발을 따라가는 tsrs 빌드를 비교한다. 따라서 발표의 대표 수치와는 별도 측정이다. 보고서는 Linux, 16 vCPU, RAM 63GB 환경에서 실행했다. 시간은 tsrs의 교차 배치 10회와 Go 컴파일러 3회 측정값의 중앙값이다. 네이티브 CLI 측정이며 브라우저 빌드는 측정하지 않았다.

검사기 수를 같게 한 비교는 두 컴파일러가 같은 수의 검사 작업자를 쓸 때를 보여준다. 기본 설정 비교는 별도 조정 없이 사용자가 얻을 수 있는 결과를 보여주지만, 한쪽이 더 많은 코어를 사용한다면 자원 사용량 차이도 결과에 포함된다. tsrs에 검사기를 더 주면 시간이 줄 수 있지만, 검사기 4개를 쓸 때보다 최대 메모리가 늘 수도 있다.

벤치마크를 인용하기 전에 다음 조건을 확인해야 한다.

  • 검사기 스레드 수: 기본값이 서로 다를 수 있다.
  • TypeScript 리비전: 언어 변경으로 같은 코드에서 오류가 달라질 수 있다.
  • 증분 캐시: 캐시가 따뜻한 검사와 새로 시작하는 검사는 작업량이 다르다.
  • 파일과 설정: 파일을 제외하면 컴파일러가 더 빨라 보일 수 있다.
  • 생성 의존성: 선언 파일이 없으면 실제 작업 부하를 검사하기 전에 중단될 수 있다.
  • 진단: 빠른 실행도 의도한 코드를 올바르게 검사해야 한다.
  • 빌드 설정: 최적화된 릴리스 바이너리와 개발 빌드는 다를 수 있다.

과거 보고서에서 세 작업 부하의 진단 차이는 상위 프로젝트 변경에 따른 것으로 설명됐고, 해당 TypeScript 개발 빌드와 대조됐다. 별도로 Drizzle ORM을 검사기 16개로 실행한 tsrs 버그도 기록됐다. 오류 개수는 같았지만 오류 위치가 달랐다. 패키지 이름만 맞춘다고 적합성 비교가 되는 것은 아니다. 미래 버전 TypeScript를 포팅한 컴파일러가 현재 안정 버전과 다르게 동작할 수도 있다. 오류 개수가 같다는 사실도 초기 확인에 불과하다. 실제 차이를 조사할 때는 진단의 위치와 의미를 비교해야 한다.

가령 검사 시간이 20초에서 5초로 줄면 4배 빨라지고 검사마다 15초를 아낀다. 다른 프로젝트에서 0.20초가 0.05초로 줄어도 배수는 같지만 하루 업무에 미치는 영향은 훨씬 작다. 실제 프로젝트에서는 반복 실행하는 명령의 시간을 기록하고, CI 메모리 문제가 있다면 프로세스 최대 메모리를 측정하는 편이 유용할 수 있다.

네이티브 CLI 설치와 사용

작은 프로젝트에 일부러 타입 오류를 넣어 먼저 검사할 수 있다. 지원 시스템용 네이티브 바이너리를 npm 패키지가 선택하므로, 공개 바이너리를 쓸 때 Rust를 설치할 필요는 없다. 다른 컴퓨터에서 설정하기 전에 플랫폼 지원 표를 확인한다.

mkdir tsrs-demo
cd tsrs-demo
npm init -y
npm install --save-dev tsrs

tsconfig.json으로 설정을 저장한다.

{
 "compilerOptions": {
 "strict": true,
 "target": "ESNext",
 "module": "ESNext"
 },
 "include": ["app.ts"]
}

다음 코드를 app.ts에 저장한다.

function greet(name: string) {
 return `Hello, ${name}`
}

console.log(greet(42))

검사를 실행한다.

npx tsrs -p . --noEmit

숫자 인수를 문자열 매개변수에 대입할 수 없다는 오류가 나오며, 검사가 실패하면 종료 코드도 실패를 나타낸다. 따라서 CI 작업에서 오류가 발견되면 작업을 실패시킬 수 있다. 마지막 줄을 console.log(greet('Flavio'))로 바꾸고 다시 실행하면 오류가 사라진다. -p .는 현재 디렉터리의 프로젝트를 선택하고 --noEmit은 검사에만 집중하게 한다. 설정 옵션은 tsconfig.json 안내에서 볼 수 있다.

JavaScript와 선언 파일을 만들려면 다음 내용을 app.ts에 저장한다.

export function greet(name: string) {
 return `Hello, ${name}`
}

console.log(greet('Flavio'))

선언 파일을 켜고 출력을 별도 디렉터리에 둔다.

npx tsrs -p . --outDir dist --declaration --noEmitOnError

결과는 타입 주석이 제거된 dist/app.js와 내보낸 함수의 공개 타입을 설명하는 dist/app.d.ts다. 선언 파일에는 함수 본문이 들어가지 않는다.

export declare function greet(name: string): string

--noEmitOnError는 오류가 있으면 결과 파일을 만들지 않도록 한다. 이 옵션이 없으면 오류 보고와 JavaScript 출력 여부는 별개의 결정이다.

반복 검사에는 네이티브 CLI의 감시 모드를 쓸 수 있다.

npx tsrs -p . --noEmit --watch

터미널을 열어 둔 채 인수를 바꾸고 파일을 저장하면 다시 검사한다. Ctrl-C로 멈춘다. 별도 실행 사이에 빌드 정보를 재사용하려면 증분 검사를 켠다.

npx tsrs -p . --noEmit --incremental

컴파일러는 이후 실행을 위한 빌드 정보를 기록한다. 벤치마크에서는 캐시 조건을 같게 해야 한다. 증분 실행과 새로 전체 검사하는 실행은 서로 다른 질문에 답한다. 네이티브 감시 지원과 플랫폼 조건은 사용 안내에 있으며 WebAssembly에는 다른 한계가 있다.

기존 프로젝트에는 별도 명령으로 먼저 추가해 기존 검사를 유지한 채 비교하는 편이 안전하다.

{
 "typecheck": "tsc -p . --noEmit",
 "typecheck:tsrs": "tsrs -p . --noEmit"
}

같은 의존성 설치와 코드 생성 절차를 거친 뒤 두 명령을 실행한다. 기존 검사 전에 데이터베이스 타입을 생성한다면 tsrs 검사에도 해당 파일이 필요하다. 비어 있거나 불완전한 입력을 검사한 성공은 의미가 적다. 특히 모노레포에서는 설정에 포함된 파일을 살펴야 한다.

프로젝트 참조 빌드는 -b를 사용하며, 참조 프로젝트 간 JavaScript와 선언 파일 출력도 지원한다. 빌드가 이 기능에 의존한다면 함께 시험해야 한다. --noEmit 검사 성공만으로 npm에 배포할 선언 파일까지 검증했다고 볼 수는 없다.

Astro, Vue 단일 파일 컴포넌트, Svelte 컴포넌트에는 일반 TypeScript 소스 외의 내용도 있다. 콘텐츠 매퍼가 관련 부분을 컴파일러에 전달할 수 있다. 현재 호환성 표에는 CLI의 콘텐츠 매퍼 지원이 기재돼 있지만 언어 서버 지원은 아직 불완전하다. 같은 범위를 확인하기 전까지 프레임워크 자체 검사 명령을 유지해야 한다. 일반 .ts 검사만 통과하고 컴포넌트 템플릿 오류를 놓칠 수 있다.

편집기 통합은 별도 문제다. 프로젝트에 tsrs를 설치해도 편집기가 사용하는 검사기가 자동으로 바뀌지는 않는다. 네이티브 패키지는 tsrs --lsp -stdio로 실행하는 언어 서버를 포함한다. 편집기는 알맞은 통합 설정을 통해 서버를 실행해야 한다. 편집기 설정 안내는 VS Code의 TypeScript native 확장과 Neovim 설정을 설명한다. 기존 TypeScript 서버 설정에 Rust 실행 파일을 지정하지 말고 해당 안내를 따라야 한다.

편집기 지원에는 별도의 호환성 시험과 미지원 항목이 있다. 팀 설정을 바꾸기 전에 진단과 이동 기능을 실제 프로젝트에서 비교해야 한다. 자동 타입 취득과 콘텐츠 매퍼 지원도 현재 표에서 확인할 항목이다.

프로젝트에서 직접 측정하기

공식 컴파일러가 아직 없다면 tsrs와 함께 설치한다.

npm install --save-dev typescript

재현 가능한 타입 검사 명령부터 정한다. macOS나 Linux에서는 셸의 time 명령을 쓸 수 있다.

time npx tsc -p . --noEmit --incremental false --pretty false --checkers 4
time npx tsrs -p . --noEmit --incremental false --pretty false --checkers 4

이 명령은 공식 컴파일러가 --checkers를 지원하는 네이티브 Go 기반 TypeScript 릴리스라고 가정한다. JavaScript로 작성된 구형 TypeScript 컴파일러는 이 옵션을 받지 않는다.

각 명령을 여러 번 실행하고 순서를 번갈아 바꾼다. 유난히 무거운 백그라운드 작업은 닫는다. 측정 시간에는 npm 실행기 시작 시간도 들어가므로 작은 프로젝트는 시작 비용이 대부분일 수 있다. npx tsc --version과 npx tsrs --version으로 버전을 기록한다. 공식 안정 버전과 비교하면 도입 판단에 도움이 된다. 정확성 차이를 조사할 때는 tsrs가 대상으로 삼는 정확한 상위 리비전과 비교한다.

macOS에서 최대 메모리를 확인할 때 /usr/bin/time -l은 최대 상주 메모리 크기를 보고한다. GNU time을 쓰는 Linux에서는 /usr/bin/time -v를 이용할 수 있다. 출력 항목과 단위가 다르므로 측정 결과와 함께 보관한다.

성능을 측정한 뒤에야 다음 옵션을 조정한다.

옵션제어하는 항목절충점
--checkers 4타입 검사 작업자 수작업자가 늘면 메모리를 더 쓸 수 있음
--singleThreaded단일 스레드 실행병렬성이 낮아져 더 오래 걸릴 수 있음
--maxMemory 2G--noEmit 검사 메모리 목표검사기 상태를 재활용하며 작업을 반복할 수 있음
--checkerCostCache .tsrs-costs파일 검사 비용 기억다음 작업 배정을 위한 캐시 파일 추가

메모리 목표는 프로세스를 그 수준에 가깝게 유지하려는 요청이다. 운영체제 메모리 제한을 대신하거나 어떤 할당도 목표를 넘지 않는다고 보장하지 않는다. 검사 순서와 관련된 출력 차이가 의심되면 기본값 문서에서 --checkerAssignment go와 --noLazyMembers를 확인할 수 있다. 이 옵션들은 특히 알려진 이력이나 인스턴스화 깊이 사례에서 고정된 Go 구현과 동작을 비교하는 데 도움이 된다. 첫 시험에서는 그대로 두고 구체적인 차이를 좁힐 때 사용하는 편이 낫다.

WebAssembly로 브라우저에서 실행하기

네이티브 컴파일러는 특정 운영체제와 프로세서용으로 빌드된다. WebAssembly 모듈은 필요한 인터페이스를 구현한 호스트 안에서 실행된다. @tsrs/wasm은 공유 Rust 컴파일러 코드를 WebAssembly용으로 빌드하며 Node.js와 브라우저에서 쓸 JavaScript API를 제공한다.

브라우저 API는 메모리 내 파일 시스템을 받는다. 설정과 의존성 선언을 포함해 파일 이름과 내용을 전달한다. 표준 TypeScript 라이브러리 선언은 패키지에 포함된다. 따라서 서버에서 컴파일러 서비스를 실행하지 않고도 온라인 편집기를 만들 수 있다. 사용자가 예제를 고치고 버튼을 누르면 자기 브라우저에서 오류를 받을 수 있다.

브라우저 API는 WebAssembly 모듈을 불러올 때 비동기식이지만, 컴파일은 해당 실행 맥락에서 동기식으로 진행된다. 페이지에서 async 함수를 호출해도 무거운 작업이 자동으로 UI 스레드에서 벗어나지는 않는다. 입력을 계속 처리하려면 Web Worker를 사용해야 하며, 예제에서는 직접 워커를 만든다.

브라우저 워커는 컴퓨터의 프로젝트 디렉터리를 받지 않는다. 요청에 넣은 가상 파일만 볼 수 있다. 파일 하나만 검사할 때는 간단하지만, 라이브러리를 가져오는 앱이라면 관련 선언과 경로도 전달해야 한다. 패키지 없음 오류는 가상 파일 시스템이 불완전하다는 뜻일 수 있다. @types/node도 선언만 제공한다. 브라우저 컴파일러에 넣으면 Node API를 이해할 수 있게 할 뿐, 그 API가 브라우저에서 실행되게 하지는 않는다.

패키지는 컴파일된 WebAssembly 모듈을 캐시하지만 API를 호출할 때마다 새 인스턴스를 만든다. 키 입력마다 살아 있는 TypeScript 프로그램을 계속 유지하지는 않는다. 증분 빌드는 .tsbuildinfo 같은 파일을 재사용할 수 있지만, 메모리 모드에서는 다음 요청에 이전 요청에서 기록한 파일을 다시 전달해야 한다. 워커를 계속 띄워 두는 것만으로는 이 재사용이 되지 않는다.

현재 WebAssembly 빌드는 단일 스레드로 실행된다. --watch, 언어 서버, 네이티브 API 서버 모드는 지원하지 않는다. 각 인스턴스의 선형 메모리는 최대 4GiB이고 호스트가 더 낮게 제한할 수 있다. 이는 WebAssembly 주소 공간 한도이지 모든 브라우저가 4GiB를 무리 없이 할당한다는 뜻이 아니다. 깊은 재귀 타입은 실행 스택을 소진할 수도 있다. API의 memoryBytes는 모듈 그림자 스택을 포함한 최대 선형 메모리이며 브라우저 탭 전체의 메모리가 아니다. 이를 네이티브 프로세스 RSS와 직접 비교하면 서로 다른 측정치를 섞게 된다.

Cloudflare는 Workers도 Rust와 WebAssembly를 추진하는 이유로 들었다. 하지만 그 환경에서 도구 체인을 쓰려면 적절한 파일 시스템과 런타임 통합이 필요하다. 아래 브라우저 데모는 브라우저 지원을 보여줄 뿐 Workers 배포를 시험하지 않는다.

브라우저 타입 검사기 만들기

텍스트 입력란, 검사 버튼, 결과 영역을 만들고 전용 워커에서 컴파일러를 실행한다. 입력된 프로그램을 실행하지 않고 타입 검사만 한다. 최신 Node.js를 사용해 Vite와 WebAssembly 패키지를 설치한다.

mkdir tsrs-browser-demo
cd tsrs-browser-demo
npm init -y
npm install --save-dev vite
npm install @tsrs/wasm

Vite는 워커를 묶고 WebAssembly 자산을 정해진 URL에 게시하는 데 쓴다. 파일 세 개를 만든다.

index.html:

<!doctype html>
<html lang="en">
 <head>
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1">
 <title>TypeScript checker</title>
 </head>
 <body>
 <h1>Check TypeScript in your browser</h1>
 <label for="source">TypeScript source</label>
 <textarea id="source" rows="8" cols="50">const greeting: string = 42</textarea>
 <button id="check" type="button">Check</button>
 <pre id="result" role="status">Ready</pre>
 <script type="module" src="/main.js"></script>
 </body>
</html>

초기 프로그램은 숫자를 문자열에 대입하므로 검사기가 오류를 보고해야 한다. main.js에는 다음 코드를 저장한다.

const source = document.querySelector('#source')
const button = document.querySelector('#check')
const output = document.querySelector('#result')

const worker = new Worker(new URL('./checker.js', import.meta.url), {
 type: 'module'
})

button.addEventListener('click', () => {
 button.disabled = true
 output.textContent = 'Checking...'
 worker.postMessage(source.value)
})

worker.addEventListener('message', ({ data }) => {
 button.disabled = false
 output.textContent = data
})

worker.addEventListener('error', () => {
 button.disabled = false
 output.textContent = 'The compiler worker failed. Reload the page to retry.'
})

페이지는 현재 입력란 내용을 워커로 보낸다. 작은 데모에서 버튼을 비활성화해 요청이 겹치지 않게 한다. 타이핑 중 자동 검사하는 편집기라면 오래된 결과를 처리하는 기능도 필요하다. 이 예제는 버튼을 눌렀을 때만 검사한다.

checker.js에는 컴파일러 워커 코드를 저장한다.

import { tsc } from '@tsrs/wasm'
import wasmUrl from '@tsrs/wasm/tsrs.wasm?url'

self.addEventListener('message', async ({ data: source }) => {
 try {
 const result = await tsc(['-p', '/project'], {
 url: wasmUrl,
 cwd: '/project',
 diagnostics: 'json',
 files: {
 '/project/tsconfig.json': JSON.stringify({
 compilerOptions: {
 strict: true,
 noEmit: true,
 target: 'ESNext',
 module: 'ESNext',
 moduleDetection: 'force'
 },
 files: ['app.ts']
 }),
 '/project/app.ts': source
 }
 })

 if (result.exitCode === 0) {
 self.postMessage('No type errors')
 return
 }

 const errors = (result.diagnostics ?? []).map((diagnostic) => {
 const line = diagnostic.startPosition
 ? diagnostic.startPosition.line + 1
 : '?'
 return `Line ${line}: TS${diagnostic.code}: ${diagnostic.text}`
 })

 self.postMessage(
 errors.join('\n') || result.stderr || result.stdout || 'Checking failed'
 )
 } catch (error) {
 self.postMessage(`Could not check the code: ${error.message}`)
 }
})

?url 가져오기는 Vite의 WebAssembly 자산 URL을 받는다. URL을 명시적으로 넘기면 자산이 묶인 JavaScript 파일 옆에 계속 있을 것이라고 가정하지 않아도 된다. 가상 tsconfig.json은 엄격한 검사를 켜고 출력은 막는다. moduleDetection: 'force'는 파일을 모듈로 처리해 최상위 선언이 전역 범위에 들어가지 않게 한다. 사용자가 입력란에서 name 같은 이름을 쓸 때 브라우저 전역 이름과 충돌하는 일을 막는다. 전역 스크립트 동작을 살펴보려면 이 옵션을 제거한다.

워커는 진단을 읽기 쉬운 문자열로 바꿔 페이지에 보낸다. 컴파일 실패나 WebAssembly 로딩 실패도 메시지로 표시하므로 페이지가 계속 “Checking…” 상태에 머물지 않는다.

Vite를 실행한다.

npx vite

출력된 로컬 URL을 열고 Check를 누르면 숫자를 문자열에 대입할 수 없다는 TS2322 오류가 나와야 한다. 입력란을 다음처럼 바꾸고 다시 확인한다.

const greeting: string = 'Hello'

결과는 No type errors여야 한다. 입력란을 비워도 유효한 빈 모듈이므로 검사를 통과해야 한다. 이는 컴파일러 로딩 실패와 다르며, 로딩 실패는 오류 메시지로 표시돼야 한다.

배포용 번들을 검사하려면 다음을 실행한다.

npx vite build
npx vite preview

미리보기 URL을 열어 검사를 반복한다. 개발 서버에서 성공했다고 해서 배포된 워커와 .wasm URL도 올바르다는 보장은 없다.

이 데모는 페이지와 워커 사이에서 소스 코드를 전달하며 타입 검사 서버는 사용하지 않는다. 브라우저는 여전히 애플리케이션 JavaScript와 WebAssembly 자산을 가져온다. 원격 패키지 로딩이나 분석 기능, 백엔드를 추가한다면 사용자 코드가 모두 로컬에 남는다고 약속하기 전에 네트워크 요청을 살펴야 한다.

도입 전 확인할 호환성

프로젝트는 상세한 호환성 표를 공개한다. 실험 기간에는 지원 범위가 바뀔 수 있으므로 시험 시점의 표를 확인해야 한다.

영역현재 경계확인할 내용
일반 타입 검사알려진 적합성 예외가 있는 상태로 지원대응하는 상위 컴파일러와 같은 파일 및 진단인지
JavaScript와 선언 파일지원배포 출력과 선언 파일 소비자
프로젝트 참조지원전체 빌드 그래프
--init, --showConfig미지원이 명령을 호출하는 스크립트
언어 서버네이티브로 제공되며 문서화된 공백이 있음이동 기능, 진단, 편집기 통합
프레임워크 콘텐츠 매퍼CLI 지원, 언어 서버 지원은 불완전.ts 파일과 함께 컴포넌트 및 템플릿 검사
WebAssembly단일 스레드이며 별도 한계가 있음워커 동작, 자산 URL, 실제 프로젝트 규모

문서는 지원되는 플랫폼 패키지와 소스 빌드도 구분한다. CI 실행 환경을 바꾸거나 팀원에게 설치를 요청하기 전에 대상 시스템에서 실제로 쓸 수 있는 패키지를 확인해야 한다. 상위 프로젝트의 대규모 테스트 모음을 통과했다는 점은 유용한 근거지만, 모든 프로젝트와 편집기 상호작용, 런타임 설정을 이미 시험했다는 뜻은 아니다.

단계적으로 시험하기

타입 검사가 느린 애플리케이션이라면 tsrs를 별도 CI 작업으로 추가해 기존 검사와 나란히 실행하는 방식부터 고려할 수 있다. 실제 변경을 대상으로 두 검사를 돌리고 차이가 있으면 조사한다. 각 컴파일러가 따르는 TypeScript 리비전을 기록하고 의존성 설치와 생성 파일, 프로젝트 선택을 같게 유지한다. 오류가 다르면 어느 결과가 맞는지 결정하기 전에 작은 재현 사례로 줄인다.

결과가 일치하면 절약한 시간과 메모리를 측정한다. 이미 빨랐던 작업에서 큰 속도 배수를 얻는 것보다, 더 작은 CI 머신을 사용할 수 있게 되는 편이 중요할 수 있다. 라이브러리라면 생성된 선언 파일을 비교하고 소비자 프로젝트에서 시험한다. 프레임워크 앱이라면 같은 검사 범위를 확인할 때까지 프레임워크 검사도 유지한다.

교육용으로는 브라우저 데모부터 시작할 수 있다. 독자가 유니온 타입이나 제네릭 함수를 바꾸고 강의 옆에서 컴파일러 반응을 볼 수 있다. 일반적인 휴대전화와 노트북에서 실행할 수 있도록 예제 프로젝트 규모를 작게 유지해야 한다.

처음부터 컴파일러, 편집기 통합, 빌드 출력, CI 설정을 한꺼번에 바꾸는 일은 피하는 편이 낫다. 변경을 분리하면 차이를 찾기 쉽고 되돌리기도 단순하다. 기존 검사가 빠르고 안정적이라면 안정 버전을 기다리는 것도 합리적이다. 실험용 패키지는 현재 작업 흐름을 유지하면서 프로젝트에 중요한 부분을 시험할 방법을 제공한다.