icon

안동민 개발노트

9장 : 타입스크립트 컴파일러

프로젝트 레퍼런스


대규모 타입스크립트 프로젝트를 운영하다 보면 하나의 거대한 tsconfig.json과 단일 컴파일 흐름만으로는 효율적인 관리가 어려워지는 시점이 옵니다.

특히 모노레포(Monorepo)에서 여러 하위 프로젝트가 서로 의존할 때 이 문제가 더 뚜렷하게 드러납니다. 모든 코드를 한 번에 컴파일하면 시간이 오래 걸리고, 작은 변경도 전체 빌드를 다시 시작하게 만들어 생산성을 떨어뜨릴 수 있습니다.

타입스크립트 3.0부터 도입된 프로젝트 레퍼런스(Project References) 기능은 이러한 문제를 해결하기 위한 강력한 솔루션입니다. 이 기능은 큰 타입스크립트 프로젝트를 여러 개의 작은 tsconfig.json 파일을 가진 하위 프로젝트로 분할하고, 이들이 서로의 의존성을 명시적으로 참조할 수 있도록 합니다.


왜 프로젝트 레퍼런스를 사용해야 하는가?

프로젝트 레퍼런스를 사용하면 다음과 같은 주요 이점을 얻을 수 있습니다.

증분 빌드 (Incremental Builds)
  • 가장 큰 장점입니다. 변경된 하위 프로젝트와 그에 의존하는 프로젝트만 다시 컴파일하여 전체 빌드 시간을 크게 단축합니다. 이는 대규모 프로젝트에서 개발자 경험을 혁신적으로 개선합니다.
모듈 경계의 명확화
  • 각 하위 프로젝트가 명확한 경계를 가지므로, 코드 베이스의 모듈화 수준을 높이고 의존성을 명확하게 파악할 수 있습니다. 이는 아키텍처 설계와 유지보수에 도움이 됩니다.
코드 이동 및 재구성 용이
  • 논리적으로 분리된 하위 프로젝트 덕분에 코드 베이스를 리팩토링하거나 새로운 기능을 추가할 때 훨씬 유연하게 대처할 수 있습니다.
IDE 성능 향상
  • IDE는 전체 프로젝트가 아닌 현재 작업 중인 하위 프로젝트와 그 의존성만 분석하여, 자동 완성 및 타입 검사 성능을 향상시킬 수 있습니다.

프로젝트 레퍼런스 설정 방법

프로젝트 레퍼런스를 사용하려면, 의존성을 가지는 각 하위 프로젝트의 tsconfig.json 파일에 몇 가지 변경 사항을 적용해야 합니다.

기본 구조 예시 (모노레포)
tsconfig.json
types.ts
tsconfig.json)
package.json
tsconfig.json
1단계: common 프로젝트 (생산자) 설정

common 프로젝트는 다른 프로젝트가 의존할 타입 정의 파일(.d.ts)을 생성해야 합니다.

