프로젝트 레퍼런스
프로젝트 레퍼런스 (Project References)
프로젝트가 커지면 전체를 하나의 tsc 실행으로 컴파일하기 어려워져요. 프로젝트 레퍼런스(project references)는 TypeScript 3.0 이상에서 쓸 수 있는 기능으로, TypeScript 프로그램을 더 작은 조각으로 나눠 구조화할 수 있게 해줍니다. 이렇게 하면 빌드 시간을 크게 줄이고, 컴포넌트 사이의 논리적 분리를 강제하며, 코드를 더 새롭고 나은 방식으로 정리할 수 있어요. 여기에 tsc의 새 모드인 --build 플래그도 함께 소개하는데, 이는 프로젝트 레퍼런스와 맞물려 더 빠른 TypeScript 빌드를 가능하게 해줍니다.
출처: TypeScript 핸드북
예시 프로젝트
꽤 평범한 프로그램을 하나 보고, 프로젝트 레퍼런스가 어떻게 더 잘 정리하는 데 도움을 주는지 살펴볼게요. converter와 units라는 두 모듈과 각각에 대응하는 테스트 파일이 있는 프로젝트를 상상해 보세요:
/
├── src/
│ ├── converter.ts
│ └── units.ts
├── test/
│ ├── converter-tests.ts
│ └── units-tests.ts
└── tsconfig.json
테스트 파일은 구현 파일을 import해서 몇 가지 테스트를 수행해요:
// converter-tests.ts
import * as converter from "../src/converter";
assert.areEqual(converter.celsiusToFahrenheit(0), 32);
이전에는 단일 tsconfig 파일을 쓰면 이 구조를 다루기가 꽤 어색했어요:
- 구현 파일이 테스트 파일을 import하는 것이 가능했어요
src가 출력 폴더 이름에 나타나지 않고서는test와src를 동시에 빌드하는 게 불가능했는데, 보통 그러길 원하지 않아요- 구현 파일의 내부 만 바꿔도 테스트를 또 타입체크 해야 했어요. 새 오류가 생길 일이 없는데도요
- 테스트만 바꿔도, 아무것도 바뀌지 않았더라도 구현을 다시 타입체크해야 했어요
여러 tsconfig 파일을 써서 일부 문제를 해결할 수 있었지만, 새 문제가 생겼어요:
- 내장된 최신 상태 확인(up-to-date checking)이 없어서 결국
tsc를 항상 두 번 실행하게 돼요 tsc를 두 번 호출하면 시작 오버헤드가 더 커져요tsc -w는 여러 config 파일을 동시에 실행할 수 없어요
프로젝트 레퍼런스는 이 모든 문제와 그 이상을 해결할 수 있어요.
프로젝트 레퍼런스란?
tsconfig.json 파일에 새로운 최상위 속성 references가 생겼어요. 참조할 프로젝트를 지정하는 객체 배열입니다:
{
"compilerOptions": {
// The usual
},
"references": [
{ "path": "../src" }
]
}
각 reference의 path 속성은 tsconfig.json 파일이 들어 있는 디렉터리, 또는 config 파일 자체(이름은 무엇이든 될 수 있어요)를 가리킬 수 있습니다.
프로젝트를 참조하면 새로운 일들이 벌어져요:
- 참조된 프로젝트에서 모듈을 import하면 그 대신 출력 선언 파일(
.d.ts)을 로드해요 - 참조된 프로젝트가
outFile을 만든다면, 출력 파일.d.ts파일의 선언들이 이 프로젝트에서 보이게 돼요 - 빌드 모드(아래 참조)는 필요할 때 참조된 프로젝트를 자동으로 빌드해요
여러 프로젝트로 나누면 타입체크와 컴파일 속도를 크게 높이고, 에디터 사용 시 메모리 사용량을 줄이며, 프로그램의 논리적 그룹을 더 잘 강제할 수 있어요.
composite
참조되는 프로젝트는 반드시 새 설정 composite이 켜져 있어야 해요. 이 설정은 TypeScript가 참조된 프로젝트의 출력물이 어디 있는지 빠르게 알아낼 수 있게 하는 데 필요합니다. composite 플래그를 켜면 몇 가지가 바뀌어요:
rootDir설정이 명시되지 않았다면,tsconfig파일이 있는 디렉터리로 기본값이 돼요- 모든 구현 파일이
include패턴과 일치하거나files배열에 나열되어야 해요. 이 제약을 위반하면tsc가 어떤 파일이 지정되지 않았는지 알려줘요 declaration이 반드시 켜져 있어야 해요
declarationMap
또한 선언 소스 맵(declaration source maps) 지원도 추가했어요. declarationMap을 켜면 지원 에디터에서 "Go to Definition", Rename 같은 기능을 써서 프로젝트 경계를 넘나들며 코드를 자연스럽게 탐색하고 편집할 수 있어요.
프로젝트 레퍼런스의 주의 사항
프로젝트 레퍼런스에는 몇 가지 알아둬야 할 트레이드오프가 있어요.
의존하는 프로젝트가 자신의 의존성으로부터 빌드된 .d.ts 파일을 사용하므로, 특정 빌드 출력을 커밋하거나, 클론 후 프로젝트를 빌드해야 가짜 오류 없이 에디터에서 프로젝트를 탐색할 수 있어요.
VS Code(TS 3.7 이후)를 쓸 때는 이를 완화하는 백그라운드의 메모리 내 .d.ts 생성 과정이 있지만 성능에 영향이 있을 수 있어요. 아주 큰 composite 프로젝트에서는 disableSourceOfProjectReferenceRedirect 옵션으로 이 기능을 끄고 싶을 수 있어요.
추가로, 기존 빌드 워크플로와의 호환성을 지키기 위해 tsc는 --build 스위치로 호출하지 않는 한 의존성을 자동으로 빌드하지 않아요. --build에 대해 더 알아볼게요.
TypeScript용 빌드 모드
오래 기다려온 기능은 TypeScript 프로젝트를 위한 똑똑한 증분 빌드예요. 3.0에서 tsc에 --build 플래그를 쓸 수 있습니다. 사실상 tsc의 새 진입점으로, 단순 컴파일러라기보다 빌드 오케스트레이터처럼 동작해요.
tsc --build(줄여서 tsc -b)를 실행하면 다음을 수행합니다:
- 참조된 모든 프로젝트를 찾음
- 최신 상태인지 감지
- 최신 상태가 아닌 프로젝트를 올바른 순서로 빌드
tsc -b에 여러 config 파일 경로를 넘길 수 있어요 (예: tsc -b src test). tsc -p와 마찬가지로 config 파일 이름이 tsconfig.json이라면 파일 이름 자체를 지정할 필요는 없어요.
tsc -b 명령줄
원하는 만큼 config 파일을 지정할 수 있습니다:
> tsc -b # Use the tsconfig.json in the current directory
> tsc -b src # Use src/tsconfig.json
> tsc -b foo/prd.tsconfig.json bar # Use foo/prd.tsconfig.json and bar/tsconfig.json
명령줄에 넘기는 파일 순서는 걱정하지 마세요. tsc가 필요하면 다시 정렬해서 의존성이 항상 먼저 빌드되도록 해줘요.
tsc -b 전용 플래그도 몇 가지 있습니다:
--verbose: 무슨 일이 벌어지는지 설명하는 자세한 로그 출력 (다른 플래그와 결합 가능)--dry: 무엇을 할지 보여주지만 실제로는 아무것도 빌드하지 않음--clean: 지정된 프로젝트의 출력 삭제 (--dry와 결합 가능)--force: 모든 프로젝트가 최신 상태가 아닌 것처럼 동작--watch: 감시 모드 (--verbose를 제외한 어떤 플래그와도 결합 불가)
주의 사항
보통 tsc는 noEmitOnError가 켜져 있지 않다면 문법·타입 오류가 있어도 출력(.js, .d.ts)을 만들어냅니다. 증분 빌드 시스템에서 이렇게 하면 아주 나빠요. 오래된 의존성 하나에 새 오류가 있으면, 그다음 빌드가 이제 최신 상태가 된 프로젝트를 건너뛰므로 그 오류를 한 번만 보게 될 테니까요. 이런 이유로 tsc -b는 사실상 모든 프로젝트에 noEmitOnError가 켜진 것처럼 동작해요.
빌드 출력(.js, .d.ts, .d.ts.map 등)을 커밋한다면, 소스 제어 도구가 로컬 사본과 원격 사본 사이의 타임스탬프를 보존하는지에 따라 특정 소스 제어 작업 후 --force 빌드를 실행해야 할 수 있어요.
MSBuild
msbuild 프로젝트가 있다면 다음을 추가해 빌드 모드를 켤 수 있어요:
<TypeScriptBuildMode>true</TypeScriptBuildMode>
proj 파일에 넣으면 자동 증분 빌드와 cleaning이 활성화됩니다.
tsconfig.json / -p와 마찬가지로 기존 TypeScript 프로젝트 속성은 존중되지 않으니, 모든 설정은 tsconfig 파일로 관리해야 한다는 점을 알아두세요.
일부 팀은 tsconfig 파일이 짝을 이루는 관리 프로젝트와 같은 암묵적인 그래프 순서를 갖도록 msbuild 기반 워크플로를 구성하기도 해요. 솔루션이 이렇다면 프로젝트 레퍼런스와 함께 msbuild를 tsc -p로 계속 쓸 수 있어요. 이들은 완전히 상호 운용됩니다.
가이드
전체 구조
tsconfig.json 파일이 많아지면 보통 설정 파일 상속으로 공통 컴파일러 옵션을 중앙화하고 싶을 거예요. 그러면 여러 파일을 고치는 대신 한 파일에서 설정을 바꿀 수 있어요.
또 하나 좋은 습관은, 모든 리프 노드 프로젝트에 대한 references만 갖고 files는 빈 배열로 설정하는 "솔루션" tsconfig.json 파일을 두는 것입니다 (그렇지 않으면 솔루션 파일이 파일을 이중으로 컴파일하게 돼요). 3.0부터 tsconfig.json 파일에 reference가 하나 이상 있으면 빈 files 배열이 더는 오류가 아니라는 점을 참고하세요.
이것은 단순한 진입점을 제공해요. 예를 들어 TypeScript 저장소에서는 모든 서브프로젝트를 src/tsconfig.json에 나열하기 때문에 그냥 tsc -b src를 실행해 모든 엔드포인트를 빌드합니다.
TypeScript 저장소에서 이 패턴들을 볼 수 있어요. src/tsconfig-base.json, src/tsconfig.json, src/tsc/tsconfig.json이 핵심 예시입니다.
상대 모듈을 위한 구조
일반적으로 상대 모듈을 쓰는 저장소를 전환하는 데는 많은 것이 필요하지 않아요. 주어진 부모 폴더의 각 하위 디렉터리에 tsconfig.json 파일을 놓고, 프로그램의 의도한 계층과 일치하도록 이 config 파일들에 reference를 추가하면 됩니다. outDir을 출력 폴더의 명시적 하위 폴더로 설정하거나, rootDir을 모든 프로젝트 폴더의 공통 루트로 설정해야 해요.
outFile을 위한 구조
outFile을 쓰는 컴파일의 배치는 상대 경로가 덜 중요하므로 더 유연해요. TypeScript 저장소 자체가 좋은 예시인데, "라이브러리" 프로젝트와 "엔드포인트" 프로젝트가 있어요. "엔드포인트" 프로젝트는 가능한 한 작게 유지하고 필요한 라이브러리만 끌어옵니다.