tsc 컴파일러 완전 이해: 파이프라인과 핵심 옵션

TypeScript 컴파일러(tsc)가 .ts 파일을 .js로 변환하는 5단계 파이프라인, 자주 쓰는 컴파일러 옵션, watch 모드와 incremental 컴파일까지 깊이 다룹니다.

· 8 min read · PALDYN Team

지난 글에서 TypeScript 개발 환경을 구성했다. 이번에는 매일 실행하는 tsc 명령어가 내부에서 어떤 일을 하는지 이해해보자. 컴파일러를 이해하면 오류 메시지를 더 잘 해석하고, tsconfig.json을 더 효과적으로 설정할 수 있다.

tsc란 무엇인가

tsc는 TypeScript Compiler의 줄임말이다. TypeScript 코드를 읽어 타입 검사를 수행하고, 순수한 JavaScript를 출력하는 도구다.

# 기본 사용법
tsc                    # tsconfig.json 기반 전체 컴파일
tsc file.ts            # 단일 파일 컴파일 (tsconfig 무시)
tsc --watch            # 파일 변경 감지 자동 컴파일
tsc --noEmit           # 타입 검사만 (파일 출력 없음)
tsc --version          # 설치된 tsc 버전 확인

tsc 내부 파이프라인

TypeScript 컴파일러는 5단계를 거쳐 .ts.js로 변환한다.

tsc 컴파일 파이프라인

1단계: Scanner (스캐너/렉서)

소스 코드를 읽어 토큰(Token) 스트림으로 분해한다. function, const, "hello", 42 같은 기본 단위를 인식한다.

