tsconfig.json 이해하기
번역가를 고용한다고 상상해봅시다. "영어를 한국어로 번역해주세요"라고만 하면 될까요. 어떤 문체로 번역할지, 전문 용어는 어떻게 처리할지, 고어체를 쓸지 현대어를 쓸지 등 세부 지침이 있어야 합니다. 지침 없이 번역을 맡기면 기대와 다른 결과물이 나옵니다.
TypeScript 컴파일러도 마찬가지입니다. tsc에게 TypeScript 코드를 JavaScript로 변환해달라고 할 때, 어떻게 변환할지 상세한 지침을 줘야 합니다. 그 지침서가 tsconfig.json입니다.
tsconfig.json이란
tsconfig.json은 프로젝트 루트에 놓는 JSON 파일입니다. TypeScript 컴파일러(tsc)가 이 파일을 읽어서 동작 방식을 결정합니다.
# tsconfig.json 생성npx tsc --init
이 명령어를 실행하면 기본 tsconfig.json이 생성됩니다. 주석이 가득하고 대부분은 비활성화된 상태입니다. 처음에는 압도당하는 느낌이지만, 핵심 옵션 몇 가지만 이해하면 됩니다.
생성된 파일의 기본 구조를 봅시다.
// 새 파일: tsconfig.json{ "compilerOptions": { "target": "ES2020", "module": "commonjs", "strict": true, "outDir": "./dist", "rootDir": "./src" }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"]}
compilerOptions가 핵심입니다. 컴파일러의 세부 동작을 여기서 제어합니다. include와 exclude는 어떤 파일을 컴파일할지 지정합니다.
target — 어느 JavaScript 버전으로 변환할까
TypeScript 코드를 변환할 때 목표가 되는 JavaScript 버전입니다.
{ "compilerOptions": { "target": "ES2020" }}
target에 따라 컴파일러가 최신 문법을 구형 문법으로 변환해줍니다. 예를 들어 target: "ES5"로 설정하면 async/await을 Promise와 then/catch 체인으로, 화살표 함수를 function 키워드로 변환합니다.
| 값 | 설명 |
|---|---|
ES5 |
매우 구형. IE 지원이 필요할 때. 거의 쓰이지 않음 |
ES6 / ES2015 |
화살표 함수, 클래스 등을 그대로 유지 |
ES2017 |
async/await 유지 |
ES2020 |
옵셔널 체이닝(?.), nullish 병합(??) 유지 |
ESNext |
항상 최신. TypeScript가 지원하는 가장 최신 버전 |
요즘 Node.js 프로젝트라면 ES2020 이상을 권장합니다. 브라우저 프로젝트라면 번들러(Vite, webpack)가 변환을 담당하므로 target을 높게 잡아도 됩니다.
module — 모듈 시스템은 무엇을 쓸까
import/export 문법을 어떤 모듈 시스템으로 변환할지 결정합니다.
{ "compilerOptions": { "module": "commonjs" }}
| 값 | 설명 |
|---|---|
commonjs |
Node.js 기본. require()/module.exports로 변환 |
ES2020 / ESNext |
ES 모듈 그대로 유지. import/export 유지 |
NodeNext |
Node.js 최신 모듈 해석 방식 |
Node.js 프로젝트에서 package.json에 "type": "module"이 없다면 commonjs, 있다면 NodeNext를 씁니다. 프론트엔드 프로젝트는 번들러가 처리하므로 ES2020 이상을 씁니다.
moduleResolution — 모듈을 어떻게 찾을까
import { add } from "./math"처럼 경로를 적었을 때 TypeScript가 실제 파일을 어떻게 찾을지 결정합니다.
{ "compilerOptions": { "moduleResolution": "node" }}
module과 moduleResolution은 세트처럼 맞춰줘야 합니다.
| module | 권장 moduleResolution |
|---|---|
commonjs |
node |
ES2020 |
bundler |
NodeNext |
NodeNext |
Vite나 webpack 같은 번들러를 쓴다면 bundler를 씁니다. 번들러가 자체적인 모듈 해석 규칙을 갖고 있기 때문입니다.
strict — 엄격 모드
이 옵션 하나가 가장 중요합니다.
{ "compilerOptions": { "strict": true }}
strict: true는 여러 엄격한 검사 옵션을 한꺼번에 활성화하는 단축키입니다. 켜지는 옵션들을 살펴봅시다.
strictNullChecks
null과 undefined를 엄격하게 검사합니다. TypeScript에서 가장 중요한 옵션 중 하나입니다.
// 새 파일: strict-null-demo.ts// strictNullChecks: false (기본값, 권장하지 않음)// let name: string = null; // 오류 없음 — 위험합니다// strictNullChecks: truelet userName: string = null; // 오류: 'null'은 'string'에 할당할 수 없습니다let maybeNull: string | null = null; // 정상 — 명시적으로 허용했습니다
이 옵션 없이는 null 관련 런타임 오류(Cannot read property of null)를 TypeScript가 잡지 못합니다. 반드시 켜야 합니다.
noImplicitAny
타입을 추론할 수 없을 때 any를 암시적으로 쓰는 것을 금지합니다.
// 새 파일: no-implicit-any-demo.ts// noImplicitAny: falsefunction greet(name) { // name의 타입이 any — 경고 없음 console.log(`안녕, ${name}!`);}// noImplicitAny: truefunction greet(name) { // 오류: 매개변수 'name'은 암시적으로 'any' 형식입니다 console.log(`안녕, ${name}!`);}// 해결: 타입을 명시합니다function greet(name: string) { // 정상 console.log(`안녕, ${name}!`);}
strictFunctionTypes
함수 타입의 매개변수를 반변(contravariant)으로 검사합니다. 함수를 다른 함수에 할당할 때 타입이 맞는지 더 엄격히 검사합니다.
// 새 파일: strict-fn-demo.tstype Handler = (event: MouseEvent) => void;function handleClick(handler: Handler) { const event = new MouseEvent("click"); handler(event);}// strictFunctionTypes: false// const fn: Handler = (e: Event) => {}; // 오류 없음 — 하지만 위험// strictFunctionTypes: trueconst fn: Handler = (e: Event) => {}; // 오류: Event는 MouseEvent보다 넓습니다const fn2: Handler = (e: MouseEvent) => {}; // 정상
strict 모드가 켜는 옵션 목록
strict: true가 켜는 주요 옵션들
├── strictNullChecks
├── strictFunctionTypes
├── strictBindCallApply
├── strictPropertyInitialization
├── noImplicitAny
├── noImplicitThis
├── alwaysStrict
└── useUnknownInCatchVariables (4.4+)
위 목록은 주요 옵션을 포함하며, TypeScript 버전마다 새로운 옵션이 추가될 수 있습니다.
strict: true를 켜고 시작하는 것이 강력히 권장됩니다. 나중에 끄기는 쉽지만, 처음에 끄고 프로젝트가 커진 뒤에 켜면 수백 개의 오류가 쏟아집니다.
outDir와 rootDir — 입력과 출력 폴더
{ "compilerOptions": { "rootDir": "./src", "outDir": "./dist" }}
rootDir은 TypeScript 소스 파일이 있는 폴더, outDir은 컴파일된 JavaScript 파일을 놓을 폴더입니다. src/user.ts를 컴파일하면 dist/user.js가 생깁니다.
폴더 구조를 지정하지 않으면 .ts 파일과 .js 파일이 같은 폴더에 섞입니다. rootDir과 outDir은 항상 설정하는 것이 좋습니다.
그 밖의 유용한 옵션
{ "compilerOptions": { "sourceMap": true, "declaration": true, "esModuleInterop": true, "resolveJsonModule": true, "baseUrl": ".", "paths": { "@/*": ["src/*"] } }}
| 옵션 | 설명 |
|---|---|
sourceMap |
디버깅을 위한 소스맵 생성. 에러 위치가 .ts 파일로 표시됨 |
declaration |
.d.ts 타입 선언 파일 생성. 라이브러리 배포 시 필요 |
esModuleInterop |
CommonJS 모듈을 ES 모듈처럼 import할 수 있게 함 |
resolveJsonModule |
.json 파일을 import할 수 있게 함 |
baseUrl |
절대 경로의 기준점 |
paths |
import 경로 별칭 설정 |
esModuleInterop은 import express from "express" 같은 문법을 가능하게 합니다. 이 옵션 없이는 import * as express from "express"처럼 써야 합니다. 대부분의 프로젝트에서 true로 설정합니다.
paths로 경로 별칭 설정하기
프로젝트가 커지면 상대 경로가 지저분해집니다.
// 이런 import를 쓰고 싶지 않습니다import { UserService } from "../../../services/user";import { formatCurrency } from "../../utils/number";
paths 옵션으로 별칭을 만들면 깔끔해집니다.
// tsconfig.json 수정{ "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["src/*"], "@utils/*": ["src/utils/*"], "@types/*": ["src/types/*"] } }}
// 새 파일: cleaner-imports.tsimport { UserService } from "@/services/user";import { formatCurrency } from "@utils/number";import { User } from "@types/user";
@/로 시작하는 경로는 src/부터의 경로임을 바로 알 수 있습니다. 파일을 다른 폴더로 옮겨도 import 경로를 수정하지 않아도 됩니다.
주의할 점은 tsconfig.json의 paths는 TypeScript 컴파일러에게만 알려줍니다. 런타임(Node.js)이나 번들러에게도 같은 설정을 해줘야 합니다. Node.js에서는 tsconfig-paths 패키지, Vite에서는 resolve.alias 설정으로 맞춰줍니다.
학습용 추천 설정
처음 TypeScript 프로젝트를 시작할 때 쓰기 좋은 설정입니다.
// 새 파일: tsconfig.json (학습용){ "compilerOptions": { "target": "ES2020", "module": "commonjs", "moduleResolution": "node", "strict": true, "outDir": "./dist", "rootDir": "./src", "sourceMap": true, "esModuleInterop": true, "resolveJsonModule": true, "skipLibCheck": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"]}
skipLibCheck: true는 node_modules 안의 .d.ts 파일들을 타입 검사에서 건너뜁니다. 서드파티 라이브러리 내부의 타입 오류가 내 프로젝트 빌드를 막는 상황을 피합니다. 학습 단계에서는 편의를 위해 넣어두는 것을 권장합니다.
include와 exclude
{ "include": ["src/**/*"], "exclude": ["node_modules", "dist", "**/*.test.ts"]}
include는 컴파일할 파일 패턴, exclude는 제외할 패턴입니다. **/*는 "하위 모든 폴더의 모든 파일"을 뜻합니다. exclude에 node_modules를 넣지 않으면 설치된 모든 패키지를 컴파일하려 합니다. 반드시 포함합니다.
exclude를 명시하지 않으면 node_modules, bower_components, jspm_packages, outDir이 기본으로 제외됩니다.
tsconfig.json은 한 번 설정하고 잊는 파일이 아닙니다. 프로젝트가 성장하면서 필요에 따라 조정합니다. 처음에는 학습용 추천 설정으로 시작하고, 어떤 옵션이 어떤 역할을 하는지 이해하면서 프로젝트에 맞게 다듬어나갑니다.
다음 챕터에서는 이 설정을 바탕으로 실제 프로젝트의 폴더 구조를 어떻게 잡을지 살펴봅니다.