Typescript compile process

목차

개요

타입스크립트가 등장하기 전 많은 프로젝트에 자바스크립트를 사용했습니다. 자바스크립트의 동적 타이핑은 자바와 다르게 개발자가 아주 유연한 코딩을 할 수 있도록 했지만, 프로젝트가 거대해 지면서 점점 디버깅에 취약하고 가독성이 낮으며 객체 지향을 추구하는 프로젝트에 걸맞지 않는 문제점이 야기되었습니다.
1년간 자바스크립트 기반의 프론트 구조를 가진 프로젝트를 진행하면서 초반에는 그 문제점을 파악하지 못했지만, 점차 서비스의 무게가 커지고 복잡해지면서 몸소 느끼게 되었습니다. 타입을 지정해주면 간단한 방법으로 피할 수 있는 오류를, validation 처리를 통해 불필요한 코드의 길이가 늘어나게 되었습니다. 변수가 어떤 타입을 나타내는지, 함수의 매개변수나 반환 타입을 명시하지 않아 수많은 오류들과 많은 개발자 사이에서 불필요한 커뮤니케이션이 발생했습니다. 사실 이런 문제점은 타입스크립트로 개발했다면 간단한 인터페이스 제공으로 협업이 수월했을 텐데 말이죠.
자바스크립트를 기준으로 한 구조로 타입스크립트를 적용할 수 없었지만, 타입스크립트를 사용했을 때에 어떻게 동작하게 되었을지에 대해 알아보고자 합니다.

이 글은 타입스크립트의 문법보다는 타입스크립트 코드가 어떻게 자바스크립트 파일로 컴파일되는지, 자바스크립트 파일과 타입 파일이 브라우저 상에서 어떻게 동작하는지에 대해 알아보고 있습니다.

Typescript 특징

타입스크립트의 특징은 크게 정적 타입 시스템과 자바스크립트 수퍼셋으로 말할 수 있습니다.

정적 타입 시스템(Static Type System)은 프로그램상 타입이 올바른지 그 검사를 런타임 이전에 시행하는 시스템입니다. 동적 타입 시스템(Dynamical Type System: 런타임에 오류를 검출)인 자바스크립트와는 달리 타입스크립트는 정적 타입 시스템으로 프로그램을 실행시키기 전에 오류를 검출하여 에러를 사전에 방지할 수 있습니다. 이는 보다 안정성 있는 프로그래밍을 할 수 있게 합니다.
아래는 자바스크립트 코드 예시입니다.

const message = ‘hello, world’;
message = 123;

위 코드에서 message 라는 변수가 있다면, 이 코드가 실행되지 않거나 코드 상에 오류를 보여주지 않고 잘 동작하는 것을 볼 수 있습니다. 이런 런타임 시 변수 값에 따라 타입이 정해지는 특성을 동적 타입 시스템이라고 합니다.

타입스크립트는 javascript SuperSet, 즉 자바스크립트의 상위 집합입니다. 타입스크립트로 작성된 코드가 브라우저에서 실행되려면 중간에 변환과정이 필요합니다. 결국 타입스크립트가 컴파일되면 자바스크립트 코드로 변환된다는 것을 알고 있어야 합니다.

또한, 타입스크립트는 타입을 정의할 때, 명시적으로 타입을 정의하기도 하지만 타입을 명시하지 않아도 해당 변수의 타입에 대한 추론으로 타입을 유추할 수 있습니다. 아래는 간단한 예제와 함께 그 개념에 대해 설명하고 있습니다.

let name : string = 'nextree';     // 명시적 타입 정의
name = 11;                         // Type 'number' is not assignable to type 'string'

