본문으로 건너뛰기

안동민 개발노트

본문 시작

프로젝트 레퍼런스

대규모 코드를 여러 composite 프로젝트로 나누고 references와 빌드 순서를 설정해 모노레포 의존 경계를 명확히 합니다.

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

특히 모노레포(Monorepo)에서 여러 하위 프로젝트가 서로 의존할 때 이 문제가 더 뚜렷하게 드러납니다.

모든 코드를 한 번에 컴파일하면 시간이 오래 걸리고, 작은 변경도 전체 빌드를 다시 시작하게 만들어 생산성을 떨어뜨릴 수 있습니다.

타입스크립트 3.0부터 도입된 프로젝트 레퍼런스(Project References) 기능은 이러한 문제를 해결하기 위한 방법입니다.

이 기능은 큰 타입스크립트 프로젝트를 여러 개의 작은 tsconfig.json 파일을 가진 하위 프로젝트로 분할하고, 이들이 서로의 의존성을 명시적으로 참조할 수 있도록 합니다.

프로젝트 레퍼런스

대규모 타입스크립트 프로젝트를 운영하다 보면 하나의 거대한 tsconfig.json과 단일 컴파일 흐름만으로는 효율적인 관리가 어려워지는 시점이 옵니다. 특히 모노레포(Monorepo)에서 여러 하위 프로젝트가 서로 의존할 때 이 문제가 더 뚜렷하게 드러납니다.

  1. 1
    를 사용하면 다음

    같은 주요 이점을 얻을 수 있습니다. 프로젝트 레퍼런스(Project References)

  2. 2
    를 사용하려면, 의존성

    가지는 각 하위 프로젝트의 tsconfig.json 파일에 몇 가지 변경 사항을 적용해야 합니다. 증분 빌드 (Incremental Builds)

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

    위와 같이 설정한 후, 터미널에서 tsc 명령어를 사용합니다. 전체 빌드 시간을 크게 단축

  4. 4
    프로젝트 레퍼런스 기준

    정확성 타입스크립트 3.0부터 도입된 프로젝트 레퍼런스(Project References)는 여러 tsconfig 프로젝트의 의존 관계를 선언합니다. 비용 증분 빌드(Incremental Builds)는 이전 빌드 정보를 재사용해 변경된 프로젝트와 의존 프로젝트만 다시 검사합니다. 확장성 변경된 하위 프로젝트와 그에 의존하는 프로젝트만 다시 컴파일하여 전체 빌드 시간을 크게 단축합니다. 예외 모듈 경계의 명확화는 각 하위 프로젝트가 공개 API와 의존 방향을 분리하게 해 코드베이스의 모듈화 수준을 높입니다.

  5. 5
    정확성 타입스크립트 3.0부터 도입된 프로젝트

    레퍼런스(Project References)는 여러 tsconfig 프로젝트의 의존 관계를 선언합니다.

  6. 6
    비용 증분 빌드(Incremental Builds)

    이전 빌드 정보를 재사용해 변경된 프로젝트와 의존 프로젝트만 다시 검사합니다.

  7. 7
    확장성 변경된 하위 프로젝트

    그에 의존하는 프로젝트만 다시 컴파일하여 전체 빌드 시간을 크게 단축합니다.

  8. 8
    예외 모듈 경계의 명확화

    각 하위 프로젝트가 공개 API와 의존 방향을 분리하게 해 코드베이스의 모듈화 수준을 높입니다.


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

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

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

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

Project References는 패키지 빌드 순서를 그래프로 만든다

큰 저장소를 하나로 빌드하지 않고 shared, server, client 같은 하위 프로젝트 간 의존 순서를 명시한다.

  1. shared
    shared 공통 타입

    유틸리티

  2. server
    server shared 출력에 의존
  3. client
    client shared 출력에 의존
  4. app
    app server/client 산출물

    조합

빌드 기준의미
composite참조 가능한 프로젝트 산출물 생성
declaration하위 프로젝트 타입 출력 필요
판단변경된 노드와 의존 노드만 다시 빌드한다

프로젝트 레퍼런스를 사용하려면, 의존성을 가지는 각 하위 프로젝트의 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 프로젝트만 다시 빌드되거나 아예 빌드되지 않을 수 있습니다.

아래 다이어그램은 변경이 발생했을 때 tsc --build가 참조 그래프를 따라 어떤 프로젝트만 다시 빌드하는지 보여줍니다.

변경된 프로젝트와 의존 프로젝트만 다시 빌드합니다

tsc --build 는 references 그래프와 .tsbuildinfo 를 보고 필요한 산출물만 갱신합니다.

  1. common

    changed 공유 타입이 바뀌면 .d.ts 산출물을 다시 만듭니다.

  2. ui

    depends common 타입을 가져오므로 새 선언을 기준으로 검사합니다.

  3. admin

    skip 영향이 없고 이전 정보가 유효하면 불필요한 재빌드를 건너뜁니다.

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 파일이 생성되어야 합니다.

