# TypeScript 7.0 beta 적용 및 성능 비교

Microsoft가 [TypeScript 7.0 beta](https://devblogs.microsoft.com/typescript/announcing-typescript-7-0-beta/)를 공개했습니다. Go로 포팅한 네이티브 컴파일러고 "약 10배 빠르다"는 게 광고 문구입니다. 그 숫자가 실제 코드베이스에서도 나오는지, 따라붙는 호환성 비용은 뭔지 직접 돌려봤습니다.

대상 프로젝트는 React 19 + Vite 5로 굴리는 SPA고 `.ts/.tsx` 파일은 604개, `composite`와 project references 구조입니다. 측정 환경은 Apple M4 10코어, pnpm 10.11입니다.

## 설치 - 기존 5.x와 병렬 운영

7.0 beta는 정식 `typescript` 패키지가 아니라 별도 패키지로 풀렸습니다.

```bash
pnpm add -D @typescript/native-preview@beta
```

CLI는 `tsgo`로 깔립니다. 정식 7.0 GA가 나오면 `typescript` 패키지로 다시 합쳐지고 CLI도 `tsc`로 돌아갈 예정입니다. 그때까진 **기존** `typescript@5.8.3`**은 그대로 두고 tsgo만 옆에 깔아 비교**하는 게 가장 안전한 선택입니다.

## 호환성 — 무엇이 깨졌나

tsconfig를 손 안 대고 tsgo로 그냥 돌리면 컴파일러 옵션 에러 두 개가 떨어집니다.

```plaintext
tsconfig.app.json(26,5): error TS5102: Option 'baseUrl' has been removed.
  Use '"paths": {"*": ["./src/*"]}' instead.
tsconfig.app.json(28,15): error TS5090: Non-relative paths are not allowed.
  Did you forget a leading './'?
```

TS 7은 `baseUrl`을 깔끔하게 빼버렸습니다. `paths`의 non-relative 매핑도 함께 막혔습니다. 즉

```jsonc
// Before (TS 5)
"baseUrl": "src",
"paths": { "@/*": ["*"] }

// After (TS 7)
"paths": { "@/*": ["./src/*"] }
```

이 변경은 TS 5에서도 그대로 동작합니다. 깨질 게 없는 정리 작업입니다.

다만 코드 레벨에서 한 군데 걸렸습니다. `baseUrl: "src"`에 기대고 있던 bare specifier 임포트가 딱 한 곳 남아 있었습니다.

```ts
// src/components/Tooltip/Tooltip.tsx
import Button from 'components/Button/Button';  // ❌ TS 7에서 해결 불가
```

다른 곳은 모두 `@/components/Button/Button` 형태 alias로 통일돼 있는데 이 파일만 옛날 방식이 살아 있었습니다. baseUrl이 빠지면서 그동안 가려져 있던 화석이 튀어나온 셈입니다.

원본 코드는 그대로 두고 비교만 돌릴 거라서 tsgo 전용 tsconfig 쪽에 임시 paths 매핑을 추가했습니다.

```jsonc
"paths": {
  "@/*": ["./src/*"],
  "components/*": ["./src/components/*"]  // 임시 우회
}
```

그 외엔 다 작동했습니다.

| 항목 | 결과 |
| --- | --- |
| `composite` + project references (`-b`) | ✅ |
| `moduleResolution: "bundler"` | ✅ |
| `target: ES2021`, `jsx: "react-jsx"`, `strict` | ✅ |
| `isolatedModules`, `allowImportingTsExtensions` | ✅ |
| 전체 src 타입체크 (604 파일) | ✅ 0 error |
| test config (vitest/jest-dom 타입 포함) | ✅ 0 error |
| `tsc -b → tsgo -b` 교체 후 `vite build` 파이프라인 | ✅ |

## 성능 — 광고가 거짓은 아니지만

3회 평균. Cold = `node_modules/.tmp` (buildinfo) 삭제 후입니다.

| 작업 | TSC 5.8.3 | tsgo 7.0-dev | 배수 |
| --- | --- | --- | --- |
| `tsc -b --force` (cold) | **3.39s** | **0.71s** | **4.8×** |
| `tsc -b` (warm, no change) | 0.20s | 0.16s | 1.3× |
| test types (cold, `--noEmit`) | **2.61s** | **0.53s** | **4.9×** |
| 풀 build (`tsc -b && vite build`) | **10.81s** | **~4.4s** | **2.5×** |

광고에 적힌 "10배"는 이 규모에선 안 나왔습니다. 5배 정도. 코드베이스가 크고 타입 인스턴스화가 무거울수록 차이가 벌어지는 구조 같습니다. 테스트를 적용해본 프로젝트는 604개 파일이고 라이브러리 타입까지 합쳐도 200K 라인 수준이라 컴파일러 자체의 오버헤드가 작은 편입니다.

`--extendedDiagnostics`를 비교해 보면 재미있는 게 하나 있습니다.

| 메트릭 | TSC 5.8.3 | tsgo 7.0-dev |
| --- | --- | --- |
| Total | 2.31s | 0.55s |
| Check | 1.74s | 0.29s |
| Memory | 431 MB | 388 MB |
| Types | 94,539 | 205,998 |

**Types 카운트가 두 배 이상 차이 납니다.** tsgo는 더 많은 타입을 인스턴스화하면서도 메모리는 덜 쓰고 시간은 1/6 수준입니다. Go 런타임의 메모리 공유 병렬화와 GC 특성이 그대로 보입니다. V8 단일 스레드 위에서 돌던 tsc와는 자원 모델 자체가 다릅니다.

병렬화 옵션 `--checkers` 스케일링도 같이 봤습니다.

| `--checkers` | Cold time |
| --- | --- |
| 1 | 1.04s |
| 4 (기본) | 0.68s |
| 8 | 0.71s |
| `--singleThreaded` | 1.11s |

M4 10코어에선 4가 sweet spot입니다. 8을 줘도 saturation에 걸려 의미 있는 개선은 안 보입니다. 이 정도 규모에선 4 워커가 분산할 만큼의 작업량이 애초에 안 나옵니다.

## 어디에 가장 효과가 있나

이번 측정에서 가장 의미 있는 숫자는 풀 빌드 파이프라인의 \*\*2.5×\*\*입니다. 4.8×처럼 화려하진 않지만 CI에서 실제로 체감되는 건 이쪽입니다. 타입체크는 풀 빌드의 일부일 뿐이고 vite의 esbuild 단계는 어차피 빠르니까요. CI 빌드 시간을 좌우하는 부분은:

1.  **PR마다 도는 type-check** — 5×. 가장 직접적인 개선
    
2.  **Docker 이미지 빌드의** `tsc -b` **단계** — 풀 빌드 시간이 60% 줄어듭니다
    
3.  **워치 모드의 cold restart** — IDE 재시작이나 클린 빌드 직후 기다리는 시간이 짧아집니다
    

반대로 **warm incremental은 차이가 거의 없습니다** (0.20s → 0.16s). `.tsbuildinfo`가 캐시되어 있으면 tsgo 쪽도 더 줄일 여지 자체가 작습니다. 일상 dev 루프에서 느끼는 차이는 생각보다 미미할 수 있다는 얘기입니다.

## 도입할 것인가

결론은 보류. 이유가 몇 가지 있습니다.

1.  **베타입니다.** 안정 프로그래밍 API는 7.1 이후로 미뤄져 있고 에디터 쪽 의미론적 강조나 임포트 관리도 일부 미완성입니다.
    
2.  `@typescript-eslint`**가 아직 TS 5 기반입니다.** ESLint 파이프라인은 베이스라인을 따라가니까, 컴파일러만 7로 올리면 결국 두 컴파일러를 동시에 들고 가야 합니다.
    
3.  **현재 빌드가 이미 충분히 빠릅니다.** 풀 빌드 10.8s를 4.4s로 줄여도 우리 워크플로의 병목은 거기가 아닙니다.
    

다만 **준비는 해두는 게 맞습니다.** GA 시점에 무리 없이 넘어가려면 지금부터 할 수 있는 작업이 있습니다.

*   `tsconfig.app.json`의 `baseUrl` 제거, `paths`를 `{ "@/*": ["./src/*"] }`로 변경 (TS 5 호환)
    

*   `Tooltip.tsx`의 bare specifier 임포트를 `@/components/...`로 통일
    
*   CI 빌드 시간 추적 — GA 직후 효과 측정이 가능하도록 베이스라인 기록
    

## typescript-eslint가 도대체 무슨 일을 하길래

여기서 자연스럽게 따라붙는 의문이 있습니다. "린트 패키지 하나 때문에 컴파일러를 못 올리는 건가?" 그래서 typescript-eslint가 우리 파이프라인에서 실제로 뭘 하는지부터 정리해 봅니다.

ESLint 자체는 JS 파서로 코드를 토큰 단위로 봅니다. 타입 정보가 없습니다. 그래서 다음 같은 룰은 **순수 ESLint로는 불가능**합니다.

*   `@typescript-eslint/no-floating-promises` — Promise를 await/then 없이 떨궜는지 검출
    
*   `@typescript-eslint/no-misused-promises` — `if (asyncFn())`처럼 Promise를 boolean 자리에 쓰는지
    
*   `@typescript-eslint/no-unsafe-*` — `any`가 흘러다니는 경로 추적
    
*   `@typescript-eslint/await-thenable` — Promise 아닌 값에 await 거는지
    
*   `@typescript-eslint/strict-boolean-expressions` — `if (str)`처럼 nullable 문자열을 boolean으로 쓰는지
    

이런 **type-aware 룰**들은 typescript-eslint가 내부적으로 TypeScript 컴파일러 API(`createProgram`, `getTypeChecker`)를 호출해서 타입 정보를 얻은 다음 AST 위에서 판단합니다. 즉 typescript-eslint는 "TS 문법을 ESLint가 읽게 해주는 어댑터"가 아니라 **TypeScript 컴파일러를 ESLint 안에서 호스팅하는 레이어**입니다.

### TS 7이 왜 문제냐면

tsgo는 Go로 다시 짜서 Node 프로세스 안의 동기 함수가 아니라 **WASM/네이티브 바인딩**으로 노출될 가능성이 큽니다. 그러면 컴파일러 API 호출이 비동기가 됩니다. 그런데 ESLint 코어는 아직 **rule.create()가 동기 함수**라는 전제로 굴러갑니다. 룰 안에서 `await checker.getTypeAtLocation(...)`을 못 쓴다는 얘기입니다. 이걸 풀려면 ESLint 내부 아키텍처 자체가 async 파서/룰을 지원해야 합니다. typescript-eslint 한 팀이 손쓸 수 있는 범위 밖입니다.

### 그럼 TS 7을 못 쓰는 건가? 아닙니다

여기가 핵심입니다. **컴파일러와 린터를 분리해서 운영하면 됩니다.**

```plaintext
[빌드/타입체크]  tsgo (TS 7)        ← 빠름
[린트]        typescript-eslint + typescript@5/6  ← 기존대로
```

이렇게 깔면:

*   `pnpm build`, `pnpm type-check`는 tsgo로 → 5배 빠름
    
*   `pnpm lint`는 기존 typescript-eslint + TS 5/6 그대로 → 영향 없음
    
*   node\_modules에 `typescript`(5/6)와 `@typescript/native-preview`(7)가 공존
    

실제로 정식 7.0 GA에선 이 시나리오를 위해 [`@typescript/typescript6` 호환성 패키지](https://devblogs.microsoft.com/typescript/announcing-typescript-7-0-beta/)를 따로 풀 예정입니다. tsgo 쓰면서도 typescript-eslint가 import할 TS 5/6 컴파일러를 옆에 깔아둘 수 있습니다.

### 그러면 왜 보류했냐

기술적으로 못 쓰는 게 아니라 **운영 비용이 늘어나기 때문**입니다.

1.  **두 컴파일러 동기화 부담**: tsgo가 통과시키는 코드를 typescript-eslint(TS 5)가 다른 룰로 잡거나, 반대 상황이 생길 수 있습니다. 7.0의 새 기본값(`strict`, `noUncheckedSideEffectImports` 등)과 5.x 동작이 미세하게 어긋나는 케이스가 PR마다 마찰이 됩니다.
    
2.  **type-aware 룰의 성능 이점이 안 살아남**: tsgo가 빨라봤자 린트가 여전히 TS 5 컴파일러로 타입 정보를 만들어내니까, **CI에서 가장 무거운 단계인 lint는 그대로**입니다. tsgo 도입 효과의 절반이 죽습니다.
    
3.  **베타 + 안정 API 부재**: 7.1 이후 안정화될 동안 기다리면, 그 시점엔 typescript-eslint도 일부 호환을 마쳤을 가능성이 있습니다.
    

| 항목 | typescript-eslint 없으면 |
| --- | --- |
| TS 문법 ESLint 인식 | 안 됨 (다른 어댑터로 대체 가능, 예: Oxlint) |
| 타입 기반 룰 (no-floating-promises 등) | **사실상 불가능** |
| TS 7 컴파일 사용 | 가능 (린트와 분리만 하면) |

요약하면 typescript-eslint는 **타입 안전성을 코드 리뷰 자동화로 끌어올리는 핵심 도구**입니다. TS 7을 못 쓰게 막는 게 아니라, **TS 7으로 갔을 때 효과가 반감되고 운영이 복잡해진다**는 게 실제 보류 사유입니다. "기술적 호환성"이 아니라 **"가성비"** 의 문제입니다.

## typescript-eslint는 언제 따라오나

그래서 typescript-eslint가 정확히 어디까지 와 있는지도 짚어봤습니다. 결론부터 쓰면 **TS 6는 이미 따라왔고, TS 7은 아직입니다.**

### TS 6: 이미 지원 완료

2026-04 기준 `@typescript-eslint/parser@8.59.0`의 peer 범위는 `typescript: ">=4.8.4 <6.1.0"`입니다. TS 6.0이 이미 들어가 있다는 얘기입니다. 트래킹 이슈 [#12123](https://github.com/typescript-eslint/typescript-eslint/issues/12123)도 Closed 상태고, `lib.d.ts` 재생성과 `target: es5` 폐지에 따른 fixture 업데이트, JSX 닫기 태그 토큰화 변경, 새 기본 컴파일러 옵션 대응까지 모두 끝났습니다.

우리 프로젝트는 `@typescript-eslint/*@^8.15.0`을 쓰고 있어서 minor bump만 하면 끝입니다. TS 6로 가는 길에 린트 쪽 장벽은 거의 없다고 봐도 됩니다.

### TS 7: 공식 ETA 없음, 그리고 구조적 블로커가 있다

핵심은 버전 호환이 아니라 **API 모델이 달라진다**는 데 있습니다. 위에서 짚은 async 이슈가 그대로 작용합니다.

1.  tsgo는 **WASM/네이티브 바인딩으로 노출될 가능성**이 큽니다. 그러면 API가 비동기로 가게 됩니다.
    
2.  ESLint는 **async parser를 아직 지원하지 않습니다.** 린터 쪽 인프라부터 손봐야 한다는 뜻입니다.
    
3.  typescript-eslint 팀은 [#10940](https://github.com/typescript-eslint/typescript-eslint/issues/10940)에서 \*\*"tsgolint로 예산을 옮길 계획 없다, 기존 typescript-eslint를 계속 발전시키겠다"\*\*고 못을 박았습니다. 같은 팀이 두 갈래를 동시에 끌고 가진 않겠다는 얘기입니다.
    

대안 진영도 살펴보면:

*   **tsgolint** (typescript-eslint 팀이 만든 Go 기반 PoC) — 초기 단계, 적극적으로 개발되고 있지 않고 프로덕션엔 부적합
    
*   **Oxlint** — typescript-go 통합 type-aware 린팅 프리뷰 중. 다른 베팅입니다.
    

종합하면 정식 TS 7(tsgo) 지원은 **빨라야 GA + 수개월**, 현실적으론 더 길어질 수도 있습니다. 그 사이엔 **컴파일은 tsgo, 린트는 TS 5/6**으로 컴파일러를 둘 끌고 가는 모델이 일반적인 운영 형태가 될 것 같습니다.

### 마이그레이션 계획

이 정보까지 반영하면 단계가 좀 더 선명해집니다.

1.  **단기 (지금 ~ 정식 7.0 GA 전)**: TS 5.8.3 유지. tsconfig 정리(`baseUrl` 제거, paths 상대화)와 `Tooltip.tsx` 임포트 통일만 선반영합니다.
    
2.  **중기 (TS 6.0 stable 안정화 후)**: typescript와 typescript-eslint를 6.x로 minor 업. 린트 깨짐은 거의 없고, CI 빌드 시간 변화를 같이 측정해 두면 됩니다.
    
3.  **장기 (TS 7 GA + typescript-eslint 7 호환 확정 후)**: 컴파일러 7.0 전환. 그전에 들어가면 "린트는 6, 컴파일은 7"이라는 이중 운영을 떠안게 되니까, 충분한 가치가 보일 때만 움직이는 게 맞습니다.
    

정리하면 TS 6는 자연스럽게 흘러가는 흐름이고 TS 7은 별도 의사결정 게이트가 필요합니다. 이번 측정은 그 게이트를 통과시킬 만큼 데이터가 모이는지 미리 확인해 보는 사전 작업으로 의미가 있습니다.

## 남는 질문

*   이 5× 격차가 코드베이스가 커질수록 더 벌어질지가 궁금합니다. 광고된 10×에 닿는 임계점이 어디쯤인지는 별도 측정이 필요할 듯합니다.
    
*   `composite` 빌드 그래프가 깊은 monorepo에서 `--builders` 옵션이 얼마나 먹히는지. 이 프로젝트는 references가 2개뿐이라 측정이 안 됐습니다.
    
*   정식 7.0이 `typescript` 패키지로 들어왔을 때 npm 의존성 트리에 어떤 충돌이 생길지. `@typescript/typescript6` 호환성 패키지를 쓰는 시나리오가 실제로 어떻게 정착될지도 두고 봐야겠습니다.
    

## 변경된 파일

*   `package.json` — `@typescript/native-preview` devDep + `type-check:tsgo`, `test:types:tsgo` 스크립트
    
*   `tsconfig.tsgo.{json,app.json,node.json,test.json}` — 비교용 별도 설정 (원본 tsconfig 보존)
    
*   기존 tsconfig 및 소스 코드는 무수정
