프로젝트 레퍼런스
대규모 코드를 여러 composite 프로젝트로 나누고 references와 빌드 순서를 설정해 모노레포 의존 경계를 명확히 합니다.
대규모 타입스크립트 프로젝트를 운영하다 보면
하나의 거대한 tsconfig.json과 단일 컴파일 흐름만으로는
효율적인 관리가 어려워지는 시점이 옵니다.
특히 모노레포(Monorepo)에서 여러 하위 프로젝트가 서로 의존할 때 이 문제가 더 뚜렷하게 드러납니다.
모든 코드를 한 번에 컴파일하면 시간이 오래 걸리고, 작은 변경도 전체 빌드를 다시 시작하게 만들어 생산성을 떨어뜨릴 수 있습니다.
타입스크립트 3.0부터 도입된 프로젝트 레퍼런스(Project References) 기능은 이러한 문제를 해결하기 위한 방법입니다.
이 기능은 큰 타입스크립트 프로젝트를 여러 개의 작은 tsconfig.json 파일을 가진 하위 프로젝트로 분할하고, 이들이 서로의 의존성을 명시적으로 참조할 수 있도록 합니다.
왜 프로젝트 레퍼런스를 사용해야 하는가?
프로젝트 레퍼런스를 사용하면 다음과 같은 주요 이점을 얻을 수 있습니다.
- 가장 큰 장점입니다. 변경된 하위 프로젝트와 그에 의존하는 프로젝트만 다시 컴파일하여 전체 빌드 시간을 크게 단축합니다. 대규모 프로젝트에서는 반복 빌드 비용을 줄이는 기준이 됩니다.
- 각 하위 프로젝트가 명확한 경계를 가지므로, 코드 베이스의 모듈화 수준을 높이고 의존성을 명확하게 파악할 수 있습니다. 이는 아키텍처 설계와 유지보수에 도움이 됩니다.
- 논리적으로 분리된 하위 프로젝트 덕분에 코드 베이스를 리팩토링하거나 새로운 기능을 추가할 때 훨씬 유연하게 대처할 수 있습니다.
- IDE는 전체 프로젝트가 아닌 현재 작업 중인 하위 프로젝트와 그 의존성만 분석하여, 자동 완성 및 타입 검사 성능을 향상시킬 수 있습니다.
프로젝트 레퍼런스 설정 방법
그림은 두 소비자가 shared에 의존하는 확장 구조입니다. 아래 코드는 이를 ui → common 한 갈래로 줄인 예입니다. 화살표는 참조 방향이며 실제 빌드는 의존 대상부터 수행합니다.
PROJECT REFERENCES · BUILD GRAPH
프로젝트 참조는 영향받는 그래프만 다시 빌드한다
app에서 server와 client가 갈라지고 두 프로젝트가 shared 선언에 의존할 때 composite 그래프와 빌드 정보가 순서를 고정한다.
- solution tsconfig가 app
solution tsconfig가 app 그래프를 시작한다.
- server와 client는 shared
server와 client는 shared 프로젝트를 참조한다.
- shared는 declaration 경계를
shared는 declaration 경계를 제공한다.
- tsc -b는 의존
tsc -b는 의존 순서와 영향을 계산한다.
- .tsbuildinfo는 증분 상태이지
.tsbuildinfo는 증분 상태이지 배포 산출물이 아니다.
references는 타입 빌드 그래프를 만들며 런타임 패키지 해석을 자동 구성하지 않는다.
프로젝트 레퍼런스를 사용하려면, 의존성을 가지는 각 하위 프로젝트의 tsconfig.json 파일에 몇 가지 변경 사항을 적용해야 합니다.
common 프로젝트 (생산자) 설정
common 프로젝트는 다른 프로젝트가 의존할 타입 정의 파일(.d.ts)을 생성해야 합니다.
{
"compilerOptions": {
"target": "es2020",
"module": "esnext",
"outDir": "./dist", // 컴파일된 JS 파일과 .d.ts 파일이 저장될 곳
"rootDir": "./src",
"declaration": true, // 필수: .d.ts 파일 생성 활성화
"declarationMap": true, // 선택: .d.ts.map 파일 생성 (원본 TS 파일로 이동 가능)
"composite": true, // 필수: 이 프로젝트가 다른 프로젝트에 의해 참조될 것임을 나타냄
"strict": true
},
"include": [
"src/**/*.ts"
]
}"declaration": true: 이 프로젝트가 다른 프로젝트에서 사용할 타입 정보를 포함하는.d.ts파일을 생성하도록 지시합니다."composite": true: 이 프로젝트가 다른tsconfig.json에 의해 참조될 수 있는 프로젝트임을 나타내는 플래그입니다. 이 옵션에서는 선언 출력이 필요하며declaration의 기본값이true가 됩니다.declarationMap은 별도로 켜야 합니다.
ui 프로젝트 (소비자) 설정
ui 프로젝트는 common 프로젝트가 제공하는 타입에 의존합니다.
references 배열을 사용하여 이 의존성을 명시합니다.
{
"compilerOptions": {
"target": "es2020",
"module": "esnext",
"outDir": "./dist",
"rootDir": "./src",
"strict": true
},
"include": [
"src/**/*.ts"
],
"references": [
{ "path": "../common" } // common 프로젝트를 참조합니다.
]
}"references": []: 이 배열 안에 현재 프로젝트가 의존하는 다른tsconfig.json파일의 경로를 지정합니다. 경로는 현재tsconfig.json파일을 기준으로 합니다.
tsconfig.json (빌드 관리)
모노레포의 루트에 있는 tsconfig.json은 일반적으로 직접 코드를 컴파일하기보다는, 하위 프로젝트들의 빌드 순서를 관리하고 증분 빌드를 트리거하는 역할을 합니다.
{
"files": [], // 이 tsconfig 자체는 컴파일할 파일이 없으므로 비워둡니다.
"references": [
{ "path": "./packages/common" }, // common 프로젝트를 먼저 빌드해야 합니다.
{ "path": "./packages/ui" } // ui 프로젝트는 common에 의존하므로 그 다음에 빌드됩니다.
]
}"files": []: 이 루트tsconfig.json은 직접 소스 파일을 컴파일하지 않고, 단지 하위 프로젝트들을 참조만 하므로files나include를 비워두는 것이 일반적입니다."references": []: 여기에 모든 하위 프로젝트를 나열합니다.tsc --build명령을 사용하면 TypeScript는 이references배열을 기반으로 올바른 빌드 순서를 자동으로 추론합니다.
프로젝트 레퍼런스 사용하기
원문의 ui 설정 두 곳에는 composite가 빠져 있습니다. 루트가 ui도 참조하므로 실행 전 두 설정 모두에 composite: true를 추가해야 합니다. 위 조각을 그대로 둔 상태를 빌드 성공 예제로 보아서는 안 됩니다. 설정을 완성한 후 다음 명령을 사용합니다.
# 루트 디렉토리에서 실행
tsc --build # 또는 tsc -btsc --build 명령은 다음과 같은 작업을 수행합니다.
my-monorepo/tsconfig.json 파일을 찾습니다.
references 배열에 나열된 모든 프로젝트를 탐색합니다.
프로젝트 간의 의존성 그래프를 생성하여 올바른 빌드 순서를 결정합니다. (예: common -> ui)
각 프로젝트를 증분 방식으로 컴파일합니다.
만약 common 프로젝트의 소스가 변경되지 않았다면, ui 프로젝트만 다시 빌드되거나 아예 빌드되지 않을 수 있습니다.
common/types 같은 지정자를 타입 검사에서 해석하는 한 방법이 paths입니다. 실제 workspace 패키지의 exports/타입 선언을 구성하는 방법도 있으며, 아래 별칭만으로 Node.js 런타임의 패키지가 만들어지지는 않습니다.
my-monorepo/packages/ui/tsconfig.json 수정
{
"compilerOptions": {
"target": "es2020",
"module": "esnext",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"baseUrl": ".", // paths의 기준 디렉토리로 사용; baseUrl 자체가 필수는 아님
"paths": {
"common/*": ["../common/src/*"] // common 모듈을 common/src에서 찾도록 매핑
}
},
"include": [
"src/**/*.ts"
],
"references": [
{ "path": "../common" }
]
}이제 ui 프로젝트의 소스 코드에서 import { MyType } from 'common/types';와 같이 임포트할 수 있습니다. (./src/types/index.ts에 있다면 common/types로 임포트)
모노레포에서의 타입 가져오기
ui 프로젝트에서 common 프로젝트의 타입을 가져오는 방법은 두 가지입니다.
소스 파일 직접 참조:
import { CommonType } from '../../common/src/types';
프로젝트 참조가 설정되면 이 경로로 가져와도 컴파일러는 참조 대상의 선언 출력을 사용합니다. 편집기의 인메모리 선언 지원과 명령행 빌드 조건은 구분해야 합니다.
컴파일된 .d.ts 파일 참조 (권장):
import type { CommonType } from 'common/types'; (타입 전용 사용 예)
이 방식은 common 프로젝트가 먼저 빌드되어 .d.ts 파일이 생성되어야 합니다.
패키지 경계로 타입을 소비하되, 값 import가 남는 경우에는 실제 로더가 common/types를 찾을 수 있도록 패키지 또는 번들러 설정도 맞춰야 합니다.
references와 paths는 출력 import를 다시 쓰지 않습니다. 타입 검사·빌드 순서·실제 모듈 해석을 나누어 확인합니다.
TYPE PLANE · RUNTIME PLANE
타입 빌드와 런타임 해석은 별도 평면이다
project references와 d.ts는 타입 검사 평면을 잇지만, emitted import는 package exports나 bundler가 다시 해석한다.
- references가 producer의 선언
references가 producer의 선언 산출을 요구한다.
- consumer는 d.ts를 사용해
consumer는 d.ts를 사용해 타입을 검사한다.
- emit된 import 문자열은
emit된 import 문자열은 자동으로 바뀌지 않는다.
- Node exports나 bundler
Node exports나 bundler alias를 별도로 맞춘다.
paths와 references가 통과해도 실제 loader가 같은 specifier를 모르면 실행은 실패한다.
주의사항 및 고려사항
composite: true의 영향:composite프로젝트는 선언 출력과 모든 구현 파일의include/files포함 조건을 만족해야 합니다.noImplicitAny를 자동으로 강화하는 옵션은 아닙니다.- 순환 의존성: 프로젝트 레퍼런스는 순환 의존성을 허용하지 않습니다. 만약 A가 B에 의존하고 B가 A에 의존하는 순환 의존성이 발생하면 빌드 오류가 발생합니다. 이는 아키텍처 문제를 진단하는 데 도움이 됩니다.
- 번들러와의 통합:
tsc --build는 각 프로젝트를 개별적으로 컴파일하고.js파일을 생성하지만, 최종적으로 이들을 하나로 묶는 것은 여전히 웹팩, Vite, Rollup과 같은 번들러의 역할입니다. 번들러 설정에서도paths매핑 등을 동일하게 구성해야 합니다. solutiontsconfig.json: 모든 하위 프로젝트를 포함하고 빌드를 조정하는 최상위tsconfig.json을 "Solutiontsconfig.json"이라고 부르기도 합니다.
프로젝트 레퍼런스는 대규모 타입스크립트 프로젝트와 모노레포에서 하위 프로젝트 간 의존성을 명시하고 증분 빌드를 가능하게 하는 기능입니다.
각 하위 프로젝트의 독립성을 유지하면서 타입 안전한 의존 관계를 구성할 때 사용합니다.