이것이 프로젝트 레퍼런스의 주된 목적이며, 런타임 의존성과 일치하므로 더 견고한 접근 방식입니다.

아래 다이어그램은 소스 파일 직접 참조와 컴파일된 .d.ts 파일 참조가 프로젝트 경계에 어떤 차이를 만드는지 비교합니다.

소비자는 가능하면 생산자의 빌드 산출 타입을 바라봅니다

composite 패키지는 .d.ts 를 만들고, 의존 패키지는 그 선언 파일을 기준으로 경계를 안정적으로 유지합니다.

  1. packages/common/src

    producer export interface User { ... } 공통 타입과 유틸리티를 독립 프로젝트로 관리합니다.

  2. dist/*.d.ts 생성

    tsc -b declaration + composite 타입 표면만 소비자에게 공개하고 증분 빌드 캐시를 남깁니다.

  3. packages/ui

    consumer references: [{ "path": "../common" }] 참조 그래프를 통해 빌드 순서와 타입 의존성을 명확히 합니다.

  4. 소스 직접 참조

    import ../../common/src/types 장점 별도 빌드 전에도 IDE에서 빠르게 따라갈 수 있습니다. 위험 패키지 경계와 런타임 배포 구조를 우회할 수 있습니다.

  5. 컴파일된 선언 참조

    import common/types 장점 실제 패키지 공개 API와 동일한 타입 표면을 사용합니다. 빌드 전제 생산자 빌드와 paths, package exports 설정이 맞아야 합니다.

프로젝트 레퍼런스는 설정 파일 몇 개가 맞물려 동작하므로, composite, declaration, references, paths를 한 번에 검증해야 합니다.

프로젝트 레퍼런스의 두 그래프

같은 import라도 TypeScript가 빌드 순서를 계산하는 그래프와 실행기가 파일을 찾는 그래프는 서로 다른 설정을 따릅니다.

  1. 타입·빌드
    references → tsc -b .d.ts

    composite와 references가 core → ui → app 순서를 만들고 선언 출력을 소비자에게 연결합니다.

  2. 실행·로드
    import → resolver .js

    Node, 번들러, package exports가 emit된 import 문자열을 실제 JavaScript 파일로 연결합니다.


주의사항 및 고려사항

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

프로젝트 레퍼런스는 대규모 타입스크립트 프로젝트와 모노레포에서 하위 프로젝트 간 의존성을 명시하고 증분 빌드를 가능하게 하는 기능입니다.

각 하위 프로젝트의 독립성을 유지하면서 타입 안전한 의존 관계를 구성할 때 사용합니다.


다음 다이어그램은 프로젝트 레퍼런스를 설정 요소와 빌드 흐름 기준으로 정리한 표입니다.

프로젝트 레퍼런스는 큰 코드베이스를 타입 단위로 나눕니다

composite, references, build 모드를 이해하면 모노레포나 대형 프로젝트에서 타입 검사와 빌드 순서를 안정적으로 나눌 수 있습니다.

  1. 1
    참조 가능 프로젝트

    composite 다른 프로젝트가 참조할 수 있도록 출력 정보와 제약을 활성화합니다. composite: true

  2. 2
    의존 순서

    references 현재 프로젝트가 먼저 빌드해야 하는 하위 프로젝트를 명시합니다. references

  3. 3
    순서 빌드

    build 모드 tsc -b는 참조 그래프를 따라 필요한 프로젝트만 순서대로 빌드합니다. tsc -b

  4. 4
    경계 관리

    공개 타입 패키지 간에는 내부 소스보다 생성된 타입 표면을 통해 연결하는 편이 안전합니다. .d.ts

아래 다이어그램은 프로젝트 레퍼런스의 도입 이유와 설정 방식이 빌드 순서, 선언 파일, 타입 해석 범위에 주는 영향을 확인합니다.

프로젝트 레퍼런스 빌드 그래프

tsconfig references는 모노레포 패키지의 타입 경계와 빌드 순서를 그래프로 고정해 변경 범위만 다시 컴파일하게 만든다.

  1. 1
    패키지 분리

    domain, ui, app처럼 의존 방향이 보이는 단위로 tsconfig를 나눈다. · project unit

  2. 2
    references 선언

    상위 프로젝트가 필요한 하위 프로젝트 경로를 references에 적는다. · dependency edge

  3. 3
    선언 파일 생성

    composite 프로젝트는 d.ts와 tsbuildinfo를 남겨 다음 빌드 입력이 된다. · 산출물

  4. 4
    증분 빌드

    tsc -b가 그래프를 따라 변경된 노드와 영향을 받은 노드만 다시 처리한다. · affected graph

  5. 5
    composite

    참조 대상 프로젝트에 composite과 declaration 설정이 있다.