function greet(name: string): string {
↓ 토큰화
[keyword:function] [identifier:greet] [(] [identifier:name] [:] [keyword:string] [)] ...

2단계: Parser (파서)

토큰 스트림을 구문 분석해 AST(Abstract Syntax Tree, 추상 구문 트리)를 생성한다. AST는 코드의 구조를 트리 형태로 표현한 것이다.

// 이 코드의 AST (간략화)
function add(a: number, b: number): number {
  return a + b;
}

// AST 노드:
// FunctionDeclaration
//   name: Identifier (add)
//   parameters: [
//     Parameter (a: number),
//     Parameter (b: number)
//   ]
//   returnType: TypeReference (number)
//   body: Block
//     ReturnStatement: BinaryExpression (a + b)

3단계: Binder (바인더)

AST를 순회하며 심볼 테이블을 만든다. 각 변수, 함수, 클래스가 어떤 스코프에 속하는지 확인하고, 선언과 사용처를 연결한다.

4단계: Checker (타입 검사기)

TypeScript의 핵심이다. 바인더가 만든 심볼 테이블을 기반으로 타입 추론과 타입 호환성 검사를 수행한다. 타입 오류가 여기서 발견된다.

function multiply(a: number, b: number): number {
  return a + b;  // 타입적으로는 OK (number + number = number)
}

multiply("hello", 2);  // Checker가 오류 감지:
// Error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'

5단계: Emitter (이미터)

타입 검사를 통과한 코드에서 타입 어노테이션을 제거하고 JavaScript 파일을 생성한다. declaration: true 옵션이면 .d.ts 파일도 생성한다.

자주 쓰는 tsconfig 옵션

tsc 주요 옵션

출력 제어 옵션

{
  "compilerOptions": {
    "target": "ES2022",     // 출력 JS 버전
    "module": "commonjs",   // 모듈 시스템
    "outDir": "./dist",     // 출력 디렉터리
    "rootDir": "./src",     // 소스 루트
    "declaration": true,    // .d.ts 생성
    "declarationMap": true, // .d.ts 소스맵
    "sourceMap": true,      // .js.map 생성
    "removeComments": true  // 주석 제거
  }
}

엄격도 옵션

{
  "compilerOptions": {
    "strict": true,                    // 아래 옵션 전체 활성화
    "noImplicitAny": true,             // any 암묵 추론 금지
    "strictNullChecks": true,          // null/undefined 분리
    "strictFunctionTypes": true,       // 함수 파라미터 공변/반변
    "noImplicitReturns": true,         // 모든 경로에서 반환 강제
    "noFallthroughCasesInSwitch": true // switch 폴스루 금지
  }
}

모듈 해석 옵션

{
  "compilerOptions": {
    "moduleResolution": "bundler",       // Vite/webpack 환경
    "baseUrl": ".",                      // 절대 경로 기준점
    "paths": {
      "@/*": ["src/*"]                   // 경로 별칭
    },
    "esModuleInterop": true,             // CommonJS 호환 import
    "allowSyntheticDefaultImports": true // default import 허용
  }
}

Watch 모드

개발 중 파일 변경마다 수동으로 tsc를 실행하는 건 번거롭다. Watch 모드를 사용하면 파일 변경 즉시 자동으로 재컴파일된다.

# Watch 모드 시작
tsc --watch
# 또는
tsc -w

# 출력 예시
[11:23:45] Starting compilation in watch mode...
[11:23:46] Found 0 errors. Watching for file changes.
[11:23:52] File change detected. Starting incremental compilation...
[11:23:53] Found 0 errors. Watching for file changes.

Incremental 컴파일

프로젝트가 커지면 전체 재컴파일이 느려진다. incremental: true로 이전 컴파일 결과를 캐시해 변경된 파일만 다시 컴파일한다.

{
  "compilerOptions": {
    "incremental": true,
    "tsBuildInfoFile": ".tsbuildinfo"  // 캐시 파일 위치
  }
}
# 첫 컴파일: 전체 (느림)
tsc  # 5초

# 이후 컴파일: 변경분만 (빠름)
tsc  # 0.3초

tsc 오류 메시지 읽는 법

TypeScript 오류는 처음에는 낯설지만, 패턴을 알면 쉽게 해석된다.

src/app.ts:15:3 - error TS2322: Type 'string' is not assignable to type 'number'.

15     count = "hello";
       ~~~~~

   src/app.ts:12:7
   12   let count: number = 0;
            ~~~~~~~~~~~~~~~
   'count' is declared here.
  • src/app.ts:15:3: 파일명:줄번호:컬럼번호
  • TS2322: TypeScript 오류 코드 (검색 가능)
  • 오류 메시지: 무엇이 문제인지
  • 하위 줄: 관련 선언 위치

오류 코드로 검색하면 자세한 설명과 해결책을 바로 찾을 수 있다. TS2322 typescript로 검색해보자.

커맨드라인 옵션 vs tsconfig.json

대부분의 옵션은 두 가지 방식으로 설정할 수 있다.

# 커맨드라인 플래그 (일회성)
tsc --target ES2022 --strict --outDir dist src/index.ts

# tsconfig.json (프로젝트 기본값 — 권장)
{
  "compilerOptions": {
    "target": "ES2022",
    "strict": true,
    "outDir": "dist"
  }
}

커맨드라인 플래그는 tsconfig.json 설정을 덮어쓴다. CI/CD에서 특정 옵션을 오버라이드할 때 유용하다.

타입 선언 파일(.d.ts) 생성

라이브러리를 배포할 때는 .d.ts 파일이 필요하다. 이 파일은 JavaScript 구현 없이 타입 정보만 포함한다.

// src/utils.ts
export function add(a: number, b: number): number {
  return a + b;
}
tsc --declaration
# dist/utils.js + dist/utils.d.ts 생성
// dist/utils.d.ts (자동 생성)
export declare function add(a: number, b: number): number;

.d.ts 파일 덕분에 npm에 배포한 JavaScript 패키지도 TypeScript 사용자가 타입 정보를 활용할 수 있다.

정리

tsc 컴파일러는 Scanner → Parser → Binder → Checker → Emitter 5단계를 거쳐 .ts.js로 변환한다. Checker 단계가 TypeScript의 핵심이며, 여기서 타입 오류가 발견된다. Watch 모드와 Incremental 컴파일로 개발 속도를 높이고, tsconfig.json으로 프로젝트에 맞는 설정을 유지하자.


지난 글: TypeScript 개발 환경 설치: Node.js부터 tsconfig까지

다음 글: TypeScript Playground: 설치 없이 브라우저에서 실험하기


읽어주셔서 감사합니다. 😊