Skip to main content

Command Palette

Search for a command to run...

TypeScript 7.0 beta 적용 및 성능 비교

Published
9 min read
T

Software engineer for web tech. Interested in sustainable growth as software engineer.

Microsoft가 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 패키지가 아니라 별도 패키지로 풀렸습니다.

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

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

호환성 — 무엇이 깨졌나

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

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 매핑도 함께 막혔습니다. 즉

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

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

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

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

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

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

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

"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.jsonbaseUrl 제거, 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-promisesif (asyncFn())처럼 Promise를 boolean 자리에 쓰는지

  • @typescript-eslint/no-unsafe-*any가 흘러다니는 경로 추적

  • @typescript-eslint/await-thenable — Promise 아닌 값에 await 거는지

  • @typescript-eslint/strict-boolean-expressionsif (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을 못 쓰는 건가? 아닙니다

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

[빌드/타입체크]  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 호환성 패키지를 따로 풀 예정입니다. 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도 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에서 **"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 및 소스 코드는 무수정