이번 편의 결과물: 루트에서 tsc -b 한 번으로 shared-types → web 순서로 빌드되고, 의도적으로 순환 참조를 만들어 오류를 확인합니다. · 다루는 개념: composite: true, tsconfig.json의 references 배열, tsc -b 증분 빌드, 순환 참조 오류
15편에서 apps/web이 @bookshelf/shared-types를 workspace: 프로토콜로 참조하게 됐습니다. pnpm install이 만든 심볼릭 링크 덕분에 실행은 잘 되지만, 지금 두 패키지의 tsconfig.json은 서로를 전혀 모릅니다. 그래서 편집기가 shared-types의 타입을 매번 소스부터 다시 읽고, 두 패키지를 한 번에 타입 검사하려면 각각 따로 명령을 실행해야 합니다. 이 편에서 composite와 references로 두 패키지의 빌드 의존 관계를 명시하고, tsc -b로 순서를 자동 관리합니다.
이 편에서 만드는 파일
bookshelf-ts/
├── tsconfig.json (+, 모노레포 루트 솔루션 파일)
├── package.json (~, typescript devDependency 추가, typecheck 스크립트를 tsc -b로 교체)
├── packages/shared-types/
│ └── tsconfig.json (~, composite: true 추가)
└── apps/web/
└── tsconfig.app.json (~, shared-types에 대한 references 추가)개념 정리
composite: 다른 프로젝트가 참조할 수 있게 만드는 표시
composite: true를 켠 프로젝트는 “내 타입 정보를 다른 프로젝트가 가져다 쓸 수 있다”고 선언하는 것입니다. 이 옵션은 declaration(.d.ts 생성)이 켜져 있을 것을 요구하고, 소스 파일이 전부 include나 files에 잡혀 있을 것을 요구합니다. 반대로, 남을 참조하기만 하고 자신은 아무도 참조하지 않는 프로젝트(apps/web)는 composite가 필요 없습니다.
references 배열
한 tsconfig.json이 다른 프로젝트의 산출물에 의존한다는 것을 references로 표시합니다.
{
"references": [{ "path": "../../packages/shared-types" }]
}path는 다른 프로젝트의 tsconfig.json이 있는 폴더(또는 파일 자체)를 가리킵니다. 이 배열이 있으면 tsc가 “이 프로젝트를 검사하기 전에 저 프로젝트부터 최신 상태로 만들어야 한다”는 것을 압니다.
tsc -b: 순서를 아는 빌드
일반 tsc는 지정된 프로젝트 하나만 검사합니다. tsc -b(빌드 모드)는 references를 따라가며 의존하는 프로젝트부터 먼저 빌드하고, 이미 최신 상태인 프로젝트는 건너뜁니다.
tsc -b # 현재 폴더의 tsconfig.json 기준
tsc -b --verbose # 어떤 프로젝트를 빌드·스킵했는지 출력
tsc -b --force # 전부 오래된 것으로 간주하고 다시 빌드순환 참조는 빌드 자체를 막는다
A가 B를 참조하고 B가 다시 A를 참조하면, “어느 쪽을 먼저 빌드해야 하는지” 결정할 수 없습니다. tsc -b는 이런 순환을 감지하면 아무것도 빌드하지 않고 즉시 오류를 냅니다.
실습
1. shared-types를 참조 가능하게 표시
// bookshelf-ts/packages/shared-types/tsconfig.json
{
"compilerOptions": {
"target": "ES2023",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"skipLibCheck": true,
"composite": true,
"declaration": true,
"outDir": "./dist"
},
"include": ["src"]
}composite: true를 추가했습니다. 이미 declaration: true와 include가 있었으므로 요구 조건은 그대로 충족됩니다.
2. apps/web에서 shared-types를 참조로 명시
// bookshelf-ts/apps/web/tsconfig.app.json
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023", "DOM"],
"module": "esnext",
"types": ["vite/client"],
"allowArbitraryExtensions": true,
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "../../packages/shared-types" }]
}apps/web은 shared-types의 산출물을 가져다 쓰는 쪽이라 composite가 필요 없습니다. noEmit: true는 그대로 둡니다. 실제 번들링은 여전히 vite가 담당하고, tsc -b는 타입 검사와 빌드 순서 관리만 맡습니다.
3. 모노레포 루트 솔루션 파일 작성
// bookshelf-ts/tsconfig.json
{
"files": [],
"references": [
{ "path": "packages/shared-types" },
{ "path": "apps/web" }
]
}// bookshelf-ts/package.json (일부)
{
"devDependencies": {
"typescript": "7.0.2"
},
"scripts": {
"dev": "pnpm --filter @bookshelf/web dev",
"build": "pnpm -r build",
"test": "pnpm -r test",
"lint": "pnpm -r lint",
"typecheck": "tsc -b"
}
}루트 typecheck 스크립트가 각 패키지의 tsc -b를 따로 실행하던 pnpm -r typecheck 대신, 루트 tsconfig.json을 기준으로 전체를 한 번에 빌드하는 tsc -b로 바뀌었습니다.
4. 설치와 실행
pnpm install
pnpm typecheck5. 순환 참조 오류 직접 만들어보기
shared-types가 apps/web을 참조하도록 잘못 추가해봅니다.
// bookshelf-ts/packages/shared-types/tsconfig.json (임시로 추가)
{
"compilerOptions": {
"composite": true,
"declaration": true,
"outDir": "./dist"
},
"include": ["src"],
"references": [{ "path": "../../apps/web/tsconfig.app.json" }]
}이제 shared-types는 apps/web을, apps/web은 다시 shared-types를 참조합니다.
pnpm typecheckProject references may not form a circular graph. Cycle detected: packages/shared-types/tsconfig.json, apps/web/tsconfig.app.json원인을 확인했으면 방금 추가한 references 항목을 지워 원래 상태로 되돌립니다. shared-types는 도메인 타입만 담는 패키지라 애초에 apps/web을 알 이유가 없습니다.
확인
- 순환 참조를 만들기 전,
pnpm typecheck를 실행하면packages/shared-types/dist/에book.js·book.d.ts가 생기고 오류 없이 끝납니다. - 같은 명령을 한 번 더 실행하면 아무것도 다시 빌드하지 않고 즉시 끝납니다(이미 최신 상태이기 때문).
packages/shared-types/src/book.ts를 한 글자 고치고 다시 실행하면 그 프로젝트만 다시 빌드됩니다.- 순환 참조를 임시로 추가한 상태에서 실행하면 빌드가 진행되지 않고 순환을 알리는 오류만 출력됩니다.
- 오류 원인이 된
references항목을 지우면 다시 정상적으로 빌드됩니다.
직접 해보기
pnpm typecheck -- --verbose(또는 루트에서tsc -b --verbose)를 실행해, 어떤 프로젝트를 빌드했고 어떤 프로젝트를 건너뛰었는지 로그로 확인해보세요.packages/shared-types/dist폴더를 삭제한 뒤 다시pnpm typecheck를 실행해, 산출물이 없을 때는 최신 여부와 상관없이 다시 빌드되는지 확인해보세요.
정답 보기
--verbose 실행 결과에는 대략 다음과 같은 줄이 섞여 나옵니다.
Project 'packages/shared-types/tsconfig.json' is out of date because output file 'packages/shared-types/dist/book.js' does not exist
Building project 'packages/shared-types/tsconfig.json'...
Project 'apps/web/tsconfig.app.json' is up to date because newest input 'apps/web/src/...' is older than output '...tsbuildinfo'dist 폴더를 지우면 산출물 자체가 없어져 tsc -b가 “출력 파일이 없다”는 이유로 무조건 다시 빌드합니다. 소스가 안 바뀌었어도 산출물이 사라지면 최신 상태로 보지 않습니다.
자주 하는 실수
| 증상 | 원인 | 고치는 법 |
|---|---|---|
shared-types를 고쳐도 apps/web에서 타입이 안 바뀜 | apps/web/tsconfig.app.json에 references를 추가하지 않음 | 참조 대상 경로를 정확히 추가하고 pnpm typecheck를 다시 실행한다 |
composite를 켰는데 오류가 남 | declaration이 꺼져 있거나 소스가 include에 안 잡힘 | composite를 쓰는 프로젝트는 declaration: true와 정확한 include가 함께 필요하다 |
| 순환 참조 오류가 나는데 원인을 못 찾음 | 참조 방향을 반대로 착각함(누가 누구를 쓰는지) | 실제로 코드를 가져다 쓰는 쪽에서 쓰이는 쪽으로만 references를 건다. shared-types는 누구도 참조하면 안 된다 |