728x90

어제 사내 Slack에 "TypeScript 7 정식 나왔다는데 이거 우리 CI 다시 봐야 하는 거 아니냐"는 메시지가 올라왔다. 프론트 팀이 아니라 인프라 팀에서 먼저 반응한 게 인상적이었다. 이유는 간단하다. TypeScript 7은 컴파일러 자체를 Go로 다시 짠 물건이라, 결국 빌드 시간CI 러너 비용에 직접 꽂히는 이야기이기 때문이다.

이 글은 원문(TypeScript v7 Released | GeekNews) 발표 내용을 기준으로, 빌드/CI를 운영하는 입장에서 뭘 확인하고 뭘 조심해야 하는지 정리한 것이다. 벤치마크 수치는 공식 발표 자료를 각자 확인하는 걸 전제로 하고, 이 글에서는 임의로 수치를 지어내지 않는다.

1. 도입: 왜 지금 화제이고 어떤 문제를 푸는지

기존 TypeScript 컴파일러(tsc)는 TypeScript/JavaScript로 작성돼 있었다. 즉 TS로 만든 컴파일러가 TS를 컴파일하는 구조였다. 이게 우아하긴 한데, 실무에서 프로젝트가 커지면 아픈 지점이 명확하게 나타난다.

  • 단일 스레드 JS 런타임 위에서 돌기 때문에, 대형 모노레포에서 tsc --build가 수십 초에서 분 단위로 늘어난다.
  • 에디터에서 타입 힌트가 뜨는 데 걸리는 지연(Language Server 응답)이 프로젝트 규모에 비례해 커진다.
  • CI에서 타입 체크 스텝이 전체 파이프라인 병목이 되는 경우가 흔하다.

TypeScript 7은 이 컴파일러를 Go로 재작성한 버전이다(프로젝트 코드네임은 흔히 tsgo로 불린다). Go를 택한 핵심 이유는 네이티브 컴파일과 동시성(goroutine 기반 병렬 처리)이다. JS 런타임의 단일 스레드 한계에서 벗어나 파싱·타입 체크 단계를 병렬화할 여지가 생긴다는 게 요지다.

주의할 점 하나. 원문 댓글에서도 지적됐듯이, 제대로 된 programmatic API는 7.0이 아니라 7.1에 공개될 예정이라고 한다. 즉 지금 시점에 "완제품 다 갈아끼우면 된다"가 아니라, API 표면이 아직 흔들리는 과도기라는 점을 전제로 봐야 한다.

2. 핵심: 동작 원리를 예시·비유로

비유하자면 기존 tsc는 "한 명의 숙련된 번역가가 문서를 처음부터 끝까지 혼자 번역하는" 방식이었다. Go 재작성 버전은 "번역가 여러 명이 문서를 챕터별로 나눠 동시에 번역하고, 마지막에 취합하는" 방식에 가깝다. 파일 파싱과 타입 검사처럼 병렬화가 가능한 구간을 코어 개수만큼 나눠 처리할 수 있게 된 것이다.

파이프라인 관점에서 컴파일러가 하는 일은 크게 세 단계다. 이건 기존과 개념적으로 동일하다.

  1. 파서(Parser): 소스 코드를 AST로 변환
  2. 타입 체커(Type Checker): AST를 순회하며 타입 검증 — 여기가 가장 무겁다
  3. 에밋(Emit): 검증된 결과를 JS로 출력(또는 --noEmit이면 생략)

Go 버전이 노리는 지점은 대부분 2번 타입 체커와 대형 프로젝트의 파일 로딩/파싱 병렬화다. 실제 설치는 별도 패키지로 배포되는 형태다. 실무에서 먼저 해볼 수 있는 건 기존 프로젝트를 건드리지 않고 병렬로 타입 체크만 돌려보는 것이다.

# 기존 프로젝트는 그대로 두고, Go 기반 컴파일러만 별도 설치해서 체크
npm install -D @typescript/native-preview

# 타입 체크만 실행 (에밋 없이)
npx tsgo --noEmit -p tsconfig.json

정상적으로 통과하면 출력은 조용하다. 타입 에러가 있으면 기존 tsc와 유사한 형태로 뜬다.

src/user/service.ts:42:18 - error TS2322: Type 'string | undefined' is not assignable to type 'string'.
  Type 'undefined' is not assignable to type 'string'.

42     const name: string = user.displayName;
                    ~~~~

Found 1 error in src/user/service.ts:42

여기서 확인할 포인트는 두 가지다. 첫째, 기존 tsc와 진단 결과(에러 개수/코드)가 일치하는가. 둘째, 실행 시간이 얼마나 줄어드는가. 시간 비교는 아주 간단하게 붙일 수 있다.

# 기존 tsc
time npx tsc --noEmit -p tsconfig.json

# Go 기반
time npx tsgo --noEmit -p tsconfig.json

수치는 프로젝트 규모와 러너 사양에 따라 천차만별이니, 반드시 본인 레포에서 직접 측정하길 권한다. 남의 벤치마크는 참고용일 뿐이다.

3. 실무 관점: 도입 시 고려사항, 트레이드오프, 흔한 함정

트레이드오프: 빠른 대신 아직 다 갖춰지지 않았다

속도는 매력적이지만, 현재 시점에서 무시할 수 없는 트레이드오프가 있다.

  • programmatic API 미완성: 원문에서 언급됐듯 완전한 API는 7.1 예정이다. ts.createProgram() 같은 API에 직접 의존하는 도구(커스텀 트랜스포머, 코드 생성기 등)는 지금 바로 갈아타기 어렵다.
  • 생태계 호환성: 많은 빌드 도구가 TS의 내부 API나 타입 정보에 의존한다. 대표적으로 아래 도구들은 확인이 필요하다.
