TypeScript 신규 프로젝트를 위한 tsconfig 설정
1. 개요
파일 tsconfig.json은 TypeScript가 타입을 검사하고 코드를 컴파일하는 방식을 결정합니다. 설정이 잘못되면 런타임에 import 오류가 발생하거나 컴파일러가 많은 타입 오류를 놓칠 수 있습니다.
이 글에서는 Node.js 신규 프로젝트를 위한 tsconfig.json을 처음부터 구성합니다. 엄격한 검사 플래그를 활성화하고, 올바른 모듈 시스템을 선택하고, build 설정과 test 설정을 분리합니다. 글을 읽은 후 바로 사용할 수 있는 설정 모음을 얻게 되고 각 라인을 이해할 수 있습니다.
2. 프로젝트 초기화
디렉토리를 만들고 TypeScript를 개발 의존성으로 설치합니다:
mkdir order-service && cd order-service
npm init -y
npm install --save-dev typescript @types/node
npx tsc --init
tsc --init 명령은 많은 주석이 포함된 tsconfig.json을 생성합니다. 이 파일은 참고용으로 유용하지만 유지보수가 어렵습니다. 이를 간결하고 각 옵션을 명확히 기록한 버전으로 바꾸겠습니다.
다음으로 package.json에서 프로젝트가 ES Module을 사용함을 선언합니다:
{
"name": "order-service",
"type": "module",
"scripts": {
"typecheck": "tsc --noEmit",
"build": "tsc -p tsconfig.build.json",
"start": "node dist/index.js"
}
}
"type": "module" 줄은 Node.js에 .js 파일이 ESM임을 알립니다. 이 선택은 TypeScript가 모듈을 해석하는 방식에 직접 영향을 미치며, 3절에서 볼 수 있습니다.
3. Node.js를 위한 최소 설정
다음이 기본 tsconfig.json입니다:
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
"moduleResolution": "NodeNext",
"types": ["node"],
"rootDir": ".",
"outDir": "dist",
"sourceMap": true,
"declaration": false,
"strict": true,
"skipLibCheck": true
},
"include": ["src", "tests"]
}
3.1. target, lib 및 types
target은 컴파일러가 출력하는 JavaScript 버전입니다.ES2022는 Node.js 18 이상에 적합합니다. 이 target에서는async/await, 선택적 연결, 클래스 필드가 유지되며 하향 변환되지 않습니다.lib은 런타임에 사용할 수 있는 API를 선언합니다."DOM"이 없으므로 서버 코드에서 실수로document나window를 호출하지 않습니다.types: ["node"]은 Node.js의 타입만 로드합니다. 선언하지 않으면 TypeScript는node_modules/@types의 모든 패키지를 자동으로 로드합니다. 이는 때때로 원하지 않는 전역 타입을 끌어올 수 있습니다.
3.2. module 및 moduleResolution
Node.js에서 직접 실행되는 프로젝트의 경우 두 옵션 모두에 NodeNext를 사용하십시오. 이 모드에서 TypeScript는 Node.js가 import를 해석하는 방식을 정확히 시뮬레이션합니다. 이는 package.json의 "type" 및 "exports" 필드를 읽으며, 상대 import에 파일 확장자를 필수로 요구합니다.
두 파일이 있다고 가정합니다:
// src/order.ts
export interface Order {
id: string;
items: string[];
note?: string;
}
export function totalItems(order: Order): number {
return order.items.length;
}
// src/index.ts
import { totalItems } from "./order";
console.log(totalItems({ id: "A1", items: ["pho", "tra-da"] }));
npx tsc --noEmit을 실행하면 오류가 보고됩니다:
src/index.ts:1:28 - error TS2835: Relative import paths need explicit file extensions
in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'.
Did you mean './order.js'?
import { totalItems } from "./order.js";로 수정해야 합니다. 확장자는 .ts가 아닌 .js입니다. 런타임에 Node.js는 컴파일된 파일을 읽기 때문입니다. 이 규칙은 처음에는 낯설지만 배포 시 ERR_MODULE_NOT_FOUND 오류를 피하는 데 도움이 됩니다.
3.3. rootDir 및 outDir
outDir은 build 후 .js 파일이 있는 위치입니다. rootDir은 outDir 내 디렉토리 구조를 결정합니다. 기본 파일에서 rootDir은 include에 src와 tests 모두 포함되어 있으므로 "."입니다. 6절에서는 build 파일이 rootDir을 src로 변경하여 dist/index.js가 dist/src/index.js 대신 올바른 위치에 있도록 합니다.
4. Strict mode 및 추가 검사 플래그
strict: true 플래그는 strictNullChecks, noImplicitAny, strictFunctionTypes를 포함한 검사 그룹을 활성화합니다. 신규 프로젝트의 경우 항상 이 플래그를 활성화해야 합니다. 나중에 큰 코드베이스에 대해 활성화하려면 훨씬 더 많은 작업이 필요합니다.
strict 외에도 다음 플래그를 추가해야 합니다:
{
"compilerOptions": {
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true
}
}
이 라인들을 기본 파일의 compilerOptions 블록에 병합합니다. 처음 두 플래그는 더 자세히 설명할 가치가 있습니다.
4.1. noUncheckedIndexedAccess
기본적으로 TypeScript는 인덱스로 검색한 요소가 항상 존재한다고 간주합니다. 실제로는 그렇지 않습니다:
import type { Order } from "./order.js";
const ordersById: Record<string, Order> = {};
const order = ordersById["A1"];
console.log(order.id);
이 플래그를 활성화하면 order의 타입은 Order | undefined이고 컴파일러가 보고합니다:
error TS18048: 'order' is possibly 'undefined'.
사용하기 전에 반드시 확인해야 합니다: if (order) { console.log(order.id); }. 플래그를 비활성화하면 위 코드는 여전히 컴파일되지만 런타임에 TypeError를 던집니다.
4.2. exactOptionalPropertyTypes
이 플래그는 ’속성이 없음’과 ’속성 값이 undefined’를 구분합니다:
import type { Order } from "./order.js";
const order: Order = { id: "A2", items: [], note: undefined };
error TS2375: Type '{ id: string; items: never[]; note: undefined; }' is not assignable
to type 'Order' with 'exactOptionalPropertyTypes: true'.
이 구분은 Object.keys, in 연산자를 사용하거나 JSON으로 직렬화할 때 중요합니다. 정말로 undefined를 허용하려면 명시적으로 선언하십시오: note?: string | undefined.
참고:
exactOptionalPropertyTypes는 때때로 타사 라이브러리의 타입과 충돌합니다. 특히 form 처리 및 ORM 라이브러리입니다. 이 플래그가 제어 범위를 벗어난 너무 많은 오류를 일으키면 비활성화하십시오.noUncheckedIndexedAccess는 유지해야 합니다.
5. 모듈 간 상호작용
compilerOptions에 다음 두 플래그를 추가합니다:
{
"compilerOptions": {
"verbatimModuleSyntax": true,
"isolatedModules": true
}
}
verbatimModuleSyntax는 간단한 규칙이 있습니다. type이 없는 import는 output에 유지되고 type이 있는 import는 제거됩니다. 따라서 컴파일 결과를 예측하기 쉽습니다. 대신 타입만 사용하는 import를 명확히 표시해야 합니다:
// 오류: Order는 interface일 뿐입니다
import { Order } from "./order.js";
error TS1484: 'Order' is a type and must be imported using a type-only import
when 'verbatimModuleSyntax' is enabled.
// 올바름
import type { Order } from "./order.js";
import { totalItems } from "./order.js";
isolatedModules은 각 파일을 독립적으로 컴파일할 수 있도록 보장합니다. esbuild, swc, Vitest와 같은 도구는 모두 한 번에 한 파일씩 처리하므로 이 플래그는 오류가 발생하기 전에 잡는 데 도움이 됩니다.
많은 오래된 가이드에서 esModuleInterop을 봤을 수 있습니다. module: NodeNext에서는 이 플래그가 기본적으로 활성화되어 있으므로 추가로 선언할 필요가 없습니다.
6. typecheck 및 build 설정 분리
기본 tsconfig.json 파일은 editor와 typecheck 명령을 제공합니다. 이는 tests를 포함합니다. build할 때 test 파일을 dist에 포함하고 싶지 않습니다. 따라서 tsconfig.build.json을 추가로 만듭니다:
{
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"noEmit": false
},
"include": ["src"],
"exclude": ["**/*.test.ts"]
}
extends는 기본 파일에서 모든 compilerOptions를 상속합니다. 자식 파일에서 include와 exclude는 부모 파일의 값을 완전히 대체하며 병합하지 않습니다. build 명령을 실행합니다:
npm run build
ls dist
index.js index.js.map order.js order.js.map
dist 디렉토리는 rootDir: "src"의 덕분에 애플리케이션 코드만 포함하며 루트에 있습니다.
6.1. Vitest로 테스트 작성
Vitest를 설치합니다:
npm install --save-dev vitest
scripts에 "test": "vitest run"을 추가한 다음 totalItems 함수에 대한 테스트를 작성합니다:
// tests/order.test.ts
import { describe, expect, it } from "vitest";
import type { Order } from "../src/order.js";
import { totalItems } from "../src/order.js";
describe("totalItems", () => {
it("주문의 상품 개수를 올바르게 계산합니다", () => {
const order: Order = { id: "A1", items: ["pho", "tra-da", "quay"] };
expect(totalItems(order)).toBe(3);
});
it("빈 주문의 경우 0을 반환합니다", () => {
expect(totalItems({ id: "A2", items: [] })).toBe(0);
});
});
npm run typecheck && npm test
✓ tests/order.test.ts (2 tests)
Test Files 1 passed (1)
Tests 2 passed (2)
Vitest는 실행할 때 타입을 자동으로 제거하지만 검사하지는 않습니다. 따라서 CI에서 항상 typecheck을 실행하십시오. 그렇지 않으면 타입 오류가 있는 테스트도 여전히 ’통과’할 수 있습니다.
7. bundler를 사용하는 프로젝트의 변형
Vite, esbuild 또는 webpack으로 코드를 번들링하는 경우 Node.js에서 직접 실행하는 대신 모듈 부분을 다음과 같이 변경합니다:
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "Bundler",
"noEmit": true,
"jsx": "react-jsx"
}
}
moduleResolution: "Bundler"은 bundler가 자동으로 해석하기 때문에 파일 확장자 없이 import를 허용합니다.noEmit: true는tsc가 타입만 검사하고 bundler가 파일 출력을 처리한다는 의미입니다.- 코드가 브라우저에서 실행되므로
"DOM"을 추가합니다.jsx라인은 React를 사용할 때만 필요합니다.
참고:
node dist/index.js로 직접 실행되는 코드에Bundler를 사용하지 마십시오. 컴파일러는.js확장자가 없는 import를 수락하지만 Node.js는 런타임에 오류를 보고합니다.
8. 흔한 오류
paths로 alias를 만들고 tsc로 build한다. "paths": { "@/*": ["src/*"] }를 선언했다고 가정합니다. Editor는 이 alias를 이해하지만 tsc는 output의 경로를 다시 쓰지 않으므로 node는 @/order.js를 찾을 수 없습니다. Node.js 프로젝트의 경우 package.json에서 subpath imports를 사용하십시오:
{
"imports": {
"#src/*": "./dist/*"
}
}
그런 다음 import { totalItems } from "#src/order.js";를 사용하여 import합니다. NodeNext 모드에서 TypeScript는 imports 필드를 읽고 dist에서 src로 역 매핑하므로 editor와 runtime 모두 올바르게 해석합니다.
skipLibCheck를 남용한다. 이 플래그는 .d.ts 파일 검사만 건너뛰어 더 빠른 build를 도와주고 라이브러리 타입 오류를 피합니다. 코드의 안전성을 감소시키지 않습니다. 그러나 .d.ts 파일을 직접 작성하면 해당 파일의 오류도 무시된다는 점을 기억하십시오.
include를 잊어버린다. include가 없으면 TypeScript는 dist를 포함한 전체 디렉토리를 스캔합니다. 결과는 일반적으로 TS5055: Cannot write file ... because it would overwrite input file 오류입니다.
CommonJS와 ESM을 혼합한다. package.json에 "type": "module"이 없으면서도 NodeNext를 사용하면 .ts 파일이 CommonJS로 컴파일됩니다. 이 경우 ESM만 지원하는 패키지를 import하면 오류가 발생합니다. 처음부터 한 방향을 선택하십시오. 신규 프로젝트의 경우 ESM을 선택하는 것이 좋습니다.
strict를 활성화한 후 any를 곳곳에 뿌린다. 이렇게 하면 strict mode 효과가 사라집니다. 데이터의 타입을 모를 때는 unknown을 사용한 다음 type guard 문을 사용하여 타입을 좁히십시오.
9. 결론
이 글은 다음 부분으로 구성된 신규 Node.js 프로젝트용 tsconfig.json을 구성했습니다:
module과moduleResolution을NodeNext로 설정하고 컴파일러가 runtime을 정확히 시뮬레이션하도록"type": "module"을 함께 사용합니다.strict와noUncheckedIndexedAccess,exactOptionalPropertyTypes플래그를 사용하여 조기에 오류를 잡습니다.verbatimModuleSyntax와isolatedModules을 사용하여 output을 예측하기 쉽고 빠른 컴파일 도구와 호환되게 합니다.tsconfig.build.json파일을 분리하여dist가 애플리케이션 코드만 포함하도록 합니다.
프로젝트가 bundler를 사용하는 경우 moduleResolution: "Bundler"와 noEmit: true로 변경하면 됩니다. 엄격한 검사 플래그는 유지됩니다. 마지막으로 CI에서 항상 tsc --noEmit을 실행하십시오. 대부분의 test runner와 bundler 도구는 당신을 대신하여 타입을 검사하지 않기 때문입니다.