TypeScript Meets Go: 10배 빠른 TypeScript 이해하기
Ethan Miller
Product Engineer · Leapcell

Go로의 TypeScript 마이그레이션 심층 탐구: 결정, 장점 및 미래 전망
I. 프로젝트 배경 및 기원
(I) 프로젝트 코드명의 유래
새로운 TypeScript 마이그레이션 프로젝트의 코드명은 Corsa입니다. 이전 코드 베이스인 Strata는 한때 TypeScript의 초기 코드명이었으며, 2010년 말 또는 2011년 초에 내부 개발 단계에서 시작되었습니다. 초기 팀은 Steve Lucco, Anders Hejlsberg, Luke로 구성되었습니다. Steve는 Internet Explorer의 JavaScript 엔진에서 스캐너와 파서를 추출 및 수정하여 원본 프로토타입 컴파일러를 작성했습니다. 이는 개념 증명에 사용된 C# 코드 베이스였습니다.
(II) 변경을 주도하는 성능 문제
ECMAScript 커뮤니티에서는 esbuild 및 swc와 같이 도구 의존성이 높은 프로젝트를 네이티브 코드로 마이그레이션하는 것이 추세입니다. TypeScript는 성능 및 확장성 문제에 직면해 있습니다. 프로젝트가 계속 성장함에 따라 컴파일러는 V8 및 JavaScript 엔진에 더 많은 부담을 줍니다. 새로운 기능이 추가됨에 따라 시작 시간이 길어집니다. 이전의 최적화는 5% - 10%의 개선만 가져올 수 있었으며, 성능 최적화의 한계에 도달했습니다.
II. Go 언어를 선택한 이유
(I) Rust 언어와의 비교
- 메모리 관리 및 호환성: 기존 TypeScript 코드 베이스는 자동 가비지 컬렉션의 존재를 가정합니다. 그러나 Rust의 메모리 관리는 자동이 아닙니다. 해당 borrow checker는 데이터 구조의 소유권에 대한 엄격한 제약 조건을 가지고 있으며 순환 데이터 구조를 금지합니다. TypeScript의 데이터 구조(예: AST(추상 구문 트리))는 순환 참조를 광범위하게 사용합니다. Rust로 마이그레이션하려면 데이터 구조를 재설계해야 하므로 어려움이 더해집니다. 따라서 Rust는 기본적으로 제외됩니다.
- 개발자 경험 및 학습 비용: JavaScript에서 Go로의 전환이 Rust로의 전환보다 쉽습니다. 일부 측면에서 Go 코드는 JavaScript 코드와 유사합니다. Rust에서 복잡하거나 재귀적인 구조를 처리할 때 TypeScript 코드에서 진화하는 과정을 이해하기가 더 어렵습니다. 인적 자원의 관점에서 Go를 선택하는 것이 더 유리합니다.
(II) C# 언어와의 비교
- 언어 설계 방향: Go는 네이티브 코드에 더 많은 우선순위를 부여하는 언어입니다. 자동 가비지 컬렉션 기능이 있으며 데이터 구조 레이아웃 및 인라인 구조 측면에서 더 표현력이 뛰어납니다. C#은 다소 바이트코드 지향적입니다. 사전 컴파일이 있지만 모든 플랫폼에서 사용할 수 있는 것은 아니며 처음부터 네이티브 성능 최적화를 목표로 설계되지 않았습니다.
- 프로그래밍 패러다임의 차이: TypeScript의 JavaScript 코드 베이스는 고도의 함수형 프로그래밍 스타일을 채택하고 있으며 코어 컴파일러는 클래스를 거의 사용하지 않습니다. Go는 또한 함수와 데이터 구조에 중점을 둡니다. 대조적으로 C#은 주로 객체 지향 프로그래밍(OOP)입니다. C#으로 마이그레이션하려면 프로그래밍 패러다임을 전환해야 하므로 마이그레이션 마찰이 증가합니다.
(III) Go 언어의 유리한 적합성
Go 언어는 모든 주류 플랫폼에서 훌륭하게 최적화된 네이티브 코드를 제공할 수 있습니다. 순환 데이터 구조 및 인라인 데이터 구조를 허용하는 데이터 구조에 대한 표현력이 뛰어납니다. 자동 가비지 컬렉션 및 공유 메모리에 대한 동시 액세스 기능은 물론 VS Code 및 기타 도구에서 제공하는 훌륭한 툴체인과 뛰어난 지원 기능을 갖추고 있습니다. TypeScript 마이그레이션의 다각적인 요구 사항을 충족하며 수많은 언어 중에서 두각을 나타냅니다.
III. 프로젝트가 직면한 과제 및 해결책
(I) 부트스트래핑 포기의 절충
부트스트래핑 언어는 자체적으로 작성된 언어입니다. TypeScript는 이전에는 부트스트래핑 언어였습니다. Go로 마이그레이션한 후 부트스트래핑을 포기하는 것에 대한 우려가 있지만 10배의 성능 향상을 위해 팀은 여전히 마이그레이션을 선택합니다. 그러나 language service 부분과 같이 JavaScript로 작성된 일부 부분은 유지됩니다. 팀은 네이티브 부분(Go)과 다른 언어의 소비자 간에 API를 구축하기 위한 솔루션을 모색하고 있습니다.
(II) 호환성 보장을 위한 노력
TypeScript에는 공식 사양이 없으며 참조 구현은 사양과 유사합니다. Go로 마이그레이션할 때 의미론적 일관성을 유지해야 합니다. 팀의 목표는 99.99% 호환성을 유지하고 동일한 코드 베이스에 대해 정확히 동일한 오류를 생성하는 것입니다. 현재 오픈 소스 컴파일러는 Visual Studio Code를 모두 컴파일하고 확인할 수 있으며 충돌 없이 20,000개의 적합성 테스트를 실행할 수 있습니다. 팀은 오류 기준선을 분석하고 차이점을 제거하여 이전 컴파일러를 즉시 대체할 수 있도록 하는 것을 목표로 합니다.
(III) 유형 정렬의 결정성 문제
이전 컴파일러는 단순한 비결정적 유형 정렬 방법을 사용했는데, 이 방법은 단일 스레드 환경에서는 결정적이지만 다중 스레드 동시 환경에서는 비결정적이었습니다. 새 코드 베이스는 결정적 유형 정렬을 도입해야 하는데, 이로 인해 일부 경우에 이전 컴파일러와 유형 순서가 달라집니다. 특히 공용체 유형의 순서는 일부 시나리오에서 중요하며 팀은 이러한 문제를 해결하기 위해 노력하고 있습니다.
(IV) API 설계의 딜레마
이전 코드 베이스에서 컴파일러의 거의 모든 내부 구조가 API로 노출되었습니다. 새 코드 베이스는 API를 재설계하고 프로세스 간 통신 중에 API 효율성을 보장하는 것을 고려해야 합니다. 현재 팀은 새 코드 베이스에 대해 버전 관리 가능하고 최신 API를 제공하는 방법을 모색하고 있습니다.
IV. 프로젝트에서 동시성의 적용 및 장점
(I) 컴파일러의 함수형 프로그래밍 기초는 동시성을 용이하게 합니다.
TypeScript 컴파일러는 원래 함수형 프로그래밍 모델을 채택했고 안전한 공유를 보장하기 위해 불변성을 광범위하게 사용했습니다. 예를 들어 스캔, 파싱 및 바인딩 후의 AST는 기본적으로 불변으로 간주됩니다. 여러 유형 검사기가 동일한 AST를 동시에 처리할 수 있으므로 JavaScript 자체에 공유 메모리를 위한 동시 메커니즘이 없더라도 동시 처리를 위한 훌륭한 기반을 제공합니다.
(II) 파싱 단계에서 동시성 구현
파싱 작업은 병렬화에 매우 적합합니다. 각 소스 파일의 파싱 작업은 완전히 독립적으로 완료될 수 있습니다. 예를 들어 5000개의 소스 파일과 8개의 CPU가 있는 경우 파일을 8개 부분으로 나눌 수 있고 각 CPU는 한 부분을 처리합니다. 공유 메모리 공간에서 완료 후 모든 데이터 구조를 빌드하고 연결하는 부분이 수행됩니다. Go에서 파싱 단계의 동시성을 구현하는 것은 매우 간단합니다. goroutine에서 작업을 실행하는 데 약 10줄의 코드만 필요하고 동시에 뮤텍스를 사용하여 공유 리소스를 보호하면 성능을 3~4배 향상시킬 수 있습니다.
(III) 유형 검사 단계를 위한 동시성 체계
유형 검사기는 프로그램의 전역 보기가 필요하므로 파싱 프로세스와 같이 완전히 독립적일 수 없습니다. 팀은 프로그램을 여러 부분(현재는 4개로 하드 코딩됨)으로 나누고 4개의 유형 검사기를 만듭니다. 각 검사기는 할당된 파일 부분을 검사합니다. 그들은 기본 불변 AST를 공유하고 자체 유형 상태를 빌드합니다. 이 방법은 약 20% 더 많은 메모리(유형 중복으로 인해)를 소비하지만 약 3배의 추가 성능 향상을 달성할 수 있습니다. 네이티브 코드로 인한 3배의 성능 향상과 결합하면 전체 성능 향상은 10배에 달할 수 있습니다.
V. TypeScript의 미래 전망
(I) 언어 기능의 개발 동향
현재 ECMAScript의 개발 속도가 느려졌습니다. 커뮤니티 피드백에 따르면 사람들은 유형 시스템의 새로운 멋진 기능보다 확장성 및 성능에 더 관심이 있습니다. TypeScript 팀은 ECMAScript 위원회의 작업에 주의를 기울이고 유형 시스템의 새로운 기능을 적절히 처리하는 동시에 유형 검사기의 10배 속도 향상의 영향을 고려하고 새로운 가능성을 모색할 것입니다.
(II) 인공지능과의 결합 가능성
빠른 유형 검사기를 사용하여 유형 분석 결과, 심볼 선언 위치 등과 같은 대규모 언어 모델(LLM)에 컨텍스트 정보를 제공합니다. AI의 출력을 실시간으로 검사하여 의미론적 정확성을 보장하고 AI가 안전하고 신뢰할 수 있는 코드를 생성할 수 있도록 보장하여 새로운 개발 경로를 엽니다.
(III) 네이티브 런타임의 구상
TypeScript용 네이티브 런타임이 가능한지 여부를 탐색합니다. 현재 Rust로 작성된 Deno가 있습니다. 객체 모델 및 숫자 처리 방식과 같이 성능에 영향을 미치는 JavaScript의 일부 요소가 있지만 TypeScript용 네이티브 런타임을 만드는 데에는 많은 불확실한 요소가 있으며 미래 개발 방향은 여전히 불분명합니다.
VI. 제3자 기여 및 커뮤니티 영향
JavaScript에서 Go로의 전환은 시스템에 비교적 부드럽습니다. JavaScript만 아는 사람보다 Go와 JavaScript를 모두 아는 사람이 적어 기여자 수가 줄어들 수 있지만 컴파일러에 기여하는 사람 수는 원래 많지 않았으며 일반적으로 네이티브 환경에 발을 들여놓는 데 관심이 있습니다. Go 언어는 간단하며, 이 간단한 설계는 10배의 성능 향상과 같은 놀라운 결과를 가져왔으며 커뮤니티의 활력과 발전을 저해하지 않을 것입니다.
VII. TypeScript와 Go 언어 간의 공통 명령문 비교
(I) 루프
- TypeScript(JavaScript 기반)
for
루프:
for (let i = 0; i < 10; i++) { console.log(i); }
for...of
루프(배열과 같은 반복 가능한 객체를 반복하는 데 사용됨):
const arr = [1, 2, 3]; for (const num of arr) { console.log(num); }
for...in
루프(주로 객체의 속성을 반복하는 데 사용됨):
const obj = { a: 1, b: 2 }; for (const key in obj) { console.log(key, obj[key]); }
- Go 언어
for
루프(Go 언어에는 하나의 기본 루프 구조인for
루프만 있지만 다양한 루프 방식을 구현할 수 있습니다.)
for i := 0; i < 10; i++ { fmt.Println(i) }
- 배열 및 슬라이스와 같은 반복 가능한 객체를 반복합니다.
arr := []int{1, 2, 3} for index, value := range arr { fmt.Println(index, value) }
- 맵을 반복합니다.
m := map[string]int{"a": 1, "b": 2} for key, value := range m { fmt.Println(key, value) }
(II) 함수
- TypeScript
- 함수 정의:
function add(a: number, b: number): number { return a + b; }
- 화살표 함수:
const multiply = (a: number, b: number): number => a * b;
- Go 언어
- 함수 정의:
func add(a int, b int) int { return a + b }
- 익명 함수(변수에 할당하거나 매개변수로 전달할 수 있습니다.)
multiply := func(a int, b int) int { return a * b }
(III) 객체 지향 프로그래밍(OOP)
- TypeScript
- 클래스 정의:
class Animal { name: string; constructor(name: string) { this.name = name; } speak() { console.log(`${this.name} makes a sound.`); } }
- 상속:
class Dog extends Animal { constructor(name: string) { super(name); } speak() { console.log(`${this.name} barks.`); } }
- Go 언어
- Go 언어에는 기존의 클래스 및 상속 개념이 없습니다. 구조체 및 메서드 집합을 통해 OOP와 유사한 기능을 구현합니다.
- 구조체 정의:
type Animal struct { Name string } func (a *Animal) Speak() { fmt.Printf("%s makes a sound.\n", a.Name) }
- 구성을 통해 상속과 유사한 것을 구현합니다.
type Dog struct { Animal } func (d *Dog) Speak() { fmt.Printf("%s barks.\n", d.Name) }
(IV) 함수형 프로그래밍
- TypeScript
- 고차 함수 예제(함수를 매개변수로 허용):
function operateOnArray(arr: number[], callback: (num: number) => number): number[] { return arr.map(callback); } const result = operateOnArray([1, 2, 3], num => num * 2);
- 불변 데이터 구조는 외부 라이브러리(예: Immutable.js)의 도움으로 구현할 수 있습니다. 예를 들어:
import { fromJS } from 'immutable'; const list = fromJS([1, 2, 3]); const newList = list.push(4);
- Go 언어
- 고차 함수 예제:
func operateOnSlice(slice []int, callback func(int) int) []int { result := make([]int, len(slice)) for i, v := range slice { result[i] = callback(v) } return result } result := operateOnSlice([]int{1, 2, 3}, func(num int) int { return num * 2 })
- Go 언어 자체는 일부 함수형 프로그래밍 언어와 같은 불변 데이터 구조를 기본적으로 지원하지 않습니다. 그러나 구조체를 복사하는 등의 방법으로 데이터 불변성을 보장하는 것과 같은 일부 설계 패턴 및 라이브러리를 통해 불변 동작을 시뮬레이션할 수 있습니다.
참조: https://www.youtube.com/watch?v=pNlq-EVld70&ab_channel=MicrosoftDeveloper
Leapcell: 웹 호스팅, 비동기 작업 및 Redis를 위한 차세대 서버리스 플랫폼
마지막으로 Go 서비스를 배포하기에 가장 적합한 플랫폼인 **Leapcell**을 추천합니다.
1. 다국어 지원
- JavaScript, Python, Go 또는 Rust로 개발하세요.
2. 무제한 프로젝트를 무료로 배포
- 사용량에 대해서만 지불하세요. 요청도 없고 요금도 없습니다.
3. 최고의 비용 효율성
- 유휴 요금 없이 사용한 만큼 지불하세요.
- 예: $25로 평균 응답 시간 60ms에서 694만 건의 요청을 지원합니다.
4. 간소화된 개발자 경험
- 간편한 설정을 위한 직관적인 UI.
- 완전 자동화된 CI/CD 파이프라인 및 GitOps 통합.
- 실행 가능한 인사이트를 위한 실시간 메트릭 및 로깅.
5. 손쉬운 확장성 및 고성능
- 쉬운 동시성 처리를 위한 자동 확장.
- 제로 운영 오버헤드 - 빌드에만 집중하세요.
Leapcell Twitter: https://x.com/LeapcellHQ