[tsconfig의 모든 것] Compiler options / Emit

    [tsconfig의 모든 것] Compiler options / Emit


    이번 포스팅에서는 지난 [tsconfig의 모든 것] Compiler options / Modules 포스팅에 이어 tsconfig의 컴파일 옵션 중 출력 파일을 컨트롤 하는 옵션들을 소개할 예정이다.

    이 옵션들은 타입스크립트에서만 지원되는 문법들을 자바스크립트로 어떻게 표현할 것인지, 혹은 ES6 이상의 문법을 ES5로 트랜스파일링할 때 어떻게 표현할 것인지와 같이 타입스크립트로 작성된 코드를 컴파일한 이후에 생성되는 자바스크립트 코드의 모습을 결정하는 옵션들이다.

    물론 Babel과 같은 트랜스파일러가 제공해주는 기능과 중복되는 부분이 있기 때문에, TSC와 Babel을 함께 사용하는 경우에는 tsconfig에서 모든 옵션을 섬세하게 설정해주지 않아도 되는 경우도 있다.

    declaration

    설명
    true 컴파일 할 때 타입 선언 파일도 함께 생성한다
    false (default) 컴파일 할 때 자바스크립트 파일만을 생성한다

    declaration 옵션은 타입스크립트의 *.d.ts 파일을 내보낼지 말지를 결정하는 옵션이다. 만약 기본적으로 이 옵션은 꺼져있기 때문에, 별도의 설정 없이 타입스크립트를 컴파일하게 되면 *.js 파일만 덩그러니 생성되는 모습을 볼 수 있다.

    // math.ts
    export const add = (x: number) => (y: number) => x + y;
    // math.js
    export var add = function (x) { return function (y) { return x + y; }; };

    하지만 declaration 옵션을 켜게 되면, 생성된 자바스크립트 파일 외에도 타입 선언을 담고 있는 *.d.ts 파일을 함께 생성하게된다.

    // math.d.ts
    export declare const add: (x: number) => (y: number) => number;

    만약 여러분이 만든 모듈이 타입스크립트를 지원하도록 하고 싶다면, 사용자가 직접 모듈의 소스코드를 가져올 수 있도록 허용하거나 자바스크립트 파일을 제공하되 타입 선언 파일인 *.d.ts 파일을 함께 제공해줘야 하기 때문에, 타입스크립트 대상의 라이브러리를 개발한다면 이 옵션을 사용하여 컴파일 시 타입 선언 파일까지 함께 생성하는 것을 추천한다.

    declarationDir

    타입 설명
    string 타입 선언 파일을 내보낼 디렉토리의 경로

    declarationDir 옵션은 이름 그대로 타입 선언 파일을 내보낼 경로를 설정할 수 있는 옵션이다. 이때 .가 의미하는 현재 경로는 tsconfig 파일이 위치한 곳을 의미하기 때문에, 만약 컴파일된 파일들이 위치한 디렉토리 안 쪽에 타입 선언 파일을 내보내고 싶다면 ./types와 같은 경로가 아닌, ./{outDir}/types와 같이 디렉토리를 직접 지정해줘야 한다.

    만약 declarationDir 옵션을 별도로 설정해주지 않는다면 타입 선언 파일은 자신의 원본 자바스크립트 파일과 동일한 위치에 생성된다.

    declarationDir 옵션을 설정하지 않은 경우
    
    myProject
    ├── math.ts
    ├── dist
    │   ├── math.d.ts <
    │   └── math.js
    └── tsconfig.json
    declarationDir 옵션을 "./dist/types"로 설정한 경우
    
    myProject
    ├── math.ts
    ├── dist
    │   ├── math.js
    │   └── types
    │       └── math.d.ts <
    └── tsconfig.json

    이렇게 하나의 디렉토리에 타입 선언 파일을 모아두게 되면 추후 package.jsontypes 프로퍼티를 사용하여 편하게 해당 패키지의 타입 선언 파일들의 위치를 지정할 수 있으므로 필자는 declarationDir 옵션을 사용하여 타입 선언을 한 곳에 모아두는 편이다.

    declarationMap

    설명
    true 타입 선언과 소스 코드를 연결하는 매핑 파일을 생성한다
    false (default) 매핑 파일을 생성하지 않는다

    declarationMap 옵션은 개발자가 IDE에서 제공하는 “Go to Definition” 같은 네비게이션 기능을 통해 원본 소스 파일로 이동할 수 있도록 도와주는 맵핑 파일을 함께 생성할 것인지에 대한 여부를 결정한다.

    앞서 알아보았듯이 declaration 옵션을 사용하여 타입 선언 파일을 생성하게 되면 다음과 같은 결과물이 컴파일된다.

    // math.ts (원본 소스 파일)
    export const add = (x: number) => (y: number) => x + y;
    // math.js (자바스크립트)
    export var add = function (x) { return function (y) { return x + y; }; };
    // math.d.ts (타입 선언)
    export declare const add: (x: number) => (y: number) => number;

    이때 실제로 실행되는 코드를 담고 있는 자바스크립트 코드와 타입 선언을 담고 있는 타입 선언 파일의 연관 관계를 정의한 매핑 파일이 없다면, IDE에서 네비게이팅 기능을 사용했을 때 소스 코드가 아닌, math.d.ts 파일의 타입 선언으로 이동하게 된다.

    하지만 어차피 개발자 자신의 소스 코드에도 타입 선언에 대한 정보는 다 노출되고 있기 때문에, 네비게이팅을 사용하는 경우는 타입 선언 정의를 알고 싶다기 보다는 실제로 함수의 내부 구현을 보고 싶은 경우가 대부분일 것이다.

    이런 상황에서 declarationMap 옵션을 사용하면 개발자가 네비게이팅 기능을 사용했을 때 타입 선언이 아닌 소스 코드로 이동할 수 있도록 별도의 매핑 파일을 함께 생성해줄 수 있다.

    // math.d.ts (타입 선언)
    
    export declare const add: (x: number) => (y: number) => number;
    //# sourceMappingURL=math.d.ts.map
    // math.d.ts.map
    {
      "version":3,
      "file":"math.d.ts",
      "sourceRoot":"",
      "sources":["../../../utils/math.ts"],
      "names":[],
      "mappings":"AAAA,eAAO,MAAM,GAAG,MAAO,MAAM,SAAS,MAAM,WAAU,CAAC"
    }

    매핑 파일은 JSON 포맷으로 구성되어 있으며, 타입 선언이 정의된 파일과 같은 경로에 생성된다. 이 매핑 파일은 원본 소스 코드의 경로를 가지고 있기 때문에, IDE의 네비게이팅 기능을 사용했을 때 “소스코드는 여기가 아니라 이 경로에 있음”이라고 알려줄 수 있는 것이다.

    즉, 이 기능을 100% 활용하고 싶다면 npm 레지스트리에 모듈을 배포할 때 반드시 소스 코드가 함께 포함되어야 한다. 일반적으로 npm 레지스트리에 배포할 때 소스코드가 아닌 빌드 결과물만을 배포하는 경우가 많은데, 이렇게 되면 어차피 라이브러리 내에 소스코드가 포함되어 있지 않기 때문에 매핑 파일을 함께 넣어줘도 의미가 없어지는 것이다.

    단순히 npm 레지스트리에 소스코드까지 포함해서 배포한다고 해서 내가 만든 모듈을 사용한 어플리케이션의 번들 사이즈가 늘어나는 것도 아니니, 내가 만든 라이브러리를 사용하는 개발자들의 생산성과 편의성을 많이 높혀주기 위해 declarationMap을 켜고 소스코드까지 포함해서 npm 레지스트리에 배포하는 것을 추천한다.

    downlevelIteration

    설명
    true ES6에 추가된 이터레이션 기능에 대한 명확한 구현을 함께 생성한다
    false (default) 기본적인 트랜스파일링만을 수행한다

    downlevelIteration 옵션은 타입스크립트가 ES6에서 추가된 for/of, Spread, Symbol.iterator 등의 기능을 보다 명확하게 트랜스파일링을 하도록 만들 수 있는 옵션이다.

    컴파일 타겟인 자바스크립트 버전이 ES6 이상이라면 이 옵션은 크게 의미가 없지만, 크로스 브라우징 등을 위해 ES5 이하의 버전을 컴파일 타겟으로 삼는 경우에는 자바스크립트가 실행되는 런타임 때 이터레이터들이 개발자의 의도와 다르게 동작하는 것을 방지할 수 있다.

    예를 들어 for/of를 사용한 타입스크립트 코드를 ES5로 트랜스파일링한다면, 아래와 같은 결과물을 만나볼 수 있다.

    const str = 'Hello!';
    for (const s of str) {
      console.log(s);
    }
    'use strict';
    var str = 'Hello!';
    for (var _i = 0, str_1 = str; _i < str_1.length; _i++) {
      var s = str_1[_i];
      console.log(s);
    }

    for/of는 ES5에는 없는 기능이므로 타입스크립트는 for/of를 일반적인 for문으로 트랜스파일링 한 것이다.

    여기까지 보면 별로 문제가 없는 것 같지만, 사실 이렇게 트랜스파일링된 코드는 원본 코드와 정확히 일치하는 동작을 보여주지는 않는다. 바로 이런 코드 때문이다.

    const str = '🙏';
    for (const s of str) {
      console.log(s);
    }

    위 코드에서 사용된 🙏 이모지는 필자가 개인적으로도 아주 애용하고 있는 이모지이다. 단순히 눈에 보이는 문자의 수가 한 개이기 때문에 이 이모지의 길이도 당연히 1이라고 생각할 수 있지만, 사실 이모지들의 길이는 1이 아니다.

    '🙏'.length // 2
    '👩‍❤️‍💋‍👩'.length // 11

    즉, 이 이모지의 길이를 사용하여 일반적인 for 문을 작성하게 되면 제대로 된 문자를 뽑아내기가 어려울 수 있다는 것이다. 무슨 말인지 잘 이해가 안 된다면, 아래 코드를 크롬 개발자도구에 복붙해서 한번 실행시켜보자.

    const str = "🙏";
    
    for (let i = 0; i < str.length; i++) {
      console.log(`for문 > ${str[i]}`);
    }
    
    for (const s of str) {
      console.log(`for/of문 > ${s}`);
    }
    for문 > �
    for문 > �
    
    for/of문 > 🙏

    이것이 바로 for/of 문을 그냥 for문으로 트랜스파일링하면 안 되는 이유이다.

    이처럼 이모지의 길이가 1이 아닌 이유를 간단히만 설명하자면, 일단 이모지가 멀티바이트 문자이기 때문이기도 하고 계속 해서 새로운 이모지가 추가되기도 하고 기존 이모지들을 결합한 이모지들이 나오면서 이모지를 표현하는 방법이 괴랄해져서 그렇기도 하다. 이모지 길이에 대한 자세한 내용은 이 포스팅에 잘 설명되어있으니 한번 읽어보도록 하자.

    어찌됐던 여기서 중요한 포인트는 for/offor의 동작이 유사하다고 해서 묻어놓고 트랜스파일링을 했다가는 이런 참사가 발생할 수도 있다는 것이다.

    그래서 타입스크립트는 downlevelIteration이라는 옵션을 별도로 제공해서 for/of, Spread, Symbol.iterator와 같은 이터레이션 기능이 개발자의 의도와 다르게 동작하지 않도록 Symbol.iterator 같은 기능이 있는지 검사하거나, 아예 이런 기능을 구현해놓은 폴리필까지 함께 추가할 수 있도록 만들어 두었다.

    emitBOM

    설명
    true 타입스크립트가 출력 파일을 생성할 때 BOM을 표시한다
    false (default) 타입스크립트가 출력 파일을 생성할 때 BOM을 표시하지 않는다

    emitBOM 옵션은 타입스크립트가 출력 파일을 생성할 때 BOM(Bite Order Mark)를 표시할지 말지를 결정할 수 있는 옵션이다.

    Bite Order Mark는 특별한 유니코드를 파일의 가장 앞 부분에 추가해서 이 파일이 어떤 인코딩 방식을 사용했는지를 나타내는 방법이다.

    BOM은 애초에 사람이 읽을 목적이 아니라 컴퓨터에게 현재 파일의 인코딩 정보를 알리기 위해서만 사용하기 때문에, 텍스트 에디터나 vim 같은 곳에서 파일을 열어보아도 BOM을 보여주지는 않는다.

    그러나 일반적으로 자바스크립트가 실행되는 런타임 환경에서 굳이 BOM까지 필요한 경우가 흔치 않기도 하고, 유니코드 3.2부터는 BOM을 사용하지 않을 것을 권장하고 있기도 해서, 굳이 켤 필요가 없는 옵션이기도 하다. (타입스크립트 공식 문서에서도 굳이 안 켜도 된다고 하고 있다)

    emitDeclarationOnly

    설명
    true 자바스크립트 없이 타입 선언 파일만을 출력한다
    false (default) 자바스크립트 파일을 포함하여 출력한다

    emitDeclarationOnly 옵션은 Declaration only라는 이름 그대로, 컴파일을 진행할 때 자바스크립트 파일 없이 타입 선언 파일만을 출력할지에 대한 여부를 결정하는 옵션이다.

    보통 타입스크립트를 자바스크립트로 변환할 때 타입스크립트 컴파일러가 아닌 별도의 도구를 사용하는 경우나, 기존에 자바스크립트로 만들어진 모듈에 타입 선언만을 제공해야하는 경우에 사용하게 된다.

    importHelpers

    설명
    true 출력 파일 내에서 tslib가 제공하는 헬퍼 함수들을 사용한다
    false (default) tslib가 제공하는 헬퍼 함수를 사용하지 않고 직접 헬퍼를 구현하도록 한다

    importHelpers 옵션은 타입스크립트를 자바스크립트 ES5 이하의 버전으로 트랜스파일링할 때 발생하는 헬퍼 함수들을 출력 파일 내에 직접 작성할 것이냐, 아니면 tslib 라이브러리가 제공하는 헬퍼 함수로 대체할 수 있도록 할 것이냐를 결정할 수 있는 옵션이다.

    한번 소스 코드와 출력 파일을 함께 보면서 이해해보도록 하자.

    export function fn(arr: number[]) {
      return [1, ...arr];
    }

    fn 함수는 인자로 받은 배열의 맨 앞에 1이라는 원소를 추가해서 반환하는 간단한 함수이다. 여러분도 아시다시피 [...arr]라는 문법으로 사용할 수 있는 Spread 기능은 ES5에 포함되어있지 않으므로, 타입스크립트는 __spreadArray 라는 헬퍼 함수를 출력 파일 내에 추가하여 Spread 기능을 트랜스파일링한다.

    var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
      if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
        if (ar || !(i in from)) {
          if (!ar) ar = Array.prototype.slice.call(from, 0, i);
          ar[i] = from[i];
        }
      }
      return to.concat(ar || Array.prototype.slice.call(from));
    };
    export function fn(arr) {
      return __spreadArray([1], arr, true);
    }

    여기서 중요한 것은 ES6의 Spread 기능을 구현하기 위해 __spreadArray라는 함수의 구현이 출력 파일 내에 함께 포함되었다는 것이다. 사실 이런 구현이 출력 파일내에 포함되는 것이 큰 문제는 아닐 수 있지만, 만약 저 __spreadArray를 다른 곳에서 또 사용해야하는 경우에는 그 모듈에 또 다시 __spreadArray 함수의 구현이 추가되기 때문에 중복된 코드가 발생하게 된다.

    이때 importHelpers 옵션을 사용하게 되면 이러한 헬퍼 함수들을 tslib 라이브러리에서 가져오도록 변경하여 코드의 중복을 제거할 수 있다.

    import { __spreadArray } from 'tslib';
    export function fn(arr) {
      return __spreadArray([1], arr, true);
    }

    이렇게 되면 __spreadArray 함수를 tslib에서 불러와서 사용하게 되므로, 매번 __spreadArray 함수를 선언할 필요가 사라지기 때문에 코드의 중복을 제거할 수 있다. 하지만 이 옵션을 사용하여 트랜스파일링된 코드는 당연히 tslib 패키지를 설치되어있어야 제대로 작동하므로, 헬퍼 함수의 중복된 구현을 허용하는 것과 tslib를 내 어플리케이션에 설치하는 것의 득과 실을 잘 따져보고 옵션을 사용하도록 하자.

    importsNotUsedAsValues

    설명
    remove 출력 파일에서 런타임 때 필요없는 import 문을 제거한다
    preserve 출력 파일에서 타입 정보는 제거하되, import 문은 유지한다
    error 타입 정보만 가져오는 import 문을 사용했을 때 에러를 발생시킨다

    importsNotUsedAsValues 옵션은 타입스크립트가 출력 파일을 생성할 때 필요없는 import 구문을 처리하는 방법을 제어할 수 있는 옵션이다. 이 옵션이 가지는 의미를 알기 위해서는 타입스크립트가 import 문을 처리하는 방법에 대해서 조금 알아야 한다.

    기본적으로 타입스크립트가 컴파일을 통해 자바스크립트 파일을 생성할 때, 타입과 관련된 정보는 지워버린다. 코드를 보면서 한번 이해해보도록 하자.

    import { InterfaceFoo } from '../utils/foo';
    import { ClassBar } from '../utils/bar';
    
    const classBar: InterfaceFoo = new ClassBar();
    import { ClassBar } from '../utils/bar';
    var classBar = new ClassBar();

    위 코드를 보면 InterfaceFoo 인터페이스를 가져오는 import 문이 자바스크립트 출력 파일 내에서는 사라진 것을 볼 수 있다. 왜냐하면 자바스크립트에는 타입스크립트의 타입이나 인터페이스 같은 기능이 없으니, 타입 정보를 남겨놔봤자 의미가 없기 때문이다.

    물론 앞서 이야기한대로 타입 정보는 어차피 자바스크립트에서 사용할 수 없으니, 이런 정보는 제거하는 것이 맞지만, 만약 ../utils/foo 모듈이 의도적인 사이드이펙트를 포함하고 있을 경우에는 문제가 발생할 수 있다.

    // utils/foo.ts
    export interface InterfaceFoo {}
    console.log('hello world!');

    utils/foo.ts 모듈은 인터페이스만을 노출하고 있는 모듈이지만, 내부에는 console.log라는 사이드 이펙트를 포함하고 있다. 이 예시에서는 단순한 콘솔 출력이지만, 실제로 Angular 같은 경우는 이런 모듈 내부에서 명시적으로 모듈을 주입하고 등록하는 사이드 이펙트가 포함되기도 한다.

    문제는 이런 경우에도 타입스크립트는 자바스크립트를 출력할 때 utils/foo 모듈을 import 했던 구문 자체를 지워버린다는 것이다. 그러면 당연히 console.log라는 사이드 이펙트는 자바스크립트 런타임에서 실행되지 않는다. 그래서 이런 경우 개발자들은 의도적으로 사이드 이펙트를 실행시키기 위해 타입스크립트를 속일 수 있는 import 문을 하나 더 추가해야한다.

    import { InterfaceFoo } from '../utils/foo';
    import '../utils/foo';
    import { ClassBar } from '../utils/bar';
    
    const classBar: InterfaceFoo = new ClassBar();
    import './utils/foo';
    import { ClassBar } from "../utils/bar";
    var classBar = new ClassBar();

    그래서 타입스크립트 3.8버전에는 “이 import문이 타입 정보만을 가져오는 구문이다”라는 것을 명시적으로 표현할 수 있는 import type 기능을 추가했고, 혹여나 일반적인 import 문을 사용하여 타입 정보만을 가져오더라도 import문을 남겨둘 수 있도록 제어할 수 있는 importsNotUsedAsValues 옵션을 제공하는 것이다.

    inlineSourceMap

    설명
    false 소스맵 파일을 따로 생성한다
    true 소스맵 파일의 내용을 Base64로 인코딩하여 소스 파일에 추가한다

    inlineSourceMap 옵션은 타입스크립트가 컴파일 시 어떤 방식으로 소스맵을 생성할 것인지를 결정하는 옵션이다. 기본적으로 타입스크립트는 *.js.map 파일의 형태로 소스맵을 제공하는데, 만약 inlineSourceMaptrue일 경우에는 소스 파일 내부에 주석으로 소스맵을 추가한다.

    간단한 함수를 가지고 있는 모듈을 직접 컴파일해보며 어떤 차이가 있는지 직접 알아보도록 하자.

    // math.ts
    export const add = (x: number) => (y: number) => x + y;

    만약 inlineSourceMap 옵션이 꺼져 있는 경우, 타입스크립트는 별도의 소스맵 파일을 생성하고, 컴파일된 JS 파일에는 해당 소스맵의 경로만을 적어두는 형태로 소스맵을 생성한다.

    // math.js
    export var add = function (x) { return function (y) { return x + y; }; };
    //# sourceMappingURL=math.js.map
    // math.js.map
    {"version":3,"file":"math.js","sourceRoot":"","sources":["../../utils/math.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,IAAM,GAAG,GAAG,UAAC,CAAS,IAAK,OAAA,UAAC,CAAS,IAAK,OAAA,CAAC,GAAG,CAAC,EAAL,CAAK,EAApB,CAAoB,CAAC"}

    이때 컴파일된 JS 파일에는 소스맵 파일의 경로가 #sourceMappingURL=math.js.map의 형태로 추가되고, 소스맵 파일에는 "sources": ["../../utils/math.ts"]처럼 원본 타입스크립트 소스 파일의 경로가 적혀있는 것을 확인할 수 있다.

    만약 inlineSourceMap 옵션을 켜게되면 이제 타입스크립트는 소스맵 파일을 생성하지 않고, 컴파일된 JS 파일 내에 직접 소스맵 파일의 내용을 Base64로 인코딩해서 추가한다.

    export var add = function (x) { return function (y) { return x + y; }; };
    //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWF0aC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3V0aWxzL21hdGgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsTUFBTSxDQUFDLElBQU0sR0FBRyxHQUFHLFVBQUMsQ0FBUyxJQUFLLE9BQUEsVUFBQyxDQUFTLElBQUssT0FBQSxDQUFDLEdBQUcsQ0FBQyxFQUFMLENBQUssRUFBcEIsQ0FBb0IsQ0FBQyJ9

    inlineSources

    설명
    false 인라인 소스맵에 소스 코드의 내용은 포함시키지 않는다.
    true 인라인 소스맵에 소스 코드의 내용도 함께 포함시킨다

    inlineSources 옵션은 inlineSourceMap 옵션을 사용하여 만들어낸 인라인 소스맵에 원본 소스 코드의 내용도 함께 포함시킬 것인지 여부를 결정한다.

    이 옵션의 값이 true인 경우, 인라인 소스맵에 sourceContent라는 필드가 추가되고 해당 필드에는 소스 코드의 내용이 함꼐 포함된다.

    export var add = function (x) { return function (y) { return x + y; }; };
    //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWF0aC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3V0aWxzL21hdGgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsTUFBTSxDQUFDLElBQU0sR0FBRyxHQUFHLFVBQUMsQ0FBUyxJQUFLLE9BQUEsVUFBQyxDQUFTLElBQUssT0FBQSxDQUFDLEdBQUcsQ0FBQyxFQUFMLENBQUssRUFBcEIsQ0FBb0IsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbImV4cG9ydCBjb25zdCBhZGQgPSAoeDogbnVtYmVyKSA9PiAoeTogbnVtYmVyKSA9PiB4ICsgeTtcbiJdfQ==
    // Decoding된 소스맵
    
    {
      "version":3,
      "file":"math.js",
      "sourceRoot":"",
      "sources":["../../utils/math.ts"],
      "names":[],
      "mappings":"AAAA,MAAM,CAAC,IAAM,GAAG,GAAG,UAAC,CAAS,IAAK,OAAA,UAAC,CAAS,IAAK,OAAA,CAAC,GAAG,CAAC,EAAL,CAAK,EAApB,CAAoB,CAAC",
      "sourcesContent":["export const add = (x: number) => (y: number) => x + y;\n"]
    }

    애초에 소스맵의 스펙sourceContent 필드는 원본 소스 파일에 접근하지 못한 경우를 대응하기 위한 일종의 예외처리에 가까우므로, 굳이 켜지 않아도 큰 문제가 없는 옵션이기도 하다.

    noEmit

    설명
    false 컴파일 후에 출력 파일을 내보낸다
    true 컴파일 후에 출력 파일을 내보내지 않는다

    noEmit 옵션은 이름 그대로 타입스크립트가 컴파일을 한 이후에 출력 파일들을 내보낼 것인지에 대한 동작을 결정한다. 설명만 들어보면 이런 경우가 필요할까 싶기는 한데, 생각보다 유용하게 자주 쓰이는 옵션이다.

    대표적인 예로는 CI나 Git Hook 같은 타이밍에 정적 타입 체크만 진행해야 하는 경우 tsc --noEmit와 같은 명령어를 NPM 스크립트로 등록해놓거나, Webpack, Parcel, Rollup 등의 도구를 사용하여 컴파일을 진행할 때도 정적 타입 체크만을 TSC에게 시키기 위해 해당 옵션을 사용하는 경우가 많다.

    noEmitHelpers

    설명
    false 컴파일된 파일에 __awaiter와 같은 헬퍼 함수를 포함시킨다
    true 컴파일된 파일에는 __awaiter와 같은 헬퍼 함수를 포함시키지 않는다

    noEmitHelpers 옵션은 컴파일이 완료된 출력 파일에 __awaiter__generator와 같은 헬퍼 함수들을 포함시킬지 여부를 결정한다.

    noEmitHelpers 옵션의 동작이 importHelpers와 유사하기 때문에 조금 헷갈릴 수 있다. importHelpers 옵션은 헬퍼 함수들의 구현을 “소스에 포함할지”, “다른 곳에서 import할지”를 결정한다면, noEmitHelpers 옵션은 헬퍼 함수들의 구현을 “소스에 포함할지”, “아예 하지 않을 것인지”를 결정하기 때문이다.

    이 차이점을 직접 컴파일된 코드를 보면서 알아보도록 하자.

    // importHelpers, noEmitHelpers가 모두 꺼져있는 경우에는 출력 파일에 헬퍼의 구현도 포함된다
    
    var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
      // ...
    };
    var __generator = (this && this.__generator) || function (thisArg, body) {
      // ...
    };
    
    export function foo() {
      return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) {
        return [2 /*return*/];
      }); });
    }
    // importHelper가 켜져있는 경우에는 외부에서 헬퍼를 가져온다
    
    import { __awaiter, __generator } from "tslib";
    export function foo() {
      return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) {
        return [2 /*return*/];
      }); });
    }
    // noEmitHelpers가 켜져있는 경우에는 아예 헬퍼를 가져오거나 선언하는 코드 조차 없다
    
    'use strict';
    export function foo() {
      return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) {
        return [2 /*return*/];
      }); });
    }

    만약 importHelpersnoEmitHelpers 옵션이 모두 켜져있다면 헬퍼 함수를 “외부에서 가져올 것이냐”, “출력 파일에 포함시키지 않을 것이냐”라는 설정이 충돌하게 되는데, 이 경우에는 importHelpers의 동작을 우선적으로 따르게 되니 이 점을 주의하도록 하자.

    noEmitOnError

    설명
    false 컴파일 중 에러가 발생하더라도 출력 파일을 내보낸다
    true 컴파일 중 에러가 발생한 경우 출력 파일을 내보내지 않는다

    noEmitOnError 옵션은 이름 그대로 컴파일 중 여러가지 요인으로 인해 에러가 발생하게 되는 경우 출력 파일을 내보낼 것인지에 대한 여부를 결정한다. 만약 이 옵션이 켜져있는 경우, 컴파일 중 에러가 발생하면 출력 파일이 내보내지지 않는다.

    이렇게만 보면 “당연히 에러가 나면 출력 파일을 안 만드는 게 맞지 않나?”라고 생각할 수 있지만, TSC의 Watch 옵션을 사용하고 있는 개발환경 같은 경우는 컴파일 중 에러가 발생해도 출력 파일을 생성하고 그로 인해 발생하는 사이드 이펙트 또한 함께 관찰하는 것이 더 편할 수도 있기 때문에, 개발환경에서는 해당 옵션을 끄고 운영환경 배포를 위한 빌드 타임 때는 켜놓는 것을 추천한다.

    preserveConstEnums

    설명
    false const enum 키워드를 사용한 Enum 선언을 컴파일 타임 때 제거한다
    true const enum 키워드를 사용한 Enum 선언을 컴파일 타임 때 제거하지 않는다

    preserveConstEnums 옵션은 컴파일 타임 때 const enum 키워드를 사용한 Enum 선언을 제거할 것인지에 대한 옵션이다. 타입스크립트는 런타임 때의 메모리 비용을 절약하기 위해 const enum 키워드로 선언한 Enum의 값을 참조하는 부분을 해당 Enum의 값으로 치환한다.

    enum 키워드만을 사용하여 선언한 Enum과 다르게 const enum 키워드를 사용한 Enum은 반드시 상수 값만 가지는 것이 보장되기 때문에 참조 투명성 또한 보장되고, 결국 컴파일 타임 때 값을 그냥 치환해버려도 아무 문제가 없는 것이다.

    const enum Foo {
      A = 1,
      B = 2,
      C = 3,
    }
    const foo = Foo.A;
    var Foo;
    (function (Foo) {
        Foo[Foo["A"] = 0] = "A";
        Foo[Foo["B"] = 1] = "B";
        Foo[Foo["C"] = 2] = "C";
    })(Foo || (Foo = {}));
    var foo = 1 /* A */; // <= Foo.A가 아닌, 값으로 치환되었다

    이때 컴파일된 코드를 자세히 보면, Enum을 표현하기 위해 Foo라는 변수를 선언하고 IIFE를 사용하여 해당 변수에 값을 할당하고 있지만, 정작 이 변수에 접근하는 부분은 없는 것을 알 수 있다. 즉, 이 JS 코드가 실행되는 런타임 환경에서는 Foo라는 변수가 없어도 아무 문제가 없다는 것이다.

    이때 preserveConstEnums 옵션의 값을 false로 설정하면 이렇게 런타임에서는 아무 의미없는 Enum 선언을 제거할 수 있다.

    var foo = 1 /* A */;

    마치며

    이렇게 tsconfig 4번째 시리즈인 Emit 편을 마무리했다. Emit과 관련된 역할을 하는 옵션들은 주로 TSC와 다른 도구를 결합하여 사용하거나, 번들 사이즈를 최적화해야하는 경우에 주로 건드리게 되는데, 필자 또한 꽤나 오랜만에 이 옵션들을 공부하게 되었던 지라 예전에 써본 옵션들이 있음에도 불구하고 기억이 가물가물 했던 것 같다.

    이상으로 [tsconfig의 모든 것] Compiler options / Emit 포스팅을 마친다.

    Evan Moon

    🐢 거북이처럼 살자

    개발을 잘하기 위해서가 아닌 개발을 즐기기 위해 노력하는 개발자입니다. 사소한 생각 정리부터 튜토리얼, 삽질기 정도를 주로 끄적이고 있습니다.