프로젝트 레퍼런스
대규모 타입스크립트 프로젝트를 운영하다 보면
하나의 거대한 tsconfig.json과 단일 컴파일 흐름만으로는
효율적인 관리가 어려워지는 시점이 옵니다.
특히 모노레포(Monorepo)에서 여러 하위 프로젝트가 서로 의존할 때 이 문제가 더 뚜렷하게 드러납니다. 모든 코드를 한 번에 컴파일하면 시간이 오래 걸리고, 작은 변경도 전체 빌드를 다시 시작하게 만들어 생산성을 떨어뜨릴 수 있습니다.
타입스크립트 3.0부터 도입된 프로젝트 레퍼런스(Project References) 기능은 이러한 문제를 해결하기 위한 방법입니다. 이 기능은 큰 타입스크립트 프로젝트를 여러 개의 작은 tsconfig.json 파일을 가진 하위 프로젝트로 분할하고, 이들이 서로의 의존성을 명시적으로 참조할 수 있도록 합니다.
왜 프로젝트 레퍼런스를 사용해야 하는가?
프로젝트 레퍼런스를 사용하면 다음과 같은 주요 이점을 얻을 수 있습니다.
- 가장 큰 장점입니다. 변경된 하위 프로젝트와 그에 의존하는 프로젝트만 다시 컴파일하여 전체 빌드 시간을 크게 단축합니다. 대규모 프로젝트에서는 반복 빌드 비용을 줄이는 기준이 됩니다.
- 각 하위 프로젝트가 명확한 경계를 가지므로, 코드 베이스의 모듈화 수준을 높이고 의존성을 명확하게 파악할 수 있습니다. 이는 아키텍처 설계와 유지보수에 도움이 됩니다.
- 논리적으로 분리된 하위 프로젝트 덕분에 코드 베이스를 리팩토링하거나 새로운 기능을 추가할 때 훨씬 유연하게 대처할 수 있습니다.
- IDE는 전체 프로젝트가 아닌 현재 작업 중인 하위 프로젝트와 그 의존성만 분석하여, 자동 완성 및 타입 검사 성능을 향상시킬 수 있습니다.
프로젝트 레퍼런스 설정 방법
프로젝트 레퍼런스를 사용하려면, 의존성을 가지는 각 하위 프로젝트의 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과declarationMap이 자동으로true로 설정됩니다 (명시적으로 써주는 것이 좋습니다).
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배열을 기반으로 올바른 빌드 순서를 자동으로 추론합니다.
프로젝트 레퍼런스 사용하기
위와 같이 설정한 후, 터미널에서 tsc 명령어를 사용합니다.
# 루트 디렉토리에서 실행
tsc --build # 또는 tsc -btsc --build 명령은 다음과 같은 작업을 수행합니다.
my-monorepo/tsconfig.json 파일을 찾습니다.
references 배열에 나열된 모든 프로젝트를 탐색합니다.
프로젝트 간의 의존성 그래프를 생성하여 올바른 빌드 순서를 결정합니다. (예: common -> ui)
각 프로젝트를 증분 방식으로 컴파일합니다. 만약 common 프로젝트의 소스가 변경되지 않았다면, ui 프로젝트만 다시 빌드되거나 아예 빌드되지 않을 수 있습니다.
아래 다이어그램은 변경이 발생했을 때 tsc --build가 참조 그래프를 따라 어떤 프로젝트만 다시 빌드하는지 보여줍니다.
ui 프로젝트에서 common 프로젝트의 모듈을 import { MyType } from 'common/types';와 같이 Node.js 패키지처럼 가져오려면, ui 프로젝트의 tsconfig.json에 paths 매핑을 추가해야 합니다.
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';
이 방식은 common 프로젝트가 컴파일되지 않아도 작동하지만, 실제 런타임 의존성과 일치하지 않을 수 있습니다.
컴파일된 .d.ts 파일 참조 (권장):
import { CommonType } from 'common/types'; (위의 paths 설정 필요)
이 방식은 common 프로젝트가 먼저 빌드되어 .d.ts 파일이 생성되어야 합니다. 이것이 프로젝트 레퍼런스의 주된 목적이며, 런타임 의존성과 일치하므로 더 견고한 접근 방식입니다.
아래 다이어그램은 소스 파일 직접 참조와 컴파일된 .d.ts 파일 참조가 프로젝트 경계에 어떤 차이를 만드는지 비교합니다.
프로젝트 레퍼런스는 설정 파일 몇 개가 맞물려 동작하므로, composite, declaration, references, paths를 한 번에 검증해야 합니다.
주의사항 및 고려사항
composite: true의 영향:composite프로젝트는.d.ts파일 생성이 필수적이며, 일부 컴파일러 옵션(예:noImplicitAny)이 더 엄격하게 적용될 수 있습니다.- 순환 의존성: 프로젝트 레퍼런스는 순환 의존성을 허용하지 않습니다. 만약 A가 B에 의존하고 B가 A에 의존하는 순환 의존성이 발생하면 빌드 오류가 발생합니다. 이는 아키텍처 문제를 진단하는 데 도움이 됩니다.
- 번들러와의 통합:
tsc --build는 각 프로젝트를 개별적으로 컴파일하고.js파일을 생성하지만, 최종적으로 이들을 하나로 묶는 것은 여전히 웹팩, Vite, Rollup과 같은 번들러의 역할입니다. 번들러 설정에서도paths매핑 등을 동일하게 구성해야 합니다. solutiontsconfig.json: 모든 하위 프로젝트를 포함하고 빌드를 조정하는 최상위tsconfig.json을 "Solutiontsconfig.json"이라고 부르기도 합니다.
프로젝트 레퍼런스는 대규모 타입스크립트 프로젝트와 모노레포에서 하위 프로젝트 간 의존성을 명시하고 증분 빌드를 가능하게 하는 기능입니다. 각 하위 프로젝트의 독립성을 유지하면서 타입 안전한 의존 관계를 구성할 때 사용합니다.
다음 다이어그램은 프로젝트 레퍼런스를 설정 요소와 빌드 흐름 기준으로 정리한 표입니다.
아래 다이어그램은 프로젝트 레퍼런스의 도입 이유와 설정 방식이 빌드 순서, 선언 파일, 타입 해석 범위에 주는 영향을 확인합니다.
아래 다이어그램은 프로젝트 레퍼런스에서 설정 위치, 타입 해석, 빌드 실패 신호를 확인합니다.
아래 다이어그램은 프로젝트 레퍼런스을 마무리하며 설정 파일, 타입 해석 범위, 빌드 결과를 한 번 더 확인합니다.