본문 바로가기

tsconfig.json 설정

tsconfig.json의 compilerOptions와 files, include, exclude, extends가 각각 무엇을 정하는지 TypeScript 6.0의 기본값과 함께 정리하고, extends로 합쳐진 유효 설정을 --showConfig로 확인한 뒤, import가 exclude보다 앞선다는 점을 작은 프로젝트로 추적합니다.

1장부터 거의 모든 실습에 tsconfig.json이 있었습니다. 이 파일은 컴파일러가 어떤 파일을 프로그램에 넣고, 어떤 규칙으로 검사하며, 무엇을 어디에 출력할지를 한곳에 적어 둔 설정입니다. tsc -p .은 지금 폴더의 이 파일을 읽어 그대로 실행하므로, 설정만 보면 그 프로젝트가 어떻게 컴파일되는지 알 수 있습니다.

이 글은 tsconfig.json의 구조와 자주 쓰는 옵션을 TypeScript 6.0의 기본값과 함께 정리합니다. 이어서 다른 설정을 물려받는 extends와, 물려받아 합쳐진 최종 설정을 확인하는 방법을 보고, 마지막으로 파일 목록과 import가 함께 프로그램을 이루는 과정을 작은 프로젝트로 추적합니다.

기본 구조

tsconfig.json의 최상위에는 컴파일러의 동작을 정하는 compilerOptions와, 프로그램에 넣을 파일을 정하는 files, include, exclude, 다른 설정을 물려받는 extends가 있습니다. 이 파일은 주석과 마지막 쉼표를 허용하는 JSON이어서, 설정마다 이유를 적어 둘 수 있습니다.

tsconfig.json
{
  // 공통 설정을 물려받는다
  "extends": "./configs/base.json",
  "compilerOptions": {
    "rootDir": "./src",
    "outDir": "./dist",
    "declaration": true,
  },
  "include": ["src/**/*.ts"],
  "exclude": ["**/*.spec.ts"],
}

files는 프로그램에 넣을 파일을 하나씩 적는 목록이고, include는 src/**/*.ts처럼 패턴으로 고르는 목록이며, exclude는 include가 고른 것 가운데 뺄 패턴입니다. exclude를 적지 않으면 node_modules와 outDir이 빠집니다. 상속도 참조도 없는 설정에 빈 "files": []만 두면 The 'files' list in config file로 시작하는 TS18002가 나오므로, 일반 프로젝트에서는 files를 생략하고 include를 씁니다.

자주 쓰는 compilerOptions

옵션은 하는 일에 따라 묶어 보면 기억하기 쉽습니다. 다음 표는 이 교재에서 자주 쓰는 옵션과 TypeScript 6.0의 기본값입니다.

옵션정하는 것6.0의 기본값
target출력할 자바스크립트의 문법 수준과 기본 lib그해의 ES 버전으로, 지금은 es2025
module출력할 모듈의 형식과 검사할 모듈 규칙esnext
moduleResolutionimport의 경로를 파일로 찾는 규칙module에 따라 정해진다
lib포함할 표준 API의 선언target에 맞는 ES 선언과 dom
strict엄격한 검사 옵션 묶음true
rootDir, outDir소스의 기준 폴더와 출력 폴더rootDir은 설정 파일이 있는 폴더
types전역에 자동으로 포함할 @types 패키지빈 배열
declaration, sourceMap선언 파일과 소스 맵의 출력false
noEmitOnError오류가 있을 때 출력을 막을지false

target은 문법을 낮추어 출력할 뿐 오래된 환경에 없는 함수를 채워 주는 폴리필(polyfill)을 설치하지 않으며, lib도 선언만 더할 뿐 실제 API를 만들지 않습니다. strict는 noImplicitAny와 strictNullChecks 같은 여러 검사를 한꺼번에 켜는 옵션이고, 그 가운데 하나를 따로 false로 적으면 그 검사만 꺼집니다. rootDir의 기본값이 바뀐 탓에, 6.0으로 옮긴 프로젝트에서 TS5011 오류가 나고 출력이 dist/src/index.js처럼 한 단계 깊어졌다면 "rootDir": "./src"를 직접 적습니다.

