[Typescript] 타입스크립트의 컴파일 과정

mainsain·2024년 2월 20일
5

Typescript

목록 보기
8/8
post-thumbnail

1️⃣ ] 자바스크립트의 런타임과 타입스크립트의 컴파일

1️⃣ ] 런타임과 컴파일타임

컴파일타임 : 소스코드가 컴파일 과정을 거쳐 컴퓨터가 인식할 수 있는 기계어코드(바이트 코드)로 변환되어 실행할 수 있는 프로그램이 되는 과정
런타임 : 컴파일 과정을 마친 응용 프로그램이 사용자에 의해 실행되는 과정

2️⃣ ] 자바스크립트 런타임

자바스크립트가 실행되는 환경을 의미하며, 대표적인 JS 런타임으로 인터넷 브라우저와, Node.js 등이 있다.

  • 주요 구성 요소로 자바스크립트 엔진, 웹 API, 콜백 큐, 이벤트 루프, 렌더 큐가 있다.

JS는 대표적인 인터프리터(interpreter) 언어로 별도의 컴파일 과정이 존재하지 않는다고 알려져 있다.
하지만 엄밀히 말해, JS에서도 컴파일 단계가 존재하며 JS를 해석하고 실행하는 역할을 가진 V8엔진은 때때로 JS코드를 최적화하기위해 컴파일단계를 거친다.
따라서 JS는 기본적으로 인터프리터 언어의 특징을 가지지만, 현대의 JS엔진은 컴파일 언어의 특성을 일부 통합하고 있다.

3️⃣ ] 타입스크립트의 컴파일

타입스크립트는 tsc 컴파일러를 통해 자바스크립트 코드로 변환된다.

  • TS는 고수준 언어가 고수준 언어(JS)로 변환되는 것이기에, 컴파일이 아닌 트랜스파일(Transpile)이라고 부르기도 한다.
  • 이 과정은 소스코드를 다른 소스코드로 변환하는 것이기에, 타입스크립트 컴파일러를 소스 대 소스 컴파일러(source -to-source compiler)라고 지칭하기도 한다.

트랜스파일의 다른 예시로, JS ES6 이상의 문법을 ES5 이하의 문법으로 변환하는 바벨(Babel)이 있다.
컴파일과 트랜스파일을 통틀어 컴파일이라고 부르기도 한다.

TS는 .ts 확장자가 붙은 파일을 찾아내 컴파일한 다음, .js 확장자가 붙은 JS파일을 만들어낸다.

  • TS 컴파일러는 소스코드를 해석해 AST(Abstract Syntax Tree, 최소 구문 트리)를 만든다.
  • 타입 확인을 거친 후 결과 코드를 생성한다.

TS 컴파일러가 소스코드를 컴파일하여 프로그램이 실행되기까지의 과정

  1. 타입스크립트 소스코드를 타입스크립트 AST로 만든다. (tsc)
  2. 타입 검사기가 AST를 확인하여 타입을 확인한다. (tsc)
  3. 타입스크립트 AST를 자바스크립트 소스로 변환한다.(tsc)
  4. 자바스크립트 소스코드를 자바스크립트 AST로 만든다. (런타임)
  5. AST가 바이트 코드로 변환된다. (런타임)
  6. 런타임에서 바이트 코드가 평가되어 프로그램이 실행된다. (런타임)

AST(Abstract Syntax Tree) : 컴파일러가 소스코드를 해석하는 과정에서 생성된 데이터 구조.
컴파일러는 어휘적 분석(lexical analysis)구문 분석(syntax analysis)을 통해 소스코드를 노드 단위의 트리 구조로 구성한다.

이때 TS 소스코드의 타입은 1 ~ 2단계에서만 사용된다. 3단계부턴 타입을 확인하지 않는다.

  • 개발자가 작성한 타입 정보는 결과 프로그램에 아무런 영향을 미치지 않는다.
  • 따라서 TS의 존재의 큰 이유는 타입 확인이다.
  • 컴파일타임에 에러를 발견할 수 있는 장점.

2️⃣ ] 타입스크립트 컴파일러의 동작

앞에서 TS 컴파일 과정의 전반적인 흐름을 살펴봤다. 이 절에선, TS 컴파일러의 주요 역할에 대해 알아본다.

1️⃣ ] 코드 검사기로서의 타입스크립트 컴파일러

