🔥 TS 6.0 의 tsconfig: strict 기본 시대
강의 목차

처음 tsconfig.json 을 열었을 때 나는 한 화면에 적힌 옵션 다섯 줄을 한참 봤다. strict, target, module, moduleResolution, verbatimModuleSyntax. 어떤 줄은 타입 검사 쪽 설정 같고 어떤 줄은 결과 .js 쪽 설정 같아서, 한 파일 안에서 무엇부터 읽을지 바로 정하지 못했다.
tsconfig.json 은 타입 검사 규칙과 실행 결과 규칙을 같이 적는 파일이다. 그래서 이 다섯 줄도 두 종류를 나눠서 보는 편이 낫다. 아래에서는 TS 6.0 권장 tsconfig.json 의 핵심 다섯 줄을 그 기준으로 본다.
bun init 이 적어 주는 파일
bun init 을 한 번 돌리면 디렉토리에 tsconfig.json 한 개가 생기고, 그 안에는 TS 6.0 기준 권장 조합이 거의 그대로 들어 있다. 다섯 줄 짜리 핵심은 다음과 같다.
{
"compilerOptions": {
"target": "es2025",
"module": "nodenext",
"moduleResolution": "nodenext",
"strict": true,
"verbatimModuleSyntax": true,
"noEmit": true,
"skipLibCheck": true,
"types": ["node"]
}
}{
"compilerOptions": {
"target": "es2025",
"module": "nodenext",
"moduleResolution": "nodenext",
"strict": true,
"verbatimModuleSyntax": true,
"noEmit": true,
"skipLibCheck": true,
"types": ["node"]
}
}위 다섯 줄(target, module, moduleResolution, strict, verbatimModuleSyntax)을 아래에서 하나씩 본다. 나머지 세 줄은 보조 옵션이다. noEmit 은 검사만 하고 결과 .js 를 만들지 않겠다는 뜻이다. skipLibCheck 는 외부 .d.ts 파일까지 다시 검사하지 않겠다는 뜻이다. types: ["node"] 는 Node 표준 타입 정의를 함께 읽겠다는 뜻이다. 각 줄이 값 레벨과 타입 레벨 중 한쪽에만 깔끔하게 속하는 것은 아니다. 그래도 두 차이를 먼저 구분해 두면 각 설정이 어느 쪽에 더 가까운지 금방 읽어 낼 수 있다.
strict: 여덟 가지 검사 한 묶음
strict: true 한 줄은 작은 검사 여덟 개를 한꺼번에 켠다. noImplicitAny, strictNullChecks, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitThis, alwaysStrict, useUnknownInCatchVariables 다. TS 6.0 에서는 TypeScript가 이 묶음을 기본으로 켠다. 이전 버전에서는 옵션을 비워 두면 strict 모드가 꺼져 있었지만, 6.0 부터는 strict: true 를 따로 적지 않아도 TypeScript가 그 묶음을 켠다.
이 여덟 개 검사 가운데 자주 부딪히는 항목 하나가 strictNullChecks 다. 함수가 T | undefined 를 돌려주면, 그 값을 쓰는 쪽에서 undefined 가능성을 먼저 확인해야 한다. 다음 파일이 그 규칙을 보여 준다.
// file: strict-mode.ts
export {};
function findUser(id: number): { name: string } | undefined {
if (id === 1) return { name: "Ada" };
return undefined;
}
const u = findUser(2);
// strict 모드에서는 u.name 을 바로 적을 수 없다.
// 컴파일러가 undefined 가능성을 먼저 확인하라고 막기 때문이다.
if (u !== undefined) {
console.log(u.name);
} else {
console.log("no user");
}// file: strict-mode.ts
export {};
function findUser(id: number): { name: string } | undefined {
if (id === 1) return { name: "Ada" };
return undefined;
}
const u = findUser(2);
// strict 모드에서는 u.name 을 바로 적을 수 없다.
// 컴파일러가 undefined 가능성을 먼저 확인하라고 막기 때문이다.
if (u !== undefined) {
console.log(u.name);
} else {
console.log("no user");
}bunx tsc --noEmit 으로 검사하면 조용히 끝난다. bunx tsx strict-mode.ts 로 실행하면 no user 한 줄이 나온다. 만약 if (u !== undefined) 분기를 지우고 console.log(u.name) 을 직접 적으면 컴파일러가 'u' is possibly 'undefined' 오류를 바로 낸다. 그 한 줄이 strict 모드의 일이다.

target: 결과 .js 기준
target: "es2025" 한 줄은 결과 .js 가 어느 ECMAScript 표준에 맞춰 적혀야 하는지를 정한다. ES2025 에 추가된 표준 기능을 코드에서 그대로 호출할 수 있고, 컴파일러가 그 호출을 다른 식으로 풀어 적지 않는다는 뜻이다. ES5 같은 옛 타깃은 TS 6.0 에서 deprecated 표시가 붙었고, 새로 만드는 프로젝트에서는 ES2020 이하를 적지 않는다.
타깃이 허용하는 예를 코드로 본다. ES2024 에서 표준에 들어온 Object.groupBy 를 ES2025 lib 에서는 그대로 쓸 수 있다.
// file: target-es2025.ts
export {};
const items = [
{ kind: "fruit", name: "apple" },
{ kind: "fruit", name: "banana" },
{ kind: "veg", name: "kale" },
] as const;
const grouped = Object.groupBy(items, (item) => item.kind);
console.log(grouped.fruit?.length);
console.log(grouped.veg?.length);// file: target-es2025.ts
export {};
const items = [
{ kind: "fruit", name: "apple" },
{ kind: "fruit", name: "banana" },
{ kind: "veg", name: "kale" },
] as const;
const grouped = Object.groupBy(items, (item) => item.kind);
console.log(grouped.fruit?.length);
console.log(grouped.veg?.length);bunx tsx target-es2025.ts 는 두 줄, 2 와 1 을 출력한다. tsc --noEmit 도 조용히 통과한다. ES5 타깃이면 컴파일러는 lib 정의에서 Object.groupBy 를 찾지 못해서 여기서 오류를 낸다. ES2025 타깃에서는 같은 호출을 그대로 적어도 된다.

