[tsconfig의 모든 것] Root fields

    [tsconfig의 모든 것] Root fields


    필자가 타입스크립트를 사용하게 된 지도 어언 4년 정도가 지났다. 필자는 자바스크립트에는 없던 타입스크립트가 제공하는 강력한 정적 타입 검사 기능에 흠뻑 빠져버렸고, 지금까지 꽤 많은 프로젝트들을 타입스크립트로 작성해왔다.

    그러다가 며칠 전 타입스크립트로 작성한 프로젝트를 새롭게 만들게 되면서 평소처럼 자연스럽게 tsconfig를 세팅하고 있었는데, 뭔가 마음처럼 잘 안되어서 tsconfig 공식 문서를 한참 들여다보던 중 문득 이런 생각이 들었다.

    jordan peele 어...? 내가 이걸 진짜 알고 쓰는 게 맞나...?

    필자만 그런 것일 수도 있지만, 사실 이런저런 프로젝트를 만들다보면 tsconfig를 직접 손으로 한땀 한땀 작성하는 경우가 많지는 않게 되는 것 같다.

    물론 맨 처음 타입스크립트로 프로젝트를 개발했을 때는 손수 정성을 들여 작성했을 것이다. (사실 오래 되서 잘 기억이…) 하지만 언젠가부터는 그냥 기존에 잘 돌아가고 있는 프로젝트의 tsconfig를 복붙해서 새로운 프로젝트에 꽂아넣고 약간의 커스터마이징만 직접 해주는 방식으로 프로젝트를 세팅하고 있었던 것이다.

    그래서 다시 초심으로 돌아가 tsconfig에 있는 옵션들이 내 타입스크립트 프로젝트에 어떤 영향을 주는 지 한번 찬찬히 뜯어보려고 한다.

    사실 tsconfig의 모든 것을 하나의 포스팅으로 풀어내고 싶었지만, 이 놈의 컴파일 옵션들이 워낙 많다보니 포스팅이 500줄이 넘어가는 슬픈 상황을 마주하게 되었으므로, 이번 포스팅에서는 tsconfig의 루트에 있는 필드들에 대해서만 자세히 알아보고, 이후 다른 포스팅에서 컴파일 옵션들에 대한 설명을 이어가도록 하겠다.

    include

    타입 기본 값
    string[] [], ['**/*']
    {
      "include": [
        "src/**/*",
        "tests/**/*",
      ]
    }

    include 필드는 타입스크립트 어플리케이션에 포함할 파일 목록을 선언할 때 사용하며, files 필드의 존재 여부에 따라 기본값이 변경되는데, 만약 필드가 선언되어 있다면 []로, 선언되어 있지 않다면 ["**/*"]으로 기본 값이 설정된다.

    이 필드는 files 필드와 하는 일이 비슷하지만, include 필드에는 glob 문법을 사용하여 포함할 파일 경로의 패턴을 표현할 수 있기 때문에 일반적으로는 files 보다 include를 주로 사용한다.

    glob 문법은 *이나 ? 같은 와일드 카드를 사용하여 여러 파일 이름의 패턴을 나타내게 되는데, 문법 자체는 정규 표현식과 유사하기 때문에 익히기에 크게 어렵지 않다.

    와일드카드 설명 예시 일치하는 파일이름
    * /를 제외한 0번 이상 나타나는 문자를 매칭 types*.ts types.d.ts, types.ts, types.test.ts
    ** /를 포함한 0번 이상 나타나는 문자를 매칭 src/**/index.ts src/index.ts, src/utils/index.ts, src/test/index.ts
    ? 하나의 문자를 매칭, 정규식의 .과 동일 ?at.ts Cat.ts, Bat.ts, Rat.ts
    [ab] [] 안에 있는 문자 중 하나를 매칭 [C|B]at.ts Cat.ts, Bat.ts
    [a-z] [] 안에 있는 문자의 범위 test[0-9].ts test0.ts, test1.ts, test9.ts
    {ab,bc} {} 안에 있는 문자열 중 하나를 매칭 *.{ts,tsx} foo.ts, foo.tsx
    {ab,bc} {} 안에 있는 문자열 중 하나를 매칭 *.{ts,tsx} foo.ts, foo.tsx

    glob 문법은 파일 경로를 통해 원하는 파일을 잡아내야하는 거의 모든 상황에서 유용하게 사용되기 때문에, 한번만 익숙해지고 나면 앞으로 프로그래밍을 하면서 만나게 될 설정 파일들이 조금은 친숙하게 다가올 것이다.

    files

    타입 기본 값
    string[] false
    {
      "files": [
        "./src/index.ts",
        "./src/utils.ts",
        "./src/models.ts"
      ]
    }

    files 필드는 타입스크립트 어플리케이션에 포함할 파일 목록을 명시적으로 선언할 때 사용한다. includes 필드와 비슷하지만, files 필드는 src/**/*와 같은 glob 문법을 사용할 수 없기 때문에, 일일히 파일명을 입력해줘야 한다는 단점이 있다.

    만약 files 필드에 값이 존재한다면 includes 필드의 기본 값은 ['**/*']에서 빈 배열인 []으로 변경된다. 즉, 기존에 includes 필드의 기본 값에 의존하고 있었다면, files 필드에 값을 추가함으로써 컴파일 결과물이 완전히 달라질 수 있으니 이 점을 유의해야 한다.

    exclude

    타입 기본 값
    string[] ['node_modules', 'bower_components', 'jspm_packages']
    {
      "include": [
        "src/**/*"
      ],
      "exclude": [
        "src/**/*.test.ts",
        "node_modules"
      ]
    }

    exclude 필드는 “제외하다”라는 사전적 의미 그대로 include 필드에 선언한 파일들 중에서 “이 파일들은 포함하지마”라는 설정을 할 때 사용한다.

    이 필드의 기본 값인 node_modules, bower_components, jspm_packages 라는 친구들을 보면 알 수 있겠지만, 보통 타입스크립트로 작성된 패키지는 js + *.d.ts 파일들의 조합으로 빌드해서 배포하기 때문에, 타입스크립트는 기본적으로 이런 외부 패키지들이 빌드가 되어있다고 가정하고 있는 것이다.

    만약 외부에서 받아온 패키지도 컴파일이 필요한 상황이라면 exclude 필드를 수정해주면 된다. (참고로 Next.js는 이 필드에 기본적으로 node_modules가 포함되어 있도록 프로젝트를 세팅하고, 이 값을 지워도 빌드할 때 자동으로 다시 넣어버린다…)

    그리고 한 가지 중요한 점은 exlucde 필드는 단지 include 필드에 세팅해준 파일들을 찾아올 때 “이건 찾지마”라는 의미이기 때문에 이 필드에 넣어준 파일들이라고 해서 반드시 내 어플리케이션에 포함되지 않는 것이 아니라는 것이다.

    즉, exlucde 필드에 node_modules 같은 디렉토리가 세팅되어있다고 해도 코드 내에서 import 문을 사용하여 직접 해당 모듈을 가져오는 경우나 /// <reference path="..." /> 처럼 트리플 슬래시 디렉티브를 사용하여 컴파일러에게 특정 모듈을 포함하라고 직접 지시하는 경우, 그리고 files 필드를 사용하여 직접 컴파일할 파일을 명시하는 경우에는 exlucde 필드에 뭘 세팅했던 무시하고 해당 모듈을 가져온다.

    extends

    타입 기본 값
    string false
    // tsconfig.base.json
    {
      "compilerOptions": {
        "noImplicitAny": true,
        "strictNullChecks": true
      }
    }
    // tsconfig.json
    {
      // tsconfig.base.json의 설정을 가져와서 확장한다.
      "extends": "./tsconfig.base.json",
      "files": [
        "./src/main.ts"
      ]
    }

    extends 필드는 이름 그대로 다른 tsconfig의 경로를 지정하면 해당 tsconfig를 상속할 수 있는 필드이다. 필자 같은 경우에는 다른 사람이 미리 만들어 놓은 설정을 가져와서 조금 커스터마이징하는 상황이나, 모노 레포지토리에 포함된 모든 프로젝트에 공통적으로 적용하고 싶은 설정이 있는 경우에 주로 사용했던 것 같다.

    만약 상속 대상인 tsconfig.base.json의 내부 필드에 ./src와 같은 상대 경로가 지정되어 있는 경우에는 해당 상속 파일을 불러온 주체인 tsconfig.json 기준으로 상대 경로가 계산된다.

    또한 이렇게 다른 tsconfig를 상속하여 새로운 tsconfig를 정의하는 경우, files, include, exclude 처럼 배열을 사용하여 선언된 필드들은 확장되는 것이 아니라 필드 자체가 덮어씌워지기 때문에 이 점을 유의해야한다.

    // tsconfig.base.json
    {
      "files": [
        "./src/main.ts"
      ]
    }
    // tsconfig.json
    {
      "extends": "./tsconfig.base.json",
      "files": [
        "./src/index.ts"
      ]
    }

    그러므로 위의 예시 같은 상황이라면 최종 files필드는 ["./src/main.ts", "./src/index.ts"]로 합쳐지는 것이 아니라 ["./src/index.ts"]로 평가된다. extends라는 필드 이름을 보면 왠지 필드의 값도 확장해줄 것 같지만, 아무런 경고나 에러도 없이 그냥 쿨하게 오버라이팅 해버리므로 주의하도록 하자.

    references

    타입 기본 값
    Array<{ path: string; prepend: boolean; }> false
    // my-project/test/tsconfig.json
    {
      "references": [
        // my-project/src/tsconfig.json을 참조한다
        { "path": "../src" }
      ]
    }

    reference 필드는 같은 프로젝트 안에서 모듈 별로 여러 개의 tsconfig를 사용하는 경우, 이 모듈들의 참조 관계를 표현하기 위한 필드이다. 보통 하나의 프로젝트에서는 하나의 tsconfig만 사용하는 경우가 많기 때문에 거의 사용할 일이 없기는 하다.

    사실 타입스크립트가 이런 옵션을 제공하는 이유는 바로 이런 경우 때문이다.

    ├── src/
    │   ├── converter.ts
    │   └── units.ts
    ├── test/
    │   ├── converter-tests.ts
    │   └── units-tests.ts
    └── tsconfig.json

    위 프로젝트는 소스 코드와 테스트 코드가 별도의 디렉토리로 분리되어있고, 루트에는 하나의 tsconfig가 전체 프로젝트의 컴파일 옵션을 관리하고 있다. 이런 구조의 프로젝트는 상당히 흔한 구조이기는 하지만, 막상 이렇게 만들어놓고 개발을 하다보면 몇 가지 불편한 점이 생긴다.

    1. 소스 코드인 src 모듈에서도 테스트 코드인 test 모듈을 불러올 수가 있음…
    2. 소스 코드에서 절대 오류가 발생하지 않는 부분을 수정했는데도 테스트 코드까지 다시 타입 검사해야 함…
    3. 반대로 테스트 코드를 고치면 소스 코드까지 다시 타입 검사해야 함…

    물론 평소라면 어느 정도 감당이 가능한 불편함이기는 하지만, 프로젝트의 크기가 커지면 커질 수록 컴파일러의 타입 검사도 비례해서 느려지기 때문에 어느 순간부터는 개발 자체에 어려움을 느낄 수도 있다. 그렇다면 프로젝트의 모듈마다 tsconfig를 만들어 두고 따로 컴파일하면 어떨까?

    ├── src/
    │   ├── converter.ts
    │   ├── units.ts
    │   └── tsconfig.json
    └── test/
        ├── converter-tests.ts
        ├── units-tests.ts
        └── tsconfig.json

    이렇게 각각의 모듈 별로 tsconfig를 가지도록 구성하면 위에서 이야기했던 문제들은 어느 정도 해결된다. 하지만 이 경우에는 각각의 모듈 별로 따로 컴파일해줘야 하는데다가, 기본적으로 tsc는 하나의 프로세스만 띄울 수 있도록 만들어졌기 때문에 동시에 여러 개의 tsconfig를 토대로 빌드를 하거나 소스 코드의 변경 사항을 감시하는 것이 불가능하다.

    그래서 타입스크립트 팀에서는 references라는 필드로 이 문제를 해결하려고 하는 것이다.

    // my-project/test/tsconfig.json
    {
      "references": [
        // my-project/src/tsconfig.json을 참조한다
        { "path": "../src" }
      ]
    }

    이 예시는 references 필드를 사용하여 test 모듈에서 src 모듈을 참조하고 있다는 것을 표현하고 있다. 이때 test 모듈에서 src 모듈에 있는 하위 모듈들을 불러오게 되면 소스 코드인 *.ts가 아니라 빌드가 완료된 결과물인 *.d.ts 파일을 가져온다. (tsc --build를 사용하면 해당 모듈의 최신 상태를 감지하고 증분 빌드도 해준다.)

    프로젝트 레퍼런스에 대한 자세한 내용은 이 포스팅의 주제는 아니니, 더 궁금하신 분들은 타입스크립트 공식 문서의 Project Referneces 문서를 확인해보도록 하자.

    마치며

    이번 포스팅에서는 tsconfig의 루트에 있는 몇 가지 필드들의 역할을 알아보았다. 사실 하나의 포스팅으로 tsconfig를 깔끔하게 정리하고 싶었는데, 이 친구가 워낙 방대한 옵션들을 가지고 계신지라 부득이하게 여러 개의 포스팅으로 나눠서 집필하게 되었다.

    이어지는 다음 포스팅에서는 타입스크립트의 컴파일 옵션들 중 타입 체크에 관한 옵션들에 대해서 이야기해보도록 하겠다.

    이상으로 [tsconfig의 모든 것] Root fields 포스팅을 마친다.

    Evan Moon

    🐢 거북이처럼 살자

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