이 밖에 선언 파일의 검사를 건너뛰어 컴파일을 빠르게 하는 skipLibCheck, 파일 이름의 대소문자가 섞인 import를 진단하는 forceConsistentCasingInFileNames, JSX를 어떻게 출력할지 정하는 jsx 같은 옵션도 자주 씁니다. 각 옵션의 자세한 동작은 다음 글에서 묶음별로 봅니다.

TypeScript 6.0에서는 여러 오래된 옵션과 값이 폐기 예정으로 지정되었습니다. target의 es5, module의 amd와 umd, system, moduleResolution의 node10과 classic, baseUrl, outFile, esModuleInterop: false가 그 예이며, 설정하면 진단이 나오고 7.0에서는 지원하지 않습니다. "ignoreDeprecations": "6.0"으로 진단을 잠시 끌 수는 있지만, 옮겨 갈 준비를 하는 동안만 쓰는 임시 방편입니다.

더 깊이: 6.0이 기본값을 바꾼 이유

TypeScript 6.0 릴리스 노트는 바뀐 기본값마다 이유를 밝혀 둡니다. strict는 대부분의 새 프로젝트가 엄격한 검사를 원한다는 판단에서, module의 esnext는 ES 모듈이 주된 형식이 되었다는 점에서 바뀌었습니다. target은 지원하는 가장 최신 ECMAScript 버전을 따라가는 값이 되었는데, 대부분의 개발자가 최신 런타임을 대상으로 배포하므로 오래된 문법으로 낮출 필요가 적다는 이유입니다. 그래서 이 기본값은 고정된 값이 아니라 컴파일러의 버전에 따라 움직이며, 6.0에서는 es2025입니다.

나머지 두 변화는 성능과 관계가 있습니다. 예전의 rootDir은 선언 파일이 아닌 모든 입력 파일의 공통 폴더로 추론되었는데, 그러려면 프로그램의 모든 파일 경로를 분석해야 했고 어떤 파일이 그 프로젝트에 속하는지도 프로젝트를 읽어 보기 전에는 알 수 없었습니다. types의 기본값을 빈 배열로 바꾼 것은 쓰지도 않는 수백, 수천 개의 선언 파일이 빌드에 들어오는 일을 막으려는 것이며, 릴리스 노트는 types를 알맞게 정하는 것만으로 빌드 시간이 20%에서 50%까지 줄어든 프로젝트가 많았다고 적습니다.

extends와 유효 설정

여러 프로젝트가 같은 설정을 쓴다면, 공통 부분을 한 파일에 두고 extends로 물려받습니다. 다음 두 파일 가운데 configs/base.json이 나눠 쓸 공통 설정이고, 루트의 tsconfig.json이 extends에 그 경로를 적어 물려받습니다. extends의 상대 경로는 그 값을 적은 설정 파일의 위치를 기준으로 해석됩니다.

configs/base.json
{
  "compilerOptions": {
    "target": "es2022",
    "module": "nodenext",
    "strict": true
  }
}
tsconfig.json
{
  "extends": "./configs/base.json",
  "compilerOptions": {
    "outDir": "./dist",
    "strict": false
  },
  "include": ["src/**/*.ts"]
}

컴파일러는 먼저 base.json을 읽고, 그 위에 현재 설정을 덮어씁니다. compilerOptions는 옵션마다 합쳐지므로 위의 결과는 target과 module은 물려받고, outDir은 더하고, strict는 false로 덮어쓴 설정입니다. 반면 files와 include, exclude는 합쳐지지 않고 현재 설정의 배열이 통째로 바꾸며, 다음다음 글에서 볼 references는 물려받지 않습니다. 설정 안의 상대 경로는 그 경로를 적은 설정 파일의 위치를 기준으로 해석됩니다.

이렇게 여러 파일이 합쳐지면 최종 설정을 눈으로 계산하기 어렵습니다. tsc --showConfig -p .은 합쳐진 최종 설정, 곧 유효 설정(effective configuration)을 JSON으로 출력하고 컴파일은 하지 않으므로, 설정이 생각대로 합쳐졌는지 확인할 때 씁니다.