module + moduleResolution: nodenext 조합
module: "nodenext" 와 moduleResolution: "nodenext" 는 보통 함께 적는다. ESM 과 CJS 가 한 프로젝트에 섞이는 Node 의 표준 동작을 그대로 따라가려는 조합이기 때문이다. package.json 의 "type": "module" 표시, node: 프로토콜 import, ESM 의 모듈 최상단 await 가 이 조합에서 함께 자연스럽게 동작한다.
아래 짧은 파일이 그 셋을 한꺼번에 보여준다.
// file: nodenext-import.ts
export {};
import { setTimeout as wait } from "node:timers/promises";
await wait(5);
console.log("nodenext + node: import + top-level await OK");// file: nodenext-import.ts
export {};
import { setTimeout as wait } from "node:timers/promises";
await wait(5);
console.log("nodenext + node: import + top-level await OK");이 파일은 세 가지를 동시에 보여 준다. node:timers/promises 같은 node: 프로토콜 import, ESM 환경에서만 가능한 모듈 최상단 await, 그리고 첫 줄 export {}; 로 남겨 둔 ESM 표시다. tsc --noEmit 은 세 가지 모두를 그대로 통과시키고, tsx nodenext-import.ts 는 5밀리초 뒤에 한 줄을 출력한다. module: "nodenext" 가 빠지면 컴파일러는 모듈 최상단 await 에서 멈춘다.

verbatimModuleSyntax: import type 규칙
verbatimModuleSyntax: true 는 import 문에 직접 영향을 준다. 타입에만 쓰는 import 는 import type 으로 적어야 하고, 값을 쓰는 import 는 일반 import 로 적어야 한다. 컴파일러가 한쪽을 다른 쪽으로 바꿔 주지 않는다.
다음 파일은 그 규칙을 바로 보여 준다. node:http 의 IncomingMessage 는 타입으로만 쓰기 때문에 import type 으로 가져온다.
// file: verbatim-syntax.ts
export {};
import type { IncomingMessage } from "node:http";
function methodOf(req: IncomingMessage): string {
return req.method ?? "GET";
}
const fakeReq = { method: "POST" } as unknown as IncomingMessage;
console.log(methodOf(fakeReq));// file: verbatim-syntax.ts
export {};
import type { IncomingMessage } from "node:http";
function methodOf(req: IncomingMessage): string {
return req.method ?? "GET";
}
const fakeReq = { method: "POST" } as unknown as IncomingMessage;
console.log(methodOf(fakeReq));import type 는 컴파일 시점에만 쓰이는 import 라는 약속을 분명히 남긴다. 컴파일러는 결과 .js 에 그 줄을 남기지 않는다. 일반 import 를 적으면 verbatimModuleSyntax 가 IncomingMessage 를 타입 전용 import 로 가져오라고 바로 오류를 낸다.
이 규칙의 가치는 두 가지다. 첫째, 코드만 읽어도 어느 import 가 런타임 의존인지, 어느 import 가 타입 의존인지 바로 구분할 수 있다. 둘째, 컴파일러가 import 문을 자동으로 바꾸지 않으니 결과 .js 가 어떻게 남는지도 예측하기 쉽다. 값과 타입을 왜 나눠 적는지도 import 문만 보면 바로 알 수 있다.

deprecated 된 옵션도 있다
TS 6.0 에서는 함께 deprecated 된 옵션도 짚고 넘어가야 한다. --outFile 은 여러 .ts 파일을 하나의 .js 로 묶어 쓰던 옛 방식이다. ESM 표준이 보편화되면서 쓸 일이 줄었고 deprecated 표시가 붙었다. --baseUrl + paths 로 모듈 경로를 직접 매핑하던 방식도 표준 imports 필드(package.json#imports) 쪽으로 이동하고 있다. 새 프로젝트에서는 두 옵션을 보통 적지 않는다. 적어 두면 컴파일러가 deprecation 경고를 띄운다.
다섯 줄을 먼저 고정한다
이 다섯 줄을 먼저 정해 두면 TypeScript 프로젝트의 기본 설정이 흔들리지 않는다. strict 는 검사 강도를 정하고, target 은 결과 .js 가 따를 ECMAScript 기준을 정한다. module 과 moduleResolution 은 Node 의 모듈 해석 방식을 맞추고, verbatimModuleSyntax 는 import 문에 타입용과 값용 구분을 남긴다.
이 기준이 먼저 잡혀 있으면 예제 코드를 읽을 때 설정 설명으로 자꾸 되돌아갈 필요가 없다. 그다음에는 코드가 실제로 무엇을 하는지에 집중하면 된다.











