Project Corsa - Typescript 7.0

    Project Corsa - Typescript 7.0
    Jane Developer
    Woosol Kim
    Full-stack Developer
    Goyang, Ilsan
    3 min read
    2025. 03. 17

    페이지를 벗어나면 미니플레이어로 전환됩니다

    https://devblogs.microsoft.com/typescript/typescript-native-port/

    안녕하세요, 저는 Anders Hejlsberg입니다. TypeScript 프로젝트의 리드 아키텍트이자 Microsoft의 테크니컬 펠로우입니다. 오늘은 “프로젝트 Corsa(Project Corsa)”에 대해 이야기하려고 합니다. 이는 TypeScript 컴파일러와 툴셋을 네이티브 코드로 포팅하려는 우리의 노력입니다.

    10여 년 전 TypeScript가 처음 생긴 이후로, TypeScript는 자체 언어(즉, TypeScript)로 작성되어 왔습니다. 이는 많은 이점을 가져다주었지만, 동시에 성능과 인력(스킬) 확보 측면에서 여러 도전을 안겨주었습니다. JavaScript 런타임 플랫폼은 주로 UI와 브라우저 사용 사례에 최적화되어 있으며, 컴파일러나 시스템 레벨 툴 같은 연산 집약적인 작업에는 그리 적합하지 않습니다.

    우리 고객들이 가장 자주 겪는 문제 중 하나는 프로젝트 규모가 커짐에 따라 메모리가 부족(out-of-memory)해지는 상황입니다. 사실상 JavaScript에서 얻을 수 있는 성능을 거의 한계까지 끌어올렸다고 생각합니다. JavaScript 자체가 지닌 오버헤드가 문제를 일으키기도 합니다. 예를 들어, JIT(Just-In-Time) 컴파일 언어이므로 시작 시 코드를 JIT 컴파일하는 데 비용이 들고, 프로퍼티를 자유롭게 확장할 수 있는 객체 모델(expando properties) 때문에 인라인 메모리 할당이 어려우며, 공유 메모리를 이용한 병렬성을 제공하지도 않습니다. 결국 이런 점들은 우리가 “테이블 위에 남겨두는 성능”이 많다는 뜻이죠.

    이를 좀 더 구체적으로 보여드리기 위해, 지금부터 현재의 TypeScript 컴파일러를 사용해서 Visual Studio Code 프로젝트(약 150만 줄 규모)를 풀 컴파일(full compile) 해보겠습니다. 이 작업을 실행해두고, 결과가 나오는 동안 이야기를 계속하죠.

    약 6개월 전부터 TypeScript를 네이티브 코드로 옮기는 작업을 본격적으로 시작했습니다. 저희가 생각했던 중요한 점은 “완전히 새로 쓰는(rewrite) 대신, 기존 컴파일러를 ‘포팅(port)’해야 한다”는 것이었습니다. 즉, 기존 컴파일러의 모든 기능과 동작 방식을 그대로 옮겨와야 했습니다. 현재 포팅 작업은 이미 상당히 진행된 상태이며, 10만 줄 이상을 옮겼고, 스캐너(scanner), 파서(parser), 바인더(binder)는 거의 완료되었습니다. 타입 체커(type checker)는 약 80% 정도 완성되었고, 이제 언어 서비스(language service) 쪽 작업도 진행 중입니다.

    현재는 Go 언어를 대상으로 포팅 중이며, “왜 C나 C++ 또는 러스트(Rust)가 아니고 Go냐?”라는 질문을 많이 받습니다. 사실 여러 언어로 프로토타이핑을 해보았는데, 우리가 하려는 작업 형태에 가장 부합하는 것이 Go라는 결론을 내렸습니다. Go는 네이티브 코드로 컴파일되어 모든 플랫폼에서 최적화된 성능을 얻을 수 있고, 데이터 배치(data layout)를 세밀하게 제어할 수 있으며, 순환 구조(cyclic data structures) 같은 것도 지원하기가 수월했습니다. 게다가 가비지 컬렉션(자동 메모리 관리)과 훌륭한 동시성(concurrency) 지원이 있어, 우리가 원하는 목표에 부합한다고 판단했습니다.

    이제 앞서 Visual Studio Code 프로젝트를 기존 컴파일러로 빌드하는 데 얼마나 걸리는지 보셨을 텐데, 대략 1분 정도가 걸립니다. 이번에는 새로 만든 네이티브 컴파일러로 빌드했을 때 얼마나 걸리는지 보여드리겠습니다. 보시는 것처럼 약 5초, 5.5초 정도 걸립니다. 무려 10배 이상의 속도 향상이죠. 이는 프로젝트 로드가 10배 이상 빨라지고, 배치 컴파일(batch compile)도 10배 이상 빨라진다는 뜻입니다.

    조금 더 자세한 예시를 보여드리겠습니다. 말 그대로 “포팅”한다는 것은 파일별, 함수별로 기존 JavaScript/TypeScript 코드를 그대로 옮긴다는 것입니다. 예를 들어, 타입 체커에 있는 doGetTypeOfSymbol 함수를 살펴봅시다. 새 Go 코드에서 이 함수를 찾고, 이전 TypeScript 코드와 비교해보면, 순서와 로직은 같고 문법만 Go에 맞게 바뀌었습니다. 즉, 코드는 달라졌지만 의미와 동작 원리는 동일합니다.

    덕분에 타입 추론 후보가 여러 개일 때 어떤 타입을 우선순위로 할당하는지 등, TypeScript 내부에서 오랫동안 누적된 미묘한 동작들이 그대로 유지됩니다. 따라서 새 컴파일러에서도 기존과 동일한 결과를 기대할 수 있습니다. 예를 들어, 제가 지금 구버전(기존) TypeScript 컴파일러 소스에 오류를 하나 집어넣어 보겠습니다. 그다음 구버전 컴파일러로 자신을 빌드하도록 하면, 그 오류를 제대로 잡아냅니다. 새 컴파일러로도 같은 작업을 해보면, 동일한 오류 메시지가 아주 빠르게 나옵니다. 이것이 우리가 지향하는 바입니다. “기존 컴파일러와 동일한 결과, 그저 훨씬 빠르게.”

    물론 커맨드라인 컴파일러만 만드는 게 아니라, **언어 서비스(language service)**도 만들고 있습니다. 어떤 의미에서는 이 언어 서비스가 훨씬 중요하죠. 이제 Go로 만든 언어 서비스를 Visual Studio 환경에서 띄워보겠습니다. 실행해보면, 우리가 기존에 누리던 타입 정보 호버(hover), “정의로 이동(go to definition)”, 오류 표시(red squigglies) 같은 기능도 그대로 잘 작동합니다. 프로젝트는 같은 Visual Studio Code 소스이며, 총 4,500개 파일에 150만 줄 코드가 들어있습니다. 지금 언어 서비스를 재시작해보면, 프로세스가 완전히 죽었다가 다시 시작되고, 모든 소스 파일 파싱 및 에러 체크가 2~3초 만에 끝납니다. 기존 언어 서비스로는 몇 배 더 걸리던 작업이 훨씬 빨라졌죠.

    새 언어 서비스에서는 언어 서버 프로토콜(LSP)을 사용합니다. TypeScript가 처음 생겼을 때는 아직 LSP라는 개념이 존재하지 않았고, 이제 많은 언어들이 표준처럼 쓰고 있습니다. 따라서 그에 맞춰 재설계하는 중이며, 결과적으로 기존 언어 서비스와 1대1로 모든 기능을 그대로 맞추기보다, 현재 시대에는 AI 기반 기능이 더 적절한 경우(예: 리팩터링 등)도 있으므로 어떤 부분을 LSP로 옮기고 어떤 부분을 AI로 보강할지 고민하고 있습니다.

    이제 “어떻게 10배나 되는 성능 향상을 얻었느냐?”라는 질문에 답해보겠습니다. 사실 절반 정도는 ‘네이티브 코드’로 옮겼기 때문이고, 나머지 절반 정도는 ‘병렬 처리(concurrency)를 활용’해서입니다.

    예를 들어, 이전 구버전 컴파일러가 자체 소스(약 25만 줄)를 빌드하는 데 약 7초가 걸립니다. 새 컴파일러를 단일 스레드 모드로 강제해서 돌려보면 2초가 걸리죠. 즉, 네이티브 코드만으로도 약 3~3.5배 속도가 빨라집니다. 그런데 기본 설정(멀티스레드)으로 돌리면 1초 미만에 끝납니다. 이처럼 코어를 4개, 8개 활용하면 기존 대비 8배 이상의 속도도 가능해집니다. (물론 큰 단일 파일 등은 병렬화가 어려운 부분도 있으니, 상황에 따라 다릅니다. Visual Studio Code 같은 프로젝트에서는 10배 정도 향상을 보고 있습니다.)

    내부 구조를 조금 더 말씀드리면, 이전 컴파일러도 상당히 “함수형” 방식으로 작성되어 있었습니다. 예를 들어, 파싱(parsing) 후 생성된 불변(immutable) AST를 여러 프로그램이 공유하도록 설계되어 있었지요. 새 컴파일러에서는 파싱, 바인딩(binding), 코드 생성(emit)을 완전 병렬화했습니다. 파일 단위로 아주 쉽게 나눌 수 있기 때문입니다. 코어가 8개면 8배 빨라지는, 소위 “병렬화하기 쉬운(embarrassingly parallelizable)” 작업입니다.

    다만, 타입 체크는 여러 파일 간 상호 참조가 있기 때문에 완전히 병렬화하기가 조금 까다롭습니다. 그래서 저희는 프로그램을 통째로 4개의 타입 체커에게 넘겨주고, 각자 파일 일부씩을 담당하도록 하는 방식을 씁니다. 물론 이러면 중복 계산이 조금은 생깁니다만, 실질적으로 23배 정도 속도 향상이 가능하고, 메모리는 2025% 정도만 더 쓰면 됩니다. 전체 메모리 사용량은 여전히 이전보다 적고요.

    이처럼 현재 저희는 새 GitHub 저장소를 공개했으며, 커맨드라인 컴파일러는 단일 프로젝트 빌드 기준으로 약 80% 정도 완성 단계입니다. 아직 JS/JSDoc, JSX, 프로젝트 참조(project references), 인크리멘탈 빌드 등은 지원되지 않지만, 올해 안으로 이 모든 기능을 갖춘 완벽한 대체제(커맨드라인 컴파일러)를 내놓는 것이 목표입니다. 또한 다양한 언어에서 이 컴파일러를 쉽게 호출할 수 있도록 새로운 프로세스 간 API(interprocess API)도 구상하고 있습니다.

    10배 성능 향상은 AI 기능 확장에도 큰 기회를 줍니다. 예를 들어, LLM(대형 언어 모델)에서 코드 제안을 받을 때 즉각적으로 타입 체크 결과를 확인한다거나, LLM 프롬프트에 더 풍부한 의미 정보를 넣어주어 고도화된 코드 어시스트가 가능해집니다.

    이제 새 GitHub 레포지토리를 방문하셔서 직접 빌드해 보시고, 프로젝트에 적용해본 뒤 피드백을 남겨주시면 감사하겠습니다. 문제가 발견되면 이슈로 보고해주십시오. 감사합니다.


    • 프로젝트 Corsa 소개: TypeScript 컴파일러를 네이티브 코드(Go 언어)로 포팅하는 작업.
    • 이유:
      • 성능과 메모리 문제(대형 프로젝트에서 자주 발생).
      • JavaScript의 JIT, 유연한 객체 모델 등으로 인해 컴파일러처럼 연산 집약적인 작업에서 한계.
    • Go를 선택한 이유:
      • 모든 플랫폼에서 최적화된 네이티브 코드 지원, 효율적인 데이터 레이아웃 제어.
      • 가비지 컬렉션, 동시성(멀티 스레드) 지원, 순환 구조 사용 등에 적합.
    • 성과:
      • 대략 10배 이상의 속도 향상 (Visual Studio Code 전체 컴파일이 약 1분 → 5초).
      • 기존 TypeScript 코드와 완전히 동일한 동작 보장(파일·함수 단위 포팅).
      • 언어 서비스도 새로 구현 중이며, LSP를 기반으로 해서 로드/분석 속도가 크게 개선.
    • 추가 기술 사항:
      • 파싱·바인딩·코드 생성(emit)은 “병렬화하기 쉬운” 구조로 설계.
      • 타입 체크는 여러 쓰레드로 분할하지만, 일부 중복 계산이 발생. 그래도 2~3배 속도 향상 가능.
    • 향후 계획:
    • 올해 안에 JS/JSX, 프로젝트 참조, 인크리멘탈 빌드 등 모든 기능을 갖춘 완전 대체 커맨드라인 컴파일러 출시 목표.
    • 새로운 프로세스 간 API, AI/LLM을 이용한 지능형 기능도 검토 중.
    woosol

    Written by Woosol Kim

    똑똑하게 일하는게 꿈