base 설정과 현재 설정이 유효 설정으로 합류한다. 유효 옵션과 files include import가 프로그램을 만들고 check와 emit으로 갈라진다. 실행 loader는 별도 계약이다.

CONFIG → PROGRAM → CHECK / EMIT

설정과 입력을 합친 프로그램에서 검사와 출력을 나눈다

extends를 먼저 읽고 현재 설정을 덮어씁니다. 유효 옵션과 파일·import 집합을 함께 사용해 프로그램을 만들며, 타입 검사와 emit의 역할을 구별합니다.

설정과 입력을 합친 프로그램에서 검사와 출력을 나눈다base 설정과 현재 설정이 유효 설정으로 합류한다. 유효 옵션과 files include import가 프로그램을 만들고 check와 emit으로 갈라진다. 실행 loader는 별도 계약이다.기본 설정extends로 읽기현재 프로젝트 설정옵션·파일 목록 override유효 설정tsc --showConfigTypeScript 프로그램유효 옵션 + files / includeimport로 연결된 입력타입 검사strict · 해석 · 진단emitJS · 선언 · source map
설정·입력의 합류와 검사·출력 분기기본 설정과 현재 프로젝트 설정이 유효 설정으로 합류합니다. 유효 설정에서 만든 TypeScript 프로그램은 타입 검사와 emit으로 갈라집니다. 검사에서 emit으로 향하는 별도 간선은 없습니다.기본 설정extends로 읽기현재 프로젝트 설정옵션·파일 목록override유효 설정tsc --showConfigTypeScript 프로그램유효 옵션 +files / includeimport로 연결된입력타입 검사strict · 해석 · 진단emitJS · 선언 ·source map

01 · MERGE

설정이 작성된 위치가 기준

상대 경로는 각 설정 파일을 기준으로 읽습니다. 현재 files·include·exclude 배열은 상속한 배열을 대체하고 references는 상속하지 않습니다.

02 · INPUT

exclude는 import를 차단하지 않는다

exclude는 include 검색만 제한합니다. import되거나 files에 명시된 파일은 입력이 될 수 있습니다. rootDir는 입력 선택이 아니라 출력 경로를 제약합니다.

03 · OUTPUT

검사와 emit을 구별한다

noEmit은 JS·선언·source map 출력을 끕니다. 필요한 선언 출력은 declaration 같은 옵션으로 선택합니다.

04 · RUNTIME

실제 loader를 별도로 검증한다

paths가 import 문자열을 다시 쓰지는 않습니다. target·lib도 런타임 API나 polyfill을 설치하지 않습니다.

유효 설정과 실제 입력을 확인한 뒤 산출물과 실행 환경을 검증합니다.

위 그림처럼 기본 설정과 현재 설정이 유효 설정으로 합쳐지고, 유효 옵션과 파일 목록, 그 파일들의 import가 함께 하나의 프로그램을 이룹니다. 그 프로그램에서 타입 검사와 출력이 갈라지며, 출력한 파일을 실행 환경이 어떻게 읽을지는 또 다른 문제입니다. 편집기와 빌드 도구가 같은 tsconfig.json을 읽는지도 함께 확인해야, 편집기에서 보이는 진단과 빌드의 진단이 어긋나지 않습니다.

파일 목록과 import

include와 exclude는 컴파일러가 처음 읽기 시작할 파일만 고릅니다. 그 파일이 import하는 파일은 exclude에 있더라도 프로그램에 들어오는데, 이 점을 작은 프로젝트로 확인해 봅니다. 새 폴더 ts-config-trace에 package.json을 { "type": "commonjs" }로 두고 다음 파일을 만듭니다.

tsconfig.json
{
  "compilerOptions": {
    "target": "es2022",
    "module": "nodenext",
    "rootDir": "./src",
    "outDir": "./dist",
    "strict": true,
    "noEmitOnError": true
  },
  "include": ["src/**/*.ts"],
  "exclude": ["src/secret.ts", "**/*.spec.ts"]
}
src/main.ts
import { amount } from "./secret";
export const total = amount + 1;
console.log(total);
src/secret.ts
export const amount: number = 2;
src/unused.spec.ts
const value: number = "bad";
tsc -p .
node dist/main.js
3