TS는 정적으로 코드를 분석해 에러를 검출하며, 코드 실행 전에 JS 런타임에서 발생할 수 있는 에러를 사전에 알려준다.

const developer = {
  work() {
      console.log("working...");
  },
};

developer.work(); // working...
developer.sleep(); // TypeError: developer.sleep is not a function

이 코드는 JS로 작성할 땐 에러가 발생하지 않지만, 런타임엔 에러가 발생한다.

그러나 같은 코드를 TS로 작성하면, 사전에 에러를 발견해 알려준다.

  • 따라서 TS 컴파일러는 런타임에서 발생할 수 있는 문법 오류의 에러 뿐만 아니라 타입 에러를 잡아낼 수 있다.
  • tsc binder를 사용하여 타입 검사를 하며, 코드를 안전하게 만든 후 TS AST를 JS 코드로 변환한다.

2️⃣ ] 코드 변환기로서의 타입스크립트 컴파일러

타입을 검사한 뒤, TS 코드를 각 런타임 환경에서 동작할 수 있도록 구버전의 JS로 트랜스파일한다.

  • TS 소스코드는 브라우저와 같은 런타임에서 실행될 수 없다.
  • TS 소스코드를 파싱하고, JS 코드로 변환해야 비로소 실행할 수 있게 된다.
type Fruit = "banana" | "watermelon" | "orange" | "apple" | "kiwi" | "mango";

const fruitBox: Fruit[] = ["banana", "apple", "mango"];

const welcome = (name: string) => {
    console.log(`hi! ${name} :)`);
};
"use strict";

var fruitBox = ["banana", "apple", "mango"];

var welcome = function (name) {
    console.log("hi! ".concat(name, " :)"));
};

TS 컴파일러가 TS 파일을 JS로 변환한 결과 예시 코드이다.

  • 타입 정보가 제거되었다. → 브라우저는 비로소 코드를 이해하고 정상적으로 수행할 수 있다.

정리하면, 타입스크립트 컴파일러의 역할을 크게 2가지로 나눌 수 있다.

  1. 최신 버전의 TS, JS 코드를 구버전의 JS로 트랜스파일한다.
  2. 코드의 타입 오류를 검사한다.

??? : 타입스크립트의 컴파일러랑 Babel과 다른게 무엇인가요??

같은 점 : 소스코드를 ES5 이하의 JS 코드로 컴파일한다.
다른 점 : tsc는 타입검사를 하지만, Babel은 하지 않는다. Babel은 최신 JS를 낮은 버전으로 컴파일하는 것이 주된 역할이다.

3️⃣ ] 타입스크립트 컴파일러의 구조

이제 타입스크립트 컴파일러의 구성 요소들을 훑어보며 컴파일러의 동작 방식을 이해하자.

  • TS 컴파일러는 다섯 단계를 거쳐 타입 검사와 JS 소스 변환을 진행한다.

1️⃣ ] 프로그램 (Program)

TS 컴파일러는 tsc 명령어로 실행되며, tsconfig.json에 명시된 컴파일 옵션을 기반으로 수행한다.

  • 먼저, 전체적인 컴파일 과정을 관리하는 프로그램 객체(인스턴스)가 생성된다.
    • 이는 컴파일할 TS 소스파일과 소스파일 내부에서 import된 파일을 불러온다.
    • 가장 최초로 불러온 파일을 기준으로 컴파일 과정이 시작된다.

2️⃣ ] 스캐너 (Scannner)

TS 소스를 JS로 변환하기 위한 첫 단계는 스캐너이다.

  • 스캐너는 소스코드를 작은 단위로 나누어 의미 있는 토큰으로 변환하는 작업을 수행한다.
const woowa = "bros";

이렇게 스캐너에 의해 분석된다.

3️⃣ ] 파서 (Parser)

스캐너가 소스 파일을 토큰으로 나눠주면, 파서는 그 토큰을 활용해 AST를 생성한다.

  • AST : 컴파일러가 동작하는 데 핵심 기반이 되는 자료구조. 소스코드의 구조를 트리형으로 표현
  • AST의 최상위 노드는 TS 소스 파일이며, 최하위 노드는 파일의 끝 지점으로 구성된다.
  • 각각의 노드는 코드상의 위치, 구문 종류, 코드 내용과 같은 정보를 담고 있다.
