TypeScript 프로젝트의 tsconfig.json 최적화, 프로젝트 참조, Isolated Declarations, 모노레포에서의 타입 전략을 실전 중심으로 다룹니다.
10장까지 TypeScript 타입 시스템의 심화 기능을 다뤘습니다. 이 장에서는 시선을 바꿔 프로젝트 설정과 빌드 인프라에 집중합니다. 아무리 정교한 타입을 설계해도, tsconfig.json이 올바르게 구성되지 않으면 그 효과를 발휘할 수 없습니다. 특히 모노레포(monorepo) 환경에서의 타입 관리는 프로젝트 규모가 커질수록 핵심적인 과제가 됩니다.
TypeScript 5.x에서 추가되거나 변경된 주요 옵션을 정리합니다.
{
"compilerOptions": {
// === 타입 검사 ===
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noFallthroughCasesInSwitch": true,
// === 모듈 시스템 ===
"module": "nodenext",
"moduleResolution": "nodenext",
"verbatimModuleSyntax": true,
"rewriteRelativeImportExtensions": true,
// === 출력 ===
"target": "es2024",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
// === 5.x 신규 옵션 ===
"erasableSyntaxOnly": true,
"isolatedDeclarations": true
}
}strict: true는 여러 하위 옵션을 한 번에 활성화합니다. 각 옵션이 어떤 검사를 수행하는지 이해하면 문제 해결에 도움됩니다.
{
"compilerOptions": {
"strict": true,
// 위 옵션은 아래를 모두 활성화:
// "strictNullChecks": true, — null/undefined 엄격 검사
// "strictFunctionTypes": true, — 함수 파라미터 반변성 검사
// "strictBindCallApply": true, — bind/call/apply 타입 검사
// "strictPropertyInitialization": true, — 클래스 프로퍼티 초기화 검사
// "noImplicitAny": true, — 암시적 any 금지
// "noImplicitThis": true, — 암시적 this 타입 금지
// "alwaysStrict": true, — "use strict" 강제
// "useUnknownInCatchVariables": true — catch의 error를 unknown으로
}
}인덱스 시그니처로 접근할 때 결과에 undefined를 포함합니다. 배열의 범위 밖 접근이나 존재하지 않는 키 접근을 방지합니다.
const arr = [1, 2, 3];
// noUncheckedIndexedAccess: false
arr[0]; // number
// noUncheckedIndexedAccess: true
arr[0]; // number | undefined
// 안전한 접근
const first = arr[0];
if (first !== undefined) {
console.log(first.toFixed(2)); // OK
}import type과 import 구문의 의미를 엄격하게 구분합니다. 이전의 importsNotUsedAsValues와 preserveValueImports를 대체합니다.
// 타입만 import할 때는 반드시 'import type' 사용
import type { User } from "./types";
// 값을 import할 때는 일반 import 사용
import { createUser } from "./utils";
// 혼합: import에서 타입과 값을 구분
import { createUser, type UserConfig } from "./users";Node.js의 --experimental-strip-types와 호환되는 코드만 허용합니다. 런타임 의미를 가진 TypeScript 전용 구문(enum, namespace, 파라미터 프로퍼티)을 금지합니다.
// 사용 불가
enum Status { Active, Inactive } // Error
namespace Utils { export function helper() {} } // Error
class Service {
constructor(private name: string) {} // Error: 파라미터 프로퍼티
}
// 대안
const Status = { Active: 0, Inactive: 1 } as const;
type Status = (typeof Status)[keyof typeof Status];
function helper() {}
export { helper as Utils_helper };
class Service {
private name: string;
constructor(name: string) {
this.name = name;
}
}소스 코드에서 .ts 확장자를 사용하고, 출력에서 자동으로 .js로 변환합니다.
// 소스 코드
import { helper } from "./utils.ts";
import { config } from "../config.ts";
// 출력 (.js)
import { helper } from "./utils.js";
import { config } from "../config.js";rewriteRelativeImportExtensions는 .ts → .js, .tsx → .jsx, .mts → .mjs, .cts → .cjs 변환을 지원합니다. Node.js ESM에서 확장자가 필수인 환경에서 특히 유용합니다.
프로젝트 참조는 대규모 TypeScript 프로젝트를 여러 하위 프로젝트로 분할하여 빌드를 최적화하는 메커니즘입니다.
project/
tsconfig.json # 루트 (참조만 정의)
packages/
shared/
tsconfig.json # composite: true
src/
index.ts
api/
tsconfig.json # references: [{ path: "../shared" }]
src/
server.ts
web/
tsconfig.json # references: [{ path: "../shared" }]
src/
app.ts
{
"compilerOptions": {
"composite": true,
"declaration": true,
"declarationMap": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src"]
}{
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"references": [
{ "path": "../shared" }
],
"include": ["src"]
}{
"files": [],
"references": [
{ "path": "./packages/shared" },
{ "path": "./packages/api" },
{ "path": "./packages/web" }
]
}# 전체 빌드 (의존성 순서 자동 해결)
tsc --build
# 특정 프로젝트만 빌드
tsc --build packages/api
# 강제 재빌드
tsc --build --force
# 클린 빌드
tsc --build --cleancomposite: true는 프로젝트 참조의 핵심입니다. 이 옵션을 켜면 다음이 강제됩니다.
declaration: true 자동 활성화rootDir 설정 필수include 패턴에 포함되어야 함.tsbuildinfo 파일을 생성하여 증분 빌드 지원Isolated Declarations는 .d.ts 파일 생성을 타입 검사 없이 수행할 수 있게 합니다. 이를 통해 선언 파일 생성을 별도 도구(swc, oxc 등)에 위임하고, 빌드를 병렬화할 수 있습니다.
{
"compilerOptions": {
"isolatedDeclarations": true,
"declaration": true
}
}이 모드에서는 공개 API의 타입이 명시적이어야 합니다. TypeScript가 추론에 의존하지 않고도 .d.ts를 생성할 수 있어야 합니다.
// Error: 반환 타입이 명시되지 않음
export function add(a: number, b: number) {
return a + b;
}
// OK: 반환 타입 명시
export function add(a: number, b: number): number {
return a + b;
}
// 내부 함수는 추론 가능 (export되지 않으므로)
function internal(x: number) {
return x * 2; // OK
}Isolated Declarations는 모노레포에서 특히 유용합니다. 각 패키지의 .d.ts 생성을 병렬로 수행할 수 있어 빌드 시간이 크게 단축됩니다. 다만 공개 API에 모두 명시적 타입 주석이 필요하므로, 기존 프로젝트에 적용할 때는 점진적으로 진행하는 것이 좋습니다.
모노레포의 내부 패키지는 빌드 없이 TypeScript 소스를 직접 참조하는 전략과, 빌드된 결과물을 참조하는 전략이 있습니다.
// packages/ui/package.json
{
"name": "@myorg/ui",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": {
"types": "./src/index.ts",
"default": "./src/index.ts"
}
}
}// packages/ui/package.json
{
"name": "@myorg/ui",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
}
}모노레포에서는 공통 설정을 루트 tsconfig.base.json에 정의하고, 각 패키지가 이를 상속합니다.
{
"compilerOptions": {
"strict": true,
"target": "es2024",
"module": "nodenext",
"moduleResolution": "nodenext",
"verbatimModuleSyntax": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"skipLibCheck": true,
"noUncheckedIndexedAccess": true,
"isolatedModules": true
}
}{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"outDir": "./dist",
"rootDir": "./src"
},
"references": [
{ "path": "../shared" }
],
"include": ["src"]
}모노레포에서 패키지 간 참조를 위한 경로 별칭을 설정합니다.
{
"compilerOptions": {
"paths": {
"@myorg/shared": ["../shared/src"],
"@myorg/shared/*": ["../shared/src/*"],
"@myorg/ui": ["../ui/src"],
"@myorg/ui/*": ["../ui/src/*"]
}
}
}paths 설정은 TypeScript 컴파일러의 모듈 해석에만 영향을 주며, 런타임 번들러(Webpack, Vite, esbuild)에는 별도의 별칭 설정이 필요합니다. 번들러와 TypeScript의 경로 설정이 일치하지 않으면 "모듈을 찾을 수 없음" 에러가 발생합니다.
{
"dependencies": {
"@myorg/shared": "workspace:*"
},
"devDependencies": {
"@types/node": "^20.0.0",
"typescript": "^5.8.0"
}
}{
"compilerOptions": {
"incremental": true,
"tsBuildInfoFile": "./.tsbuildinfo"
}
}.tsbuildinfo 파일은 이전 빌드의 정보를 캐싱하여, 변경된 파일만 다시 컴파일합니다.
{
"compilerOptions": {
"skipLibCheck": true
}
}skipLibCheck은 .d.ts 파일의 타입 검사를 건너뜁니다. 외부 라이브러리의 타입 선언에 오류가 있어도 빌드가 통과하며, 빌드 시간이 크게 단축됩니다.
대규모 프로젝트에서는 타입 검사와 JavaScript 빌드를 분리하는 것이 일반적입니다.
{
"scripts": {
"typecheck": "tsc --noEmit",
"build": "esbuild src/index.ts --bundle --outdir=dist",
"build:types": "tsc --emitDeclarationOnly",
"ci": "pnpm typecheck && pnpm build && pnpm build:types"
}
}esbuild, swc, Vite 등의 빌드 도구는 TypeScript 타입을 제거만 하고 타입 검사는 수행하지 않습니다. 타입 검사는 CI/CD 파이프라인에서 tsc --noEmit으로 별도 수행하는 것이 일반적인 패턴입니다.
| 전략 | 용도 | 특징 |
|---|---|---|
node16 / nodenext | Node.js 프로젝트 | package.json의 exports 필드 존중, ESM/CJS 구분 |
bundler | 번들러 사용 프로젝트 | exports 존중, 확장자 선택적, 가장 유연 |
node (레거시) | 기존 CJS 프로젝트 | main 필드만 참조, exports 무시 |
{
"compilerOptions": {
"module": "esnext",
"moduleResolution": "bundler"
}
}{
"compilerOptions": {
"module": "nodenext",
"moduleResolution": "nodenext"
}
}TypeScript 프로젝트 설정은 타입 안전성, 빌드 성능, 개발자 경험에 직접적인 영향을 줍니다. 5.x에서 추가된 verbatimModuleSyntax, rewriteRelativeImportExtensions, erasableSyntaxOnly, isolatedDeclarations는 모듈 시스템의 명확성과 빌드 효율성을 크게 개선합니다. 모노레포에서는 프로젝트 참조, tsconfig 상속, Isolated Declarations를 조합하여 확장 가능한 타입 인프라를 구축할 수 있습니다.
다음 장에서는 이 시리즈의 마무리로, 지금까지 배운 모든 내용을 종합하여 타입 안전 유틸리티 라이브러리를 처음부터 설계하고 구현하는 실전 프로젝트를 진행합니다.
이 글이 도움이 되셨나요?
관련 주제 더 보기
TypeScript 5.x의 고급 타입 기법을 총동원하여 타입 안전 유틸리티 라이브러리를 처음부터 설계하고 구현하는 실전 프로젝트입니다.
TypeScript 타입 시스템을 프로그래밍 언어로 활용하는 고급 기법 — 산술 연산, 문자열 파서, 상태 머신 등을 타입만으로 구현하는 패턴을 다룹니다.
TypeScript 5.4의 NoInfer 유틸리티 타입과 5.x에서 추가된 새로운 타입 도구들을 활용한 라이브러리 설계 패턴을 다룹니다.