include와 exclude로 고른 시작 파일은 src/main.ts 하나뿐입니다. 하지만 컴파일러는 main.ts의 import를 따라가 src/secret.ts도 프로그램에 넣으므로, 두 파일이 검사되고 dist/main.js와 dist/secret.js가 출력됩니다. unused.spec.ts는 exclude에 걸렸고 어디서도 가져오지 않으므로 프로그램에 들어오지 않아, 그 안의 타입 오류도 이 컴파일에서는 진단되지 않습니다.

정리

  • tsconfig.json은 compilerOptions로 검사와 출력의 규칙을, files와 include, exclude로 처음 읽을 파일을 정하며, 주석과 마지막 쉼표를 쓸 수 있습니다.
  • TypeScript 6.0에서 strict는 true, module은 esnext, types는 빈 배열, rootDir은 설정 파일의 폴더가 기본값입니다.
  • target과 lib는 문법과 선언을 정할 뿐 폴리필이나 실제 API를 만들지 않으며, 6.0에서 폐기 예정인 옵션은 ignoreDeprecations로 잠시만 진단을 끕니다.
  • extends는 compilerOptions를 옵션마다 덮어쓰고 파일 목록은 통째로 바꾸며, 합쳐진 유효 설정은 tsc --showConfig로 확인합니다.
  • exclude는 시작 파일을 고를 때만 쓰이므로, import된 파일은 exclude에 있어도 프로그램에 들어옵니다.

연습 문제

1. import된 파일의 오류

위 실습에서 src/secret.ts만 export const amount: number = "two";로 바꾸고, 새 폴더에서 다시 컴파일하면 어떻게 되나요? secret.ts는 exclude에 있으니 영향이 없을까요?

해설 보기

secret.ts는 main.ts가 가져오므로 프로그램에 들어 있고, 문자열을 number에 넣는 TS2322가 나옵니다. noEmitOnError가 켜져 있으므로 두 파일 모두 새로 출력되지 않습니다. 이 옵션은 남아 있는 예전 출력을 지우지 않으므로, 새 폴더가 아니라면 예전 dist를 새 결과로 착각할 수 있습니다.

2. 물려받은 include

configs/base.json에 "include": ["src/**/*.ts", "tests/**/*.ts"]를 더했고, 현재 설정에는 "include": ["src/**/*.ts"]가 있습니다. tests 폴더의 파일은 프로그램에 들어오나요?

해설 보기

들어오지 않습니다. include는 옵션처럼 합쳐지지 않고, 현재 설정의 배열이 물려받은 배열을 통째로 바꾸기 때문입니다. tests 폴더도 포함하려면 현재 설정의 include에 그 패턴을 다시 적거나, 현재 설정에서 include를 지워 물려받은 배열을 그대로 쓰게 합니다. 어느 쪽인지 헷갈리면 tsc --showConfig로 최종 include를 확인합니다.

3. 6.0으로 옮긴 뒤 깊어진 출력

TypeScript 5.9에서는 dist/index.js로 출력되던 프로젝트가 6.0에서는 dist/src/index.js로 출력됩니다. 설정에는 outDir만 있고 rootDir은 없습니다. 왜 그런가요?

해설 보기

5.9까지는 rootDir을 적지 않으면 입력 파일들의 공통 폴더인 src로 추론했지만, 6.0부터는 설정 파일이 있는 폴더가 기본값입니다. 그래서 src/index.ts가 rootDir 기준으로 src/index.ts라는 상대 경로를 가지게 되어, outDir 아래에 src 폴더가 하나 더 생깁니다. 이때 6.0은 입력 파일의 공통 폴더가 src이니 rootDir를 직접 적으라는 TS5011 오류도 함께 냅니다. "rootDir": "./src"를 직접 적으면 오류가 사라지고 예전처럼 dist/index.js로 출력됩니다.

다음 글 컴파일러 옵션 상세 설명에서는 이 글의 옵션을 출력과 모듈, 엄격성, 진단 같은 묶음으로 나누어 하나씩 자세히 봅니다.