컴파일러 옵션 상세 설명
컴파일러 옵션을 엄격성과 추가 검사, 모듈과 변환 도구, 실행 환경, 출력, 자바스크립트와 빌드의 묶음으로 나누어 각 옵션이 무엇을 바꾸고 무엇은 바꾸지 않는지 TypeScript 6.0을 기준으로 정리하고, strict 위에 더하는 두 옵션의 효과를 같은 코드로 비교합니다.
compilerOptions에는 옵션이 매우 많아서, 이름을 하나씩 외우기보다 각 옵션이 어디에 작용하는지로 묶어 보는 편이 이해하기 쉽습니다. 어떤 옵션은 타입 검사의 엄격함을, 어떤 옵션은 import를 찾는 규칙을, 어떤 옵션은 출력되는 파일을 바꿉니다. 같은 문제를 겪더라도 그 문제가 어느 묶음에서 생겼는지 알아야 맞는 옵션을 고를 수 있습니다.
이 글은 옵션을 여섯 묶음으로 나누어, 각 옵션이 무엇을 바꾸고 무엇은 바꾸지 않는지 TypeScript 6.0을 기준으로 정리합니다. 마지막으로 strict 위에 더하는 두 옵션이 같은 코드에서 무엇을 다르게 진단하는지 비교합니다.
TYPE / RUNTIME / CONTRACT
옵션을 입력·검사·해석·산출·재사용 효과로 비교한다
옵션은 서로 다른 책임을 조절하며 한 옵션이 여러 책임에 영향을 줄 수도 있습니다. 실행 순서처럼 연결하지 않고, 실제 입력과 진단·산출물에서 효과를 확인합니다.
INPUT
입력 집합
files·include·exclude를 확인합니다. import graph도 입력을 늘리므로 exclude만으로 파일을 봉쇄하지 않습니다.
CHECK
타입 검사
strict는 엄격성 묶음입니다. noUncheckedIndexedAccess 같은 독립 옵션을 추가하면 허용되는 타입 관계가 달라집니다.
RESOLVE
모듈 해석
moduleResolution·paths는 타입 검사에서 import 후보를 찾습니다. module과 해석 조합을 맞추되 실제 loader의 해석도 확인합니다.
EMIT
JS·선언·맵 산출
target·module·declaration·sourceMap은 문법·모듈·선언·맵 출력을 조절합니다. noEmit이면 이 출력은 생성하지 않습니다.
REUSE / DEBUG
캐시와 디버깅
incremental은 빌드 정보를 저장해 다음 실행에서 재사용합니다. sourceMap은 산출물이면서 디버깅 도구를 잇습니다. 옵션을 단 하나의 층에만 가두지 않습니다.
선택 기준은 복사한 옵션 묶음보다 실패 지점과 관찰한 결과입니다.
위 그림의 다섯 카드는 서로 이어지는 단계가 아니라 독립된 비교입니다. 입력 파일을 고르는 옵션, 타입 검사를 바꾸는 옵션, 모듈의 해석을 바꾸는 옵션, 출력을 바꾸는 옵션, 다시 쓰기 위한 정보를 남기는 옵션은 각각 다른 일을 맡고, 한 옵션이 여러 곳에 영향을 주기도 합니다.
엄격성
strict는 엄격한 검사 여러 개를 한 번에 켜는 옵션이며, TypeScript 6.0부터 기본값이 true입니다. strict에 포함된 옵션은 다음과 같고, 그 가운데 하나를 따로 false로 적으면 그 검사만 꺼집니다.
| 옵션 | 검사하는 것 |
|---|---|
noImplicitAny | 추론할 단서가 없어 암묵적으로 any가 되는 곳 |
strictNullChecks | null과 undefined를 다른 타입에 넣는 것 |
strictFunctionTypes | 함수 타입의 매개변수를 더 엄격하게 비교하는 것 |
strictBindCallApply | bind, call, apply의 인수 |
strictPropertyInitialization | 생성자에서 값을 넣지 않은 속성 |
noImplicitThis | 타입을 알 수 없는 this |
useUnknownInCatchVariables | catch의 변수를 any 대신 unknown으로 보는 것 |
strictBuiltinIteratorReturn | 내장 이터레이터의 반환값을 undefined로 보는 것 |
noImplicitAny는 타입을 적지 않은 모든 곳을 진단하는 것이 아니라, const n = 1처럼 추론할 수 있는 곳은 그대로 두고 매개변수처럼 단서가 없는 곳만 진단합니다. 모든 파일을 엄격 모드로 읽고 "use strict"를 출력하는 alwaysStrict는 예전에는 strict에 포함되었지만, 6.0부터는 strict와 상관없이 기본으로 켜지고 false로 끄는 설정은 폐기 예정이 되었습니다. 기본값이 버전에 따라 바뀌어 왔으므로, 엄격한 검사가 의도라면 기본값에 기대지 말고 "strict": true를 직접 적어 둡니다.
strict에 더하는 검사
strict에 포함되지 않아 따로 켜야 하는 검사도 있습니다. 이 옵션들은 특정한 실수를 겨냥하므로, 프로젝트에서 반복되는 실패의 모양에 맞추어 고릅니다.
| 옵션 | 검사하는 것 |
|---|---|
noUncheckedIndexedAccess | 인덱스로 읽은 값에 undefined 가능성을 더한다 |
exactOptionalPropertyTypes | 선택적 속성에 undefined를 직접 넣는 것을 구분한다 |
noImplicitReturns | 값을 돌려주는 함수에서 일부 경로가 값을 돌려주지 않는 것 |
noFallthroughCasesInSwitch | 실행문이 있는 case가 다음 case로 넘어가는 것 |
noImplicitOverride | 부모의 메서드를 재정의하면서 override를 빠뜨린 것 |
noPropertyAccessFromIndexSignature | 인덱스 시그니처의 속성을 점 표기로 읽는 것 |
noUnusedLocals, noUnusedParameters | 쓰지 않는 지역 변수와 매개변수 |
TYPE / RUNTIME / CONTRACT
strict 위에 구체적인 실패 경계의 옵션을 더한다
실패가 반복된다는 사실만으로 옵션을 정하지 않습니다. 인덱스 접근, 속성 부재, 반환 경로처럼 각 옵션이 실제로 검사할 수 있는 위험을 먼저 구별합니다.
BASELINE
strict 기준선
noImplicitAny·strictNullChecks 등의 엄격성 묶음부터 적용합니다. 모든 안전 옵션을 켜는 별칭은 아닙니다.
INDEX
미확인 배열·사전 접근
noUncheckedIndexedAccess는 미확인 접근 결과에 undefined 가능성을 더합니다. 확인 없이 값을 쓰던 경계를 찾습니다.
OPTIONAL
부재와 undefined 대입
exactOptionalPropertyTypes는 선택 속성이 없는 상태와 undefined를 대입한 상태를 구별합니다. 명시적으로 undefined를 허용할지는 속성 타입으로 정합니다.
RETURN / UNUSED
경로와 사용 여부 정책
noImplicitReturns, noUnusedLocals, noUnusedParameters는 따로 선택합니다. 이 검사로 드러나지 않는 동작·설계 문제는 테스트와 API 수정으로 검증합니다.
오류를 옵션이 검사하는 경계에 대응시키고 실제 진단을 확인합니다.
위 그림처럼 strict를 기준선으로 두고, 확인 없이 읽은 인덱스나 undefined를 넣은 선택적 속성처럼 실제로 겪는 실패에 맞는 옵션을 더합니다. 이 옵션들이 검사할 수 없는 동작, 예를 들어 계산이 맞는지나 값의 범위는 여전히 테스트와 설계로 확인해야 합니다. noUnusedParameters를 켠 프로젝트에서 일부러 쓰지 않는 매개변수는 이름을 _로 시작하게 지으면 진단되지 않습니다.
모듈과 변환 도구
module과 moduleResolution은 7장에서 본 대로 출력할 모듈의 형식과 import를 찾는 규칙을 정하고, paths는 별칭을 더하되 출력의 경로를 바꾸지 않습니다. 이 묶음에는 컴파일러가 아닌 다른 도구가 파일을 하나씩 변환할 때를 위한 옵션도 들어 있습니다.
| 옵션 | 하는 일 |
|---|---|
verbatimModuleSyntax | 적은 import와 export를 그대로 출력하고, 타입만 가져올 때 type을 요구한다 |
isolatedModules | 파일 하나만 보고 변환하면 틀릴 수 있는 문법을 진단한다 |
erasableSyntaxOnly | 타입을 지우는 것만으로 실행할 수 없는 열거형, 네임스페이스, 매개변수 속성을 진단한다 |
resolveJsonModule | .json 파일을 가져와 그 내용의 타입을 쓴다 |
esModuleInterop | CommonJS 모듈을 기본 가져오기로 받게 하며, 6.0에서 false로 끄는 설정은 폐기 예정이다 |
esbuild나 Vite, Node.js의 타입 제거 실행처럼 파일을 하나씩 변환하는 도구는 다른 파일의 타입을 보지 않습니다. 그래서 타입만 있는 이름을 값처럼 다시 내보내는 코드처럼 파일 하나만으로는 판단할 수 없는 문법을, isolatedModules와 verbatimModuleSyntax가 미리 알려 줍니다. erasableSyntaxOnly는 2장과 7장에서 본 것처럼 새 코드를 만들어 내는 문법을 막아, 타입 제거만으로 실행할 수 있는 코드로 제한합니다.
더 깊이: 가져오기 생략과 verbatimModuleSyntax
verbatimModuleSyntax가 없을 때 타입스크립트는 가져오기 생략(import elision)이라는 일을 합니다. import { Car } from "./car"로 가져온 Car를 타입의 자리에서만 쓰면, 컴파일러는 그 import 문 전체를 출력에서 지웁니다. Car가 실행 중에는 없는 인터페이스일 수 있으니 대개는 맞는 처리지만, import "./car"조차 남지 않으므로 그 모듈이 실행될 때 일으키던 부수 효과도 함께 사라집니다. 또 export { Car } from "./car"를 남길지는 Car가 클래스인지 인터페이스인지에 따라 달라서, 다른 파일의 선언을 보지 않는 변환 도구는 이 결정을 내릴 수 없습니다.
TypeScript 5.0 릴리스 노트는 예전에는 이 문제를 importsNotUsedAsValues, preserveValueImports, isolatedModules 세 옵션으로 나누어 다루었지만 세부 규칙을 이해하기 어려웠다고 설명합니다. 5.0의 verbatimModuleSyntax는 규칙을 하나로 줄였습니다. type 표시가 없는 가져오기와 내보내기는 그대로 남기고, type 표시가 있는 것은 모두 지웁니다. 그래서 import { type xyz } from "xyz"는 import {} from "xyz"로 남아 모듈을 실행하며, 앞의 두 옵션은 이 옵션으로 대체되어 5.0에서 폐기 예정이 되었고, 5.5부터는 아예 쓸 수 없습니다. 이 옵션을 켜면 ES 모듈 문법을 require로 바꾸어 출력하지 않고 오류를 내므로, CommonJS를 출력해야 하는 파일에서는 import foo = require("foo")와 export = 같은 타입스크립트의 옛 모듈 문법을 씁니다.
실행 환경
target은 출력할 문법의 수준을, lib는 쓸 수 있는 표준 API의 선언을, types는 전역에 포함할 @types 패키지를 정합니다. jsx는 JSX를 react-jsx처럼 함수 호출로 바꿀지 preserve로 남겨 다른 도구에 맡길지 정합니다.
이 옵션들은 모두 선언과 출력의 문법만 정한다는 점이 중요합니다. target을 낮추어도 오래된 환경에 없는 Array.prototype.at 같은 함수가 생기지 않고, lib에 dom을 넣어도 Node.js에 document가 생기지 않습니다. 실행 환경이 실제로 무엇을 지원하는지는 따로 확인하고, 필요하다면 폴리필을 직접 넣습니다.
출력
출력의 묶음은 어떤 파일을 어디에 만들지 정합니다.
| 옵션 | 하는 일 | 6.0의 기본값 |
|---|---|---|
rootDir, outDir | 소스의 기준 폴더와 출력 폴더 | rootDir은 설정 파일의 폴더 |
declaration, declarationMap | 선언 파일과, 선언에서 원래 소스로 가는 지도 | false |
sourceMap | 출력한 자바스크립트에서 원래 소스로 가는 지도 | false |
noEmit | 파일을 만들지 않고 검사만 한다 | false |
noEmitOnError | 오류가 있으면 출력을 막는다 | false |
removeComments | 출력에서 주석을 지운다 | false |
소스 맵은 디버거와 오류의 스택에서 출력한 자바스크립트 대신 원래 타입스크립트의 위치를 보여 주므로 개발할 때 거의 항상 켭니다. 다만 배포하는 소스 맵에는 원래 코드가 담길 수 있으므로, 공개할지는 따로 정합니다. 오래된 프로젝트의 outFile은 출력을 한 파일로 합치던 옵션인데, 6.0에서 폐기 예정이 되었으므로 번들러가 그 일을 맡게 합니다.
자바스크립트와 빌드
allowJs는 자바스크립트 파일을 프로그램에 넣어 출력까지 하게 하고, checkJs는 그 파일들도 타입 검사하게 합니다. 1장에서 본 것처럼 파일의 맨 위에 // @ts-check를 두어 파일마다 검사를 켤 수도 있습니다.
skipLibCheck는 선언 파일의 검사를 건너뛰어 컴파일을 빠르게 하지만, @types 패키지끼리 부딪히는 진단까지 함께 숨깁니다. incremental은 다음 컴파일이 다시 쓸 정보를 .tsbuildinfo 파일에 남기는데, noEmit과 함께 켜도 이 파일은 기록될 수 있습니다. 빌드의 속도를 다루는 옵션은 증분 컴파일과 빌드 최적화에서 자세히 봅니다.
두 추가 옵션 비교하기
strict 위에 더하는 두 옵션이 같은 코드에서 무엇을 진단하는지 비교해 봅니다. strict와 module: "nodenext"만 둔 프로젝트에 다음 파일을 두고, 옵션의 조합을 바꾸어 가며 같은 코드를 검사합니다.
export const numbers: number[] = [];
export const first: number = numbers[0];
export interface Options {
timeout?: number;
}
export const missing: Options = {};
export const explicitUndefined: Options = { timeout: undefined };| 더한 옵션 | first | missing | explicitUndefined |
|---|---|---|---|
| 없음 | 통과 | 통과 | 통과 |
noUncheckedIndexedAccess | 진단 | 통과 | 통과 |
exactOptionalPropertyTypes | 통과 | 통과 | 진단 |
| 둘 다 | 진단 | 통과 | 진단 |
strict만으로는 모두 통과하지만, 실제로 빈 배열의 numbers[0]은 undefined이므로 first: number라는 표기와 실제 값이 어긋납니다. noUncheckedIndexedAccess를 켜면 numbers[0]의 타입이 number | undefined가 되어 first의 대입이 Type 'number | undefined' is not assignable to type 'number'.로 진단됩니다. exactOptionalPropertyTypes를 켜면 timeout?: number는 속성이 없는 것만 허용하고 undefined를 직접 넣는 것은 허용하지 않으므로 explicitUndefined가 진단되며, 속성을 생략한 missing은 어느 조합에서도 통과합니다. 옵션의 정확한 규칙은 공식 TSConfig 참조에서 하나씩 찾아볼 수 있습니다.
정리
- 옵션은 엄격성과 추가 검사, 모듈과 변환 도구, 실행 환경, 출력, 자바스크립트와 빌드의 묶음으로 나누어 어디에 작용하는지 봅니다.
strict는 여러 엄격한 검사를 한 번에 켜며 6.0에서 기본값이true이고, 기본값에 기대지 않도록 직접 적어 둡니다.noUncheckedIndexedAccess와exactOptionalPropertyTypes같은 추가 검사는strict에 포함되지 않으므로, 반복되는 실패의 모양에 맞추어 따로 켭니다.isolatedModules와verbatimModuleSyntax,erasableSyntaxOnly는 파일을 하나씩 변환하는 도구와 함께 쓸 때 틀릴 수 있는 문법을 미리 알려 줍니다.target과lib,types는 문법과 선언만 정할 뿐 실행 환경에 없는 기능을 만들지 않습니다.
연습 문제
1. 요구사항에 맞게 고치기
두 추가 옵션을 모두 켠 상태에서, "빈 배열이면 first에 0을 쓰고, timeout은 지정하지 않는다"는 요구사항에 맞게 위 코드의 두 진단을 없애 보세요. numbers[0]!로 고치는 방법은 요구사항을 만족하나요?
해설 보기
첫 대입은 export const first: number = numbers[0] ?? 0;으로, 마지막 대입은 export const explicitUndefined: Options = {};로 바꿉니다. 그러면 first는 실제로 0이 되고 마지막 객체에는 timeout 키가 없습니다. numbers[0]!은 타입 검사에서 undefined의 가능성을 지울 뿐 실행 중의 값을 0으로 바꾸지 않으므로, first에는 여전히 undefined가 들어가 요구사항을 만족하지 않습니다.
2. 기본값이 바뀐 strict
TypeScript 5.9에서 strict를 적지 않은 채 진단 없이 컴파일되던 프로젝트를 6.0으로 옮겼더니, 많은 진단이 새로 나왔습니다. 왜 그런가요? 당장 예전처럼 컴파일하려면 무엇을 적어야 하나요?
해설 보기
5.9까지는 strict를 적지 않으면 false였지만 6.0부터는 true가 기본값이어서, noImplicitAny와 strictNullChecks 같은 검사가 새로 켜졌기 때문입니다. 당장 예전처럼 컴파일하려면 "strict": false를 적습니다. 다만 새로 나온 진단은 대부분 실행 중에 생길 수 있는 실수를 가리키므로, 옵션을 끄기보다 검사를 하나씩 켜 가며 진단을 고치는 편이 장기적으로 낫습니다.
3. 타입 제거 실행과 맞추기
Node.js의 타입 제거 실행으로 .ts 파일을 바로 실행하려는 프로젝트에서, 실행하기 전에 문제를 알려 줄 옵션을 두 가지 고르고 각각 무엇을 막는지 설명해 보세요.
해설 보기
erasableSyntaxOnly는 열거형과 실행 코드가 있는 네임스페이스, 매개변수 속성처럼 타입 제거로는 실행할 수 없는 문법을 진단합니다. verbatimModuleSyntax는 타입만 가져오는 곳에 type을 요구하는데, Node.js는 type이 없는 가져오기를 값의 가져오기로 남겨 실행 중에 멈추기 때문입니다. Node.js의 문서도 이 두 옵션을 함께 켜는 설정을 권합니다.
다음 글 프로젝트 레퍼런스에서는 하나의 저장소를 여러 타입스크립트 프로젝트로 나누고, 서로를 참조하며 순서대로 빌드하는 방법을 다룹니다.