my-monorepo/packages/common/tsconfig.json
{
  "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에 의해 참조될 수 있는 프로젝트임을 나타내는 플래그입니다. 이 옵션을 켜면 declarationdeclarationMap이 자동으로 true로 설정됩니다 (명시적으로 써주는 것이 좋습니다).
2단계: ui 프로젝트 (소비자) 설정

ui 프로젝트는 common 프로젝트가 제공하는 타입에 의존합니다. references 배열을 사용하여 이 의존성을 명시합니다.

my-monorepo/packages/ui/tsconfig.json
{
  "compilerOptions": {
    "target": "es2020",
    "module": "esnext",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true
  },
  "include": [
    "src/**/*.ts"
  ],
  "references": [
    { "path": "../common" } // common 프로젝트를 참조합니다.
  ]
}
  • "references": []: 이 배열 안에 현재 프로젝트가 의존하는 다른 tsconfig.json 파일의 경로를 지정합니다. 경로는 현재 tsconfig.json 파일을 기준으로 합니다.
3단계: 루트 tsconfig.json (빌드 관리)

모노레포의 루트에 있는 tsconfig.json은 일반적으로 직접 코드를 컴파일하기보다는, 하위 프로젝트들의 빌드 순서를 관리하고 증분 빌드를 트리거하는 역할을 합니다.

my-monorepo/tsconfig.json
{
  "files": [], // 이 tsconfig 자체는 컴파일할 파일이 없으므로 비워둡니다.
  "references": [
    { "path": "./packages/common" }, // common 프로젝트를 먼저 빌드해야 합니다.
    { "path": "./packages/ui" }      // ui 프로젝트는 common에 의존하므로 그 다음에 빌드됩니다.
  ]
}
  • "files": []: 이 루트 tsconfig.json은 직접 소스 파일을 컴파일하지 않고, 단지 하위 프로젝트들을 참조만 하므로 filesinclude를 비워두는 것이 일반적입니다.
  • "references": []: 여기에 모든 하위 프로젝트를 나열합니다. tsc --build 명령을 사용하면 TypeScript는 이 references 배열을 기반으로 올바른 빌드 순서를 자동으로 추론합니다.

프로젝트 레퍼런스 사용하기

위와 같이 설정한 후, 터미널에서 tsc 명령어를 사용합니다.

# 루트 디렉토리에서 실행
tsc --build # 또는 tsc -b

tsc --build 명령은 다음과 같은 작업을 수행합니다.

my-monorepo/tsconfig.json 파일을 찾습니다.

references 배열에 나열된 모든 프로젝트를 탐색합니다.

프로젝트 간의 의존성 그래프를 생성하여 올바른 빌드 순서를 결정합니다. (예: common -> ui)

각 프로젝트를 증분 방식으로 컴파일합니다. 만약 common 프로젝트의 소스가 변경되지 않았다면, ui 프로젝트만 다시 빌드되거나 아예 빌드되지 않을 수 있습니다.

Node.js 패키지 경로 매핑 (선택 사항)

ui 프로젝트에서 common 프로젝트의 모듈을 import { MyType } from 'common/types';와 같이 Node.js 패키지처럼 가져오려면, ui 프로젝트의 tsconfig.jsonpaths 매핑을 추가해야 합니다.

my-monorepo/packages/ui/tsconfig.json 수정

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 파일이 생성되어야 합니다. 이것이 프로젝트 레퍼런스의 주된 목적이며, 런타임 의존성과 일치하므로 더 견고한 접근 방식입니다.


주의사항 및 고려사항

  • composite: true의 영향: composite 프로젝트는 .d.ts 파일 생성이 필수적이며, 일부 컴파일러 옵션(예: noImplicitAny)이 더 엄격하게 적용될 수 있습니다.
  • 순환 의존성: 프로젝트 레퍼런스는 순환 의존성을 허용하지 않습니다. 만약 A가 B에 의존하고 B가 A에 의존하는 순환 의존성이 발생하면 빌드 오류가 발생합니다. 이는 아키텍처 문제를 진단하는 데 도움이 됩니다.
  • 번들러와의 통합: tsc --build는 각 프로젝트를 개별적으로 컴파일하고 .js 파일을 생성하지만, 최종적으로 이들을 하나로 묶는 것은 여전히 웹팩, Vite, Rollup과 같은 번들러의 역할입니다. 번들러 설정에서도 paths 매핑 등을 동일하게 구성해야 합니다.
  • solution tsconfig.json: 모든 하위 프로젝트를 포함하고 빌드를 조정하는 최상위 tsconfig.json을 "Solution tsconfig.json"이라고 부르기도 합니다.

프로젝트 레퍼런스는 대규모 타입스크립트 프로젝트, 특히 모노레포 환경에서 개발 효율성과 빌드 성능을 획기적으로 향상시킬 수 있는 강력한 기능입니다. 각 하위 프로젝트의 독립성을 유지하면서도, 타입 안전한 방식으로 서로 의존할 수 있도록 하여 복잡한 코드 베이스를 더 효과적으로 관리할 수 있게 합니다.

목차