위 코드를 봤을 때, name은 문자열 타입이라는 것을 알 수 있습니다. 이렇게 명시적으로 변수의 유형을 정의하는 특징을 타입스크립트의 명시적 타입 정의라고 합니다. name이라는 변수의 타입이 문자열로 정의됐기 때문에 name은 문자열만 할당될 수 있는 변수입니다. 코드의 두 번째 라인에서는 number형인 값 11을 할당하고 있습니다. 이 부분에서는 컴파일 오류가 발생할 것입니다. 변수를 선언할 때 지정한 타입과 맞지 않아 할당될 수 없기 때문입니다.
위와 비슷한 변수 하나를 더 정의해 보겠습니다.

let message = 'Hello';
message = 11;   // 컴파일 에러

위 코드는 타입이 명시되지 않았습니다. 이런 경우에는 타입스크립트 내부에서 값을 기준으로 해당 변수 message타입을 추론합니다. 처음 변수에 할당된 값 'Hello'는 문자열입니다. javascript 타입으로는 string으로 말할 수 있습니다.
타입스크립트는 이 초기값의 타입으로 message의 타입을 유추합니다.

Typescript Compile Process

우리가 타입스크립트 코드를 작성하고, 실행되기까지의 과정은 크게 Compile, Bundle, Deploy 3번의 프로세스를 거칩니다. 앞서 작성된 내용과 같이 가장 첫 번째 단계에서 타입스크립트는 자바스크립트 코드로 변환됩니다.

타입스크립트 AST(Abstract Syntax Tree)

첫 번째로 모든 컴파일 과정의 기준이 될 추상 구문 트리를 만들어 냅니다. 여기서 핵심이 될 두 가지는 Scanner와 Parser입니다. 이들은 컴파일 과정에 각 구문을 토큰으로 추출하여 AST로 변환하기까지의 역할을 수행합니다. 추상 구문 트리(Abstract Syntax Tree)는 소스 코드에서 발생되는 구조를 트리 형식으로 나타낸 것입니다. 자세한 코드가 아닌 추상적인 구문으로 나타낸 트리라는 뜻입니다. 구문 트리가 만들어지면, 이를 기준으로 분석, 타입 검사를 합니다.
먼저 Scanner가 타입스크립트로 입력된 코드 문자열을 각각 예약어, 콜론, 부호 등의 토큰으로 분리시키는 역할을 합니다. 위 그림의 Tokens는 Scanner 를 통해 분리된 간단한 토큰들을 보여줍니다. (이 과정을 tokenize라고 합니다.)
Parser는 Scanner가 분리해준 토큰을 구문의 구조에 따라 트리 구조로 만들어내는 역할을 합니다. 그리고 이 코드가 올바른 문법인지 분석하여 구문 오류를 잡아내는 역할도 맡고 있습니다.

타입 검사

타입스크립트 컴파일러의 가장 큰 역할을 하는 타입검사 단계입니다. 이 단계에서는 binder, type system의 핵심이라 할수있는 type checker가 등장합니다.
Binder는 전체 AST를 읽어서 타입 검사에 필요한 데이터를 수집하는 과정입니다. 각 영역 별로 identifier 를 구분하고 식별자가 어느 위치에 정의되었는지 어떤 플래그를 적용할 것인지 선택하여 메타데이터를 만들어 냅니다. 이렇게 설정된 Symbol: primary building block of the TypeScript semantic system은 symbol table에 HashMap으로 저장됩니다. 이는 이후 단계에서 코드 분석을 진행할 때 기준이 됩니다. 아래는 함수와 변수를 symbol로 간단히 표현하고 있습니다.

functionName
FunctionDeclaration
Flags: Function

variableName
VariableDeclaration
Flags: BlockScopedVariable

또한 binder는 Flow nodes로 변수를 추적합니다.
코드의 분기점이 되는 흐름 조건부, 즉 Flow Conditional 을 기준으로 영역을 정하여 Flow Container를 지정합니다.

이미지 참조 How the Typescript Compiler Compiles - understanding the compiler internal

타입스크립트는 검사할 노드로부터 부모 노드를 찾아가며 역방향으로 찾아가며 변수와 타입을 추적할 수 있습니다. 아래는 Typescript Playground에서 지원하는 플러그인으로 확인한 Flow Graph입니다.

