타입스크립트를 처음 켜면 제일 먼저 만나는 파일이 tsconfig.json이다. 이게 컴파일러(코드를 검사하고 자바스크립트로 바꿔주는 도구)에게 규칙을 알려주는 설정 파일인데, 처음 열어보면 옵션이 수십 개라 겁부터 난다. 나도 그랬다. 근데 실무에서 매일 손대는 건 사실 몇 개뿐이고, 나머지는 한 번 정해두면 몇 달을 안 건드린다.


그래서 여기서는 전부 훑는 대신, 켜고 끌 때 코드가 실제로 어떻게 달라지는지를 옵션별로 보여주겠다. 효과를 눈으로 보면 왜 켜는지 손에 남는다.


tsconfig.json은 프로젝트 루트에 둔다. 직접 만들 필요 없이 명령 한 줄이면 주석 잔뜩 달린 기본 파일이 튀어나온다. 이 파일이 있는 폴더가 그 프로젝트의 경계가 된다.


npx tsc --init
// tsconfig.json 생성됨


핵심은 전부 compilerOptions 안에 들어간다. 실무에서 쓰는 최소 형태는 대략 이 정도다.


{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"strict": true,
"skipLibCheck": true
}
}


target은 어느 시대의 자바스크립트로 낮춰줄지 정한다. 최신 문법으로 짜도 오래된 브라우저는 못 읽으니, 컴파일러가 예전 문법으로 바꿔준다. target이 그 기준 연도다. 아래 화살표 함수 한 줄이 target에 따라 어떻게 달라지는지 보면 감이 온다.


// 원본 (TS/최신 JS)
const add = (a, b) => a + b;

// target: ES5 로 변환한 결과
var add = function (a, b) { return a + b; };


ES5로 두면 화살표 함수까지 function으로 풀어버리고, ES2020이면 요즘 브라우저가 다 아는 문법은 그대로 둔다. 낮출수록 결과물이 길어지고 못생겨지는 대신 호환 범위가 넓어진다. 요즘 프로젝트면 ES2020 언저리가 무난하다. 참고로 target은 문법을 바꿔줄 뿐, 옛 브라우저에 없는 함수(예: Array.prototype.at)까지 채워주지는 않는다. 그건 폴리필(빠진 기능을 코드로 메워주는 것)의 몫이다.


strict 하나가 열 개 몫을 한다. 이 옵션이 타입스크립트의 진짜 알맹이다. strict: true 한 줄을 켜면 그 아래로 엄격 검사 옵션 여러 개가 한꺼번에 켜진다. noImplicitAny, strictNullChecks 같은 것들이 전부 딸려 온다. 이걸 꺼두면 타입스크립트를 쓰면서도 자바스크립트나 다름없이 헐렁하게 굴러가서, 돈 주고 산 안전벨트를 풀고 타는 꼴이 된다. 새 프로젝트라면 무조건 켜라. 아래 두 옵션이 strict가 켜주는 것들 중 체감이 가장 큰 것들이다.


noImplicitAny는 타입 안 붙인 걸 잡아낸다. 함수 매개변수에 타입을 안 적으면 타입스크립트는 그걸 any(아무거나)로 본다. any는 검사를 통째로 포기한다는 뜻이라, 이 옵션은 그런 몰래 any를 에러로 바꿔 준다.


function double(x) {
return x * 2;
}
// error TS7006: Parameter 'x' implicitly has an 'any' type.


고치는 법은 간단하다. x: number라고 타입을 적어주면 에러가 사라진다. 귀찮아 보여도, 이 한 줄이 나중에 double("3") 같은 실수를 컴파일 단계에서 막아준다.


strictNullChecks는 null과 undefined를 진지하게 다룬다. 이 옵션이 꺼져 있으면 어떤 값이든 null일 수 있다는 걸 무시하고 넘어간다. 그러다 실제로 null이 들어오면 런타임에 터진다. 켜두면 null일 가능성이 있는 값을 그냥 쓰려 할 때 미리 막아준다.


const el = document.querySelector(".btn");
el.addEventListener("click", () => {});
// error TS18047: 'el' is possibly 'null'.


querySelector는 요소를 못 찾으면 null을 준다. 그래서 반환 타입이 Element | null이고, 곧바로 점을 찍으면 위처럼 막힌다. if (el)로 한 번 걸러주면 그 블록 안에서는 null이 빠진 걸로 좁혀져서 통과한다. 옛날에 이걸 몰라 셀렉터 오타 하나로 페이지가 통째로 죽은 적이 있는데, 이 옵션이 켜져 있었으면 저장하기도 전에 알았을 일이다.


같은 옵션이 함수 매개변수에서도 일한다. 있어도 되고 없어도 되는 값(?를 붙인 선택 매개변수)은 undefined일 수 있으니, 검사 없이 쓰면 막힌다.


function greet(name?: string) {
return name.toUpperCase();
}
// error TS18048: 'name' is possibly 'undefined'.


name?: string은 사실 string | undefined다. 이럴 때도 if (name)로 한 번 걸러주거나 name ?? "손님"처럼 기본값을 채워주면 통과한다. 이 두 예제가 실무에서 마주치는 possibly null, possibly undefined의 거의 전부다.


module과 moduleResolution은 import를 어떻게 풀지 정한다. moduleimport/export를 어떤 방식으로 내보낼지, moduleResolutionimport 경로에서 실제 파일을 어떻게 찾을지를 정한다. 요즘 번들러(Vite, webpack 같은 빌드 도구)를 쓰면 "module": "ESNext""moduleResolution": "bundler" 조합이 무난하다. 여기서 헤매면 멀쩡한 코드가 모듈을 못 찾는다며 빨간 줄을 뿜으니, 프레임워크가 깔아준 기본값을 함부로 바꾸지 마라.


나머지는 취향과 상황에 따라 만진다. skipLibCheck: true는 남이 만든 타입 정의 파일까지 검사하느라 느려지고 엉뚱한 에러가 뜨는 걸 막아줘서 거의 항상 켠다. esModuleInterop: true는 옛 방식으로 만든 라이브러리를 import할 때 생기는 잔고장을 줄여준다. noUnusedLocals는 안 쓰는 변수를 잡아주는데 개발 중엔 성가셔서 끄는 사람도 많다. outDirrootDir은 변환된 자바스크립트를 어디에 쌓을지 정하고, includeexclude는 검사 대상 폴더를 지정한다.


이 둘은 compilerOptions 바깥, 최상위에 적는다는 걸 자주 헷갈린다. 안에 넣어봤자 무시당한다.


{
"compilerOptions": { "strict": true },
"include": ["src"],
"exclude": ["node_modules", "dist"]
}


편집기의 빨간 줄과 실제 검사는 따로 논다. 초보 때 제일 많이 하는 착각이, 편집기에 빨간 줄이 없으니 통과라고 믿는 것이다. 편집기는 열려 있는 파일 위주로 봐주기 때문에, 안 열어본 파일의 타입 에러는 놓치기 쉽다. 배포 전에 프로젝트 전체를 한 번에 검사하려면 아래 명령을 직접 돌려야 확실하다.


npx tsc --noEmit
// --noEmit: 검사만 하고 결과 파일은 안 만든다


tsconfig는 한 번 제대로 잡아두면 오래 안 건드리는 파일이다. 그래서 처음에 strict를 켜두고 시작하는 게 중요하다. 나중에 코드가 쌓인 뒤에 켜면 빨간 줄 수백 개를 한꺼번에 마주하게 되고, 그때 가서 끄고 싶은 유혹에 진다. 프로젝트를 열자마자 벨트부터 매고 출발하는 게 결국 제일 편하다.