function normalFunction() {
    console.log("normalFunction");
}

normalFunction();

위 예제코드는 위와같은 구조로 AST를 구성한다.

4️⃣ ] 바인더 (Binder)

바인더의 주요 역할은, 체커(checker) 단계에서 타입 검사를 할 수 있도록 기반을 마련하는 것

  • 바인더 : 타입 검사를 위해 심볼(Symbol)이라는 데이터 구조를 생성한다.
  • 심볼 : 이전 단계의 AST에서 선언된 타입의 노드 정보를 저장한다.
export interface Symbol {
    flags: SymbolFlags; // Symbol flags

    escapedName: string; // Name of symbol

    declarations?: Declaration[]; // Declarations associated with this symbol

    // 이하 생략...
}

심볼의 인터페이스 일부 코드이다.

  • flags 필드는 AST에서 선언된 타입의 노드 정보를 저장하는 식별자이다.
  • 심볼을 구분하는 식별자 목록은 아래와 같다.
// src/compiler/types.ts
export const enum SymbolFlags {
    None = 0,
    FunctionScopedVariable = 1 << 0, // Variable (var) or parameter
    BlockScopedVariable = 1 << 1, // A block-scoped variable (let or const)
    Property = 1 << 2, // Property or enum member
    EnumMember = 1 << 3, // Enum member
    Function = 1 << 4, // Function
    Class = 1 << 5, // Class
    Interface = 1 << 6, // Interface
    // ...
}
  • 심볼 인터페이스의 declarations 필드는 AST 노드의 배열 형태를 보인다.
  • 결과적으로 바인더는 심볼을 생성하고, 그에 대응하는 AST 노드를 연결하는 역할을 수행한다.

위 사진은, 여러 선언 요소에 대한 심볼이다.

5️⃣ ] 체커(Checker)와 이미터(Emitter)

체커

체커는 파서가 생성한 AST와 바인더가 생성한 심볼을 활용하여 타입 검사를 수행한다.

  • 풀어서 설명하자면
  • AST의 노드를 탐색하며 심볼 정보를 불러와 주어진 소스 파일에 대해 타입 검사를 수행

참고) 파서의 소스 크기 : 500 KB, 체커의 소스 크기 : 2.7 MB로 전체 컴파일 과정에서 타입 검사가 차지하는 비중이 크다는 것을 짐작할 수 있음

이미터

타입스크립트 소스 파일을 변환하는 역할을 한다.

즉, TS 소스를 자바스크립트(js) 파일과 타입 선언 파일(d.ts)로 생성한다.

  • 이미터는 TS 소스 파일을 변환하는 과정에서 개발자가 설정한 TS 설정파일을 읽어오고
  • 체커를 통해 코드에 대한 타입 검증 정보를 가져온다.
  • 그리고 emitter.ts 소스 파일 내부의 emitFiles() 함수를 사용하여 타입스크립트 소스 변환을 진행한다.

타입스크립트의 컴파일 과정 정리

  1. tsc 명령어를 실행하여 프로그램 객체가 컴파일 과정을 시작한다.
  2. 스캐너는 소스 파일을 토큰 단위로 분리한다.
  3. 파서는 토큰을 이용하여 AST를 생성한다.
  4. 바인더는 AST의 각 노드에 대응하는 심볼을 생성한다. 심볼은 선언된 타입의 노드 정보를 담고 있다.
  5. 체커는 AST를 탐색하면서 심볼 정보를 활용하여 타입 검사를 수행한다.
  6. 타입 검사 결과 에러가 없다면 이미터를 사용해서 자바스크립트 소스 파일로 변환한다.

읽어보면 좋은 글

https://velog.io/@sehyunny/how-ts-compiler-compiles

profile
새로운 자극을 주세요.

1개의 댓글

comment-user-thumbnail
2025년 2월 15일

글 잘 읽었습니다!
한가지 잘못된 점을 발견해서 제보드립니다.
"TS는 고수준 언어가 고수준 언어(JS)로 변환되는 것이기에, 컴파일이 아닌 트랜스파일(Transpile)이라고 부르기도 한다."
이 부분을 "TS는 고수준 언어가 고수준 언어(JS)로 변환되는 것이기에, 트랜스파일(Transpile)이 아닌 컴파일이라고 부르기도 한다."
으로 수정해야 할 것 같아요

답글 달기

관련 채용 정보