TypeChecker는 위에서 만들어 낸 AST와 Symbols table을 기준으로 타입 검사를 시행합니다.
검사는 구조적으로 이루어 지며 외부에서 내부로 진행됩니다. 먼저 이 값이 object 형태인지, 다음은 필드, 마지막으로는 값의 타입이 일치하는지 비교합니다. 이렇게 타입을 검사하는 특징을 타입스크립트의 구조적 타이핑이라고 합니다.

{ age: number } = { age: 'nextree' }

위와 같은 경우에는 값을 비교할 때, TypeFlags.NumberLike‘nextree’TypeFlags.StringLiteral 이므로 false를 반환할 것입니다.
이 비교는 TypeFlags를 사용하는데, 이 Enum은 Typescript 파일의 /src/compiler/types.ts 에 정의되어 있으며, 가능한 모든 타입을 가지고 있습니다.

아래와 같이 타입을 지정하지 않을 경우에 AST의 initializer의 결과 값(‘Hello world’)의 타입을 message의 type 신텍스로 대입합니다.

const message = 'Hello world';

이렇듯 타입스크립트의 타입 추론은 값의 형태에 기반하여 이루어집니다.

transform

타입스크립트 코드가 자바스크립트 코드로 변환하기 위해서는 compile Option이 필요합니다. 우리는 이 옵션들을 tsConfig.json 파일로 관리할 것입니다. 아래는 예시입니다.

{
  "compilerOptions": {
    "module": "system",		// TSC가 컴파일할 대상 모듈 시스템
    "outDir": "dist",		// 생성된 자바스크립트의 디렉터리
    "target": "es2015",		// 컴파일 할 자바스크립트 버전
  },
  "include": ["src/**/*"],  // typescript 가 찾을 디렉터리
}

타입스크립트에는 AST를 파일로 변환하는 역할을 하는 emmiter가 있습니다. emmiter에는 두 가지가 있는데 역할로 나눈 간단한 비교는 아래와 같습니다.

emmiter는 AST와 Checker를 통해 자바스크립트 코드를 만들어 냅니다.
먼저 타입스크립트 AST를 자바스크립트 AST로 변환합니다. 간단하게 타입스크립트 AST에서 타입을 나타내는 Keyword 신텍스를 제거하는 과정이라고 말할 수 있습니다.
그 후, 각 문법 버전에 따라 Asset flag를 지정합니다. transformer는 이렇게 Asset, 자바스크립트 코드, 타입스크립트 코드로 구분하여 처리한 후 자바스크립트 코드 (.js)를 만들어 냅니다. 또한 타입만 정의해 놓은 type definition(.d.ts) 파일도 함께 생성합니다.

// es2015 Asset
const message: string = ‘Hello world’;  // typescript code
console.log(message);                   // javascript code

// 생성된 자바스크립트 코드
"use strict";
const message = 'Hello world';
console.log(message);

// 생성된 DTS 코드
declare const message: string;

.d.ts파일은 컴파일러가 만들어 내기도 하지만 사용자가 정의할 수 있는 파일입니다. 이 파일로 변수 유형을 지정해 놓으면, 컴파일러는 .d.ts파일을 이용해 타입 추론에 참고합니다.

Typescript를 선택하는 이유

컴파일 과정을 알아보니 타입스크립트가 어떻게 동작하는지 그 세부 내용을 알게 되었습니다.
위 정리해 놓은 타입스크립트의 특징과 자바스크립트의 문제점으로 볼수 있듯이, 개발자들은 점점 안정적인 객체 지향적인 개발을 추구하게 되면서 엄격하게 타입을 규정하며 원활한 디버깅이 가능한 타입스크립트를 지향 중입니다. 간단히 설명하자면 타입스크립트는 자바스크립트를 감싸고 있으며, 각 인스턴스에 타입을 줌으로써 보다 가독성 높은 코딩을 가능할 수 있게 합니다.

jyeoni


참조 문서 및 도서