
무슨 일이 있었나요?
Cloudflare가 블로그에서 tsrs라는 프로젝트를 소개했어요. 이름 그대로 TypeScript를 Rust로 포팅한 프로젝트예요. 여기서 TypeScript를 옮겼다는 건 언어 자체를 바꿨다는 게 아니에요. 우리가 tsc로 실행하는 TypeScript 컴파일러를 Rust로 다시 구현했다는 뜻이에요.
이 소식이 흥미로운 건 타이밍 때문이에요. 2025년 3월 마이크로소프트 TypeScript 팀은 컴파일러를 Go로 포팅하는 Project Corsa를 발표했고, 이게 TypeScript 7로 이어지는 공식 노선이에요. 그때 '왜 Rust가 아니라 Go냐'는 질문이 정말 많았는데, Cloudflare가 그 질문에 코드로 다른 답을 내놓은 셈이에요. 구체적인 동기와 구현 범위, 성능 수치는 원문 블로그에 자세히 나와 있으니 함께 읽어보시길 추천해요. 여기서는 이 소식을 이해하는 데 필요한 맥락을 풀어볼게요.
TypeScript 컴파일러를 왜 다시 만들까?
TypeScript 컴파일러는 원래 TypeScript로 작성돼서 Node.js 위에서 돌아가요. 자기 언어로 자기 컴파일러를 만드는 셀프 호스팅 구조죠. 초기에는 장점이 많았지만 프로젝트가 커지면서 한계가 드러났어요.
먼저 느려요. 수십만 줄짜리 모노레포라면 타입 체크 한 번에 수십 초에서 몇 분까지 걸리기도 하고, 에디터 자동완성이 버벅이는 것도 같은 원인이에요. 병렬화도 어려워요. JavaScript는 기본적으로 싱글 스레드라서 CPU 코어가 16개여도 타입 체크는 사실상 코어 하나로 돌아가요. 메모리도 많이 먹어요. JS 객체는 생각보다 무겁고, GC(가비지 컬렉터, 안 쓰는 메모리를 자동으로 치워주는 청소부)의 부담도 커요. 마이크로소프트는 Go 포트로 대형 프로젝트 기준 약 10배 빨라졌다고 밝혔어요. 네이티브 코드로 실행되고 여러 코어를 같이 쓸 수 있게 된 덕분이에요.
Go냐 Rust냐, 핵심 쟁점
마이크로소프트가 Go를 고른 이유는 꽤 실용적이었어요. 목표가 새로 짜는 게 아니라 옮기는 것이었거든요. 기존 코드 구조를 최대한 그대로 유지해야 동작을 1:1로 맞출 수 있고, 수년간 쌓인 미묘한 타입 추론 동작도 깨뜨리지 않을 수 있어요.
그런데 TypeScript 컴파일러는 순환 참조가 가득한 자료구조에 크게 기대고 있어요. AST 노드가 부모를 가리키고, 심볼이 선언을 가리키고, 그 선언이 다시 심볼을 가리키는 식이에요. 메모리 관리는 GC에 맡기는 스타일이고요. Go에는 GC가 있어서 이런 구조를 거의 그대로 옮길 수 있어요. 반면 Rust는 소유권(ownership) 규칙 때문에 여러 곳에서 서로를 가리키는 구조를 그대로 쓰기가 까다로워요.
이게 뭐냐면, Rust에서는 '데이터의 주인은 딱 한 명'이 원칙이에요. 그래서 서로가 서로를 가리키는 그래프를 만들 때는 보통 이런 우회로를 써요.
// 포인터 대신 번호(인덱스)로 노드를 가리키는 '아레나' 패턴
struct NodeId(u32);
struct Node {
kind: SyntaxKind,
parent: Option<NodeId>,
children: Vec<NodeId>,
}
struct Arena {
nodes: Vec<Node>, // 모든 노드를 한 배열에 몰아넣고 번호로 접근
}
모든 노드를 큰 배열 하나에 넣고 '몇 번째 노드'라는 번호로 서로를 가리키는 방식이에요. 메모리 효율이나 캐시 친화성은 오히려 좋아지지만, 원래 코드와 생김새가 달라져서 줄 단위로 옮기기는 어려워져요. tsrs가 이 문제를 어떻게 풀었는지가 가장 흥미로운 관전 포인트예요.
업계 맥락: Rust 진영의 오랜 숙제
JS/TS 툴체인은 이미 상당 부분 Rust로 넘어왔어요. Next.js가 쓰는 트랜스파일러 SWC, 빠른 린터 oxlint를 품은 oxc, 포매터 겸 린터인 Biome, Vite의 차세대 번들러 Rolldown이 대표적이죠. 그런데 이 도구들은 대부분 타입을 지우는 일만 하고, 타입이 맞는지 검사하는 일은 여전히 tsc에 맡겨왔어요.
Rust로 타입 체커를 만들려는 시도가 없었던 건 아니에요. SWC 개발자가 진행한 stc는 결국 중단됐고, Ezno 같은 실험적인 프로젝트도 있었지만 tsc와 완벽하게 호환되는 건 넘기 힘든 벽이었어요. TypeScript 타입 시스템에는 공식 명세가 없고 구현 자체가 곧 명세라서, 호환성을 맞추려면 tsc의 동작을 그대로 따라가는 수밖에 없거든요. 재설계가 아닌 '포트'라는 접근이 의미 있는 이유가 바로 여기에 있어요.
한국 개발자에게 주는 시사점
당장 갈아탈 필요는 없어요. 실무에서 표준 노선은 여전히 마이크로소프트의 Go 포트예요. 프로덕션 CI에서 tsc를 대체하려면 호환성 검증이 충분히 쌓여야 해요. 다만 Rust 기반 도구를 만들거나 쓰는 팀이라면 눈여겨볼 만해요. 번들러나 린터, 엣지 런타임 같은 Rust 시스템에 타입 체크를 바로 넣을 수 있다면, 별도 바이너리를 띄우는 것보다 통합이 훨씬 깔끔해질 수 있거든요. Rust를 공부하는 분들에게도 좋은 교재예요. GC 언어로 쓰인 대규모 코드베이스를 Rust로 옮기면서 순환 참조나 수명(lifetime), 병렬 처리를 어떻게 다루는지 실전 사례로 볼 수 있으니까요.
마무리
한 줄 정리: TypeScript 네이티브화의 공식 답은 Go였지만, Cloudflare의 tsrs는 Rust라는 또 하나의 선택지를 보여줬어요.
여러분이라면 TypeScript 컴파일러를 포팅할 때 Go와 Rust 중 뭘 고르셨을까요? 타입 체커까지 Rust 생태계에 들어오면 JS 툴체인이 어떻게 바뀔지 의견 나눠봐요!
🔗 출처: Hacker News