도구영향 포인트
esbuild / swc원래 타입 체크를 하지 않고 트랜스파일만 함 → 영향 적음. 타입 체크는 별도 스텝
ts-nodeTS 컴파일러 API 의존 → 새 API 안정화 전까지 호환성 확인 필요
webpack (ts-loader)내부 API 사용 방식에 따라 영향 → 로더 버전 호환 확인
ESLint (typescript-eslint)타입 정보 기반 룰은 파서 연동 방식 확인 필요

여기서 중요한 실무 감각. 상당수 팀은 이미 트랜스파일(esbuild/swc)과 타입 체크(tsc)를 분리해서 운영한다. 이런 구조라면 타입 체크 스텝만 tsgo로 실험 삼아 바꿔보기가 훨씬 수월하다. 반대로 ts-loaderts-node로 컴파일과 실행을 통째로 묶어놓은 레거시 구성은 마이그레이션 난이도가 올라간다.

흔한 함정 1: tsconfig 옵션 미지원/차이

Go 재작성 초기 단계에서는 일부 컴파일러 옵션이나 엣지 케이스 동작이 기존과 다를 수 있다. 알 수 없는 옵션을 만나면 이런 식으로 튕긴다.

error: Unknown compiler option 'someLegacyOption'.

# 또는 tsconfig 파싱 단계에서
error TS5023: Unknown compiler option '...'.

대응: 기존 tsc --noEmittsgo --noEmit에러 개수·에러 코드를 diff로 비교해서 결과가 갈리는 파일이 있는지 먼저 확인한다. 결과가 다르면 그 파일은 옵션 해석 차이 또는 미지원 케이스일 가능성이 높다.

# 두 결과를 파일로 떨궈서 비교
npx tsc   --noEmit -p tsconfig.json 2> tsc.log   || true
npx tsgo  --noEmit -p tsconfig.json 2> tsgo.log  || true
diff <(sort tsc.log) <(sort tsgo.log)

흔한 함정 2: CI 캐시 키를 안 바꿔서 옛날 결과가 통과됨

컴파일러를 바꿨는데 CI 캐시 키에 컴파일러 버전이 안 들어가 있으면, 예전 캐시가 적중해서 "실제로는 안 돌았는데 통과한 것처럼" 보이는 사고가 난다. 이건 도구 문제가 아니라 파이프라인 설계 문제인데, 컴파일러 교체 시점에 특히 잘 터진다. 캐시 키에 컴파일러 패키지 버전을 반드시 포함시켜라.

CI/CD 마이그레이션은 병렬 검증 스텝으로 시작하라

한 번에 갈아엎지 말고, 기존 tsc는 그대로 두고 tsgo를 non-blocking 스텝으로 추가하는 게 안전하다. GitHub Actions 예시.

jobs:
  typecheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci

      # 기존: 실패하면 파이프라인 중단 (source of truth)
      - name: Type check (tsc)
        run: npx tsc --noEmit -p tsconfig.json

      # 신규: 결과만 관찰, 실패해도 파이프라인 안 막음
      - name: Type check (tsgo, experimental)
        run: npx tsgo --noEmit -p tsconfig.json
        continue-on-error: true

이 상태로 몇 주 돌려보면서 두 스텝의 결과가 지속적으로 일치하고 시간 이득이 유의미하면, 그때 tsgo를 정식 스텝으로 승격하고 tsc를 내리면 된다. 인프라 관점에서 이런 "그림자 실행(shadow run)" 패턴은 리스크를 크게 줄여준다.

대안

지금 당장 타입 체크 속도만 급하고 리스크를 최소화하고 싶다면, 컴파일러 교체 대신 다음도 고려할 수 있다.

  • tsc --build프로젝트 레퍼런스 + incremental 설정으로 증분 빌드 최적화
  • 트랜스파일은 esbuild/swc로 넘기고 타입 체크만 별도 스텝으로 분리(이미 표준적인 구성)
  • 모노레포라면 Turborepo/Nx 캐시로 타입 체크 결과 자체를 캐싱

4. 정리: 한 줄 요약 + 누가 언제 써야 하는가

한 줄 요약: TypeScript 7은 Go로 재작성해 속도·병렬성을 노린 컴파일러이고, 지금은 programmatic API가 완성되기 전(7.1 예정)이라 "관찰용으로 붙여보되 소스 오브 트루스는 아직 기존 tsc"로 두는 게 안전하다.

  • 지금 실험해도 좋은 팀: 트랜스파일과 타입 체크가 이미 분리돼 있고, CI 타입 체크 시간이 병목인 대형 프로젝트. non-blocking 스텝으로 붙여 비교하기 좋다.
  • 서두르지 말아야 할 팀: ts-node/ts-loader나 커스텀 트랜스포머 등 컴파일러 내부 API에 강하게 의존하는 도구를 쓰는 곳. 7.1 API 안정화 이후를 보는 게 낫다.
  • 공통 원칙: 벤치마크는 남의 걸 믿지 말고 본인 레포에서 time으로 직접 재라. 에러 개수/코드가 기존과 일치하는지 diff로 검증하는 절차를 반드시 넣어라.

정리하면, 이건 "버전 올려서 끝"이 아니라 컴파일러 아키텍처가 바뀐 사건이다. 프론트 팀만의 이슈로 넘기지 말고, 빌드 시간과 CI 비용을 보는 인프라/백엔드 입장에서 미리 그림자 실행으로 데이터를 쌓아두면 나중에 전환 결정할 때 근거가 된다.

참고 자료

※ 벤치마크 수치와 세부 옵션 지원 범위는 버전에 따라 달라질 수 있으니, 도입 전 반드시 공식 문서와 본인 레포 측정 결과를 기준으로 판단하기 바란다.

728x90

+ Recent posts