iBetter Books
수정

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가 핵심입니다. 컴파일러의 세부 동작을 여기서 제어합니다. includeexclude는 어떤 파일을 컴파일할지 지정합니다.

target — 어느 JavaScript 버전으로 변환할까

TypeScript 코드를 변환할 때 목표가 되는 JavaScript 버전입니다.

{  "compilerOptions": {    "target": "ES2020"  }}

target에 따라 컴파일러가 최신 문법을 구형 문법으로 변환해줍니다. 예를 들어 target: "ES5"로 설정하면 async/awaitPromisethen/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"  }}

modulemoduleResolution은 세트처럼 맞춰줘야 합니다.

module 권장 moduleResolution
commonjs node
ES2020 bundler
NodeNext NodeNext

Vite나 webpack 같은 번들러를 쓴다면 bundler를 씁니다. 번들러가 자체적인 모듈 해석 규칙을 갖고 있기 때문입니다.

strict — 엄격 모드

이 옵션 하나가 가장 중요합니다.

{  "compilerOptions": {    "strict": true  }}

strict: true는 여러 엄격한 검사 옵션을 한꺼번에 활성화하는 단축키입니다. 켜지는 옵션들을 살펴봅시다.

strictNullChecks

nullundefined를 엄격하게 검사합니다. 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 파일이 같은 폴더에 섞입니다. rootDiroutDir은 항상 설정하는 것이 좋습니다.

그 밖의 유용한 옵션

{  "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 경로 별칭 설정

esModuleInteropimport 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.jsonpaths는 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: truenode_modules 안의 .d.ts 파일들을 타입 검사에서 건너뜁니다. 서드파티 라이브러리 내부의 타입 오류가 내 프로젝트 빌드를 막는 상황을 피합니다. 학습 단계에서는 편의를 위해 넣어두는 것을 권장합니다.

include와 exclude

{  "include": ["src/**/*"],  "exclude": ["node_modules", "dist", "**/*.test.ts"]}

include는 컴파일할 파일 패턴, exclude는 제외할 패턴입니다. **/*는 "하위 모든 폴더의 모든 파일"을 뜻합니다. excludenode_modules를 넣지 않으면 설치된 모든 패키지를 컴파일하려 합니다. 반드시 포함합니다.

exclude를 명시하지 않으면 node_modules, bower_components, jspm_packages, outDir이 기본으로 제외됩니다.


tsconfig.json은 한 번 설정하고 잊는 파일이 아닙니다. 프로젝트가 성장하면서 필요에 따라 조정합니다. 처음에는 학습용 추천 설정으로 시작하고, 어떤 옵션이 어떤 역할을 하는지 이해하면서 프로젝트에 맞게 다듬어나갑니다.

다음 챕터에서는 이 설정을 바탕으로 실제 프로젝트의 폴더 구조를 어떻게 잡을지 살펴봅니다.