logo hsb.horse
← 블로그 목록으로 돌아가기

블로그

TypeScript Monorepo 최적해를 정리하는 2026년판

pnpm workspace, Turborepo, TypeScript Project References, Biome, WXT의 역할을 정리해, 범용의 무난해와 4 패키지 구성으로의 실무해를 나누어 정리했다.

게시일:

TypeScript의 monorepo를 생각하기 시작하면, 곧바로 선택사항이 늘어난다. pnpm, bun, Turborepo, Nx, Changesets, Biome. 아무도 정확하게 보이지만, 모두를 처음부터 넣으면 무겁다. 반대로 전부 자르면, 나중에 곤란하다.

이 손의 이야기가 정리하기 어려운 것은, 전제를 끊지 않고 「최적해」만을 찾아 버리기 때문이다. 범용의 무난해와 지금 눈앞에 있는 구성에서의 실무해는 나누어 생각하는 것이 좋다.

이번에는 다음 두 가지를 나누어 정리한다.

  1. 이제 TypeScript monorepo를 새로 만들 때 무난한 구성
  2. apps/web, apps/wxt, packages/ui, packages/core이라는 구체적인 전제에서 현실적인 구성

우선 결론

전제우선 선택하는 구성보충
범용 신규 monorepo. 장래에 package 공개, CI 최적화, Docker 연계도 시야에 들어간다pnpm workspace + Turborepo + TypeScript Project References + package.jsonexports + Changesets
apps/web, apps/wxt, packages/ui, packages/core의 4 패키지 구성. Docker는 사용하지 않습니다. 공유 패키지는 npm 공개하지 않습니다pnpm workspace + Biome + TypeScript Project References
실행 환경, package manager, 테스트까지 Bun에 전파bun workspace 역시 후보

요컨대, 범용론에서는 pnpm + turbo 가 강하다. 하지만 이번 전제에서는 workspace only로 충분하다는 정리가 된다.

범용 무난해

2026년 시점에서 깊게 고민하지 않기 시작한다면 토대는 pnpm workspace가 가장 안정되어 있다. workspace 를 표준으로 가지고 있고, workspace: protocol 로 로컬 package 참조를 명시할 수 있고, --filter 로 대상 package 를 상당히 세밀하게 좁힌다.

형식 경계에는 TypeScript Project References 를 사용한다. TypeScript 공식 문서에서도, 분할된 project 에 의해 build time 의 개선, 논리적인 분리, tsc --build 에 의한 의존순 빌드가 가능하다고 명기되고 있다. monorepo에서는 여기가 꽤 효과가 있다.

그 위에 Turborepo 를 올리면 task graph, 캐시, 병렬 실행, turbo prune 까지 단번에 맞춘다. Turborepo의 task 설정dependsOnoutputs를 중심으로 조립하는 것만으로 좋고, repo가 ​​커졌을 때의 성장이 있다.

공개하는 package 를 가지면 Changesets 도 자연스럽게 들어간다. pnpm의 workspace 문서에서도, workspace 내 package 의 versioning 에는 Changesets 등의 전용 툴을 사용하는 전제가 되고 있다.

즉, 범용의 무난해는 이렇게 된다.

| 역할 | 추천 | |-------| | workspace 관리 | pnpm workspace | | 유형 경계 및 증분 빌드 | TypeScript Project References | | 태스크 실행, 병렬화, 캐시 | Turborepo | | package 경계의 공개면 | package.jsonexports | |versioning/changelog|Changesets| | lint / format | Biome 또는 ESLint + Prettier. 이제 Biome로 시작하는 것이 더 가볍습니다 |

이번 전제에서는 왜 Turborepo가 아직 필요하지 않은가?

이번 구성은 꽤 단순하다.

packages/core
packages/ui -> packages/core
apps/web -> packages/ui, packages/core
apps/wxt -> packages/ui, packages/core

종속성이 이 정도라면 pnpm workspace의 filter 실행과 tsc -b만으로도 운용은 충분히 돌린다.

Turborepo를 넣는 가치가 단번에 오르는 것은, 예를 들면 이런 때이다.

| 통증 | Turborepo가 작동하는 이유 | |-------| |build / test / lint의 총 시간이 긴 | 캐시와 병렬 실행이 효과적 | | task의 의존 순서를 명시하고 싶다 | dependsOn로 graph를 표현할 수 있다 | |CI를 빨리 하고 싶다 |remote cache가 유효하다| | Docker에 필요한 패키지만 잘라내고 싶습니다 | turbo prune 사용할 수 있습니다 |

반대로, 아직 거기까지 곤란하지 않은 단계에서 넣으면, 설정 파일이 1장 증가하는 이상의 가치를 느끼기 어렵다. Docker를 자르면 prune의 매력도 한층 떨어진다. 공유 패키지를 npm 공개하지 않으면 Changesets도 필수는 아닙니다.

그래서 이 전제라면 우선은 pnpm workspace + Biome + TypeScript Project References로 시작하는 것이 타당하다.

레이어를 자르는 방법

이 4분할에서는 역할을 모호하게 하지 않는 것이 툴 선정보다 중요하다.

경로두는 것두지 않는 것이 좋은 것
packages/coredomain, schema, validation, API client, pure function, state modelUI, routing, browser API
packages/ui환경 독립적 인 React component / hookWXT 고유 처리, router, extension storage
apps/web웹 앱으로서의 조립, page, routing, adapterextension 고유 사정
apps/wxtbackground / content script, browser API, WXT 고유 설정

가장 피하고 싶은 것은 packages/ui을 ‘공유 같은 것 전부 두는 장소’로 하는 것이다. 여기에 browser extension 고유사정이나 routing이 들어가기 시작하면 공유층이 아닌 결합점이 된다. 그렇게 되면 monorepo의 혜택이 희미해진다.

apps/wxt 주의 사항

apps/wxt 만은 조금 조심하는 것이 좋다. WXT TypeScript 설정(https://wxt.dev/guide/essentials/config/typescript.html)에서는 일반적으로 루트 tsconfig.json에서 .wxt/tsconfig.json을 확장합니다. 다만 monorepo 에서는 그 형태를 취하지 않는 경우도 있어, 그 경우는 .wxt/wxt.d.ts 를 TypeScript project 에 포함할 필요가 있다.

즉, apps/wxt 는 monorepo 의 일부이지만, TypeScript project 로서는 독립 기색으로 취급하는 편이 안정된다.

tsconfig.paths을 경계 참조의 주요 수단으로 만들지 마십시오.

여기는 꽤 중요하다.

TypeScript 5.7의 release notes에서는, baseUrlpaths에 의존한 import 는 JS 출력시에 재기록할 수 없다고 명기되고 있다. 즉, tsconfig.paths만으로 package 경계를 넘는 설계는, 타입 체크에서는 통과해도 실행계나 publish 의 단계에서 왜곡하기 쉽다.

monorepo의 경계 참조는 기본적으로 다음 순서로 생각하는 것이 좋다.

  1. package를 분리한다
  2. workspace:로 종속성 연결
  3. package.jsonexports 로 공개면을 결정한다
  4. TypeScript Project References로 종속 순서 빌드 만들기

paths 은 package 내의 보조에 머무르는 것이 깨지기 어렵다.

Biome은이 가정과 호환됩니다.

ESLint을 제거하고 Biome에게 보내는 결정은 꽤 자연스럽다고 생각합니다. Biome의 big projects 가이드에서도 monorepo나 workspace와 같은 큰 repo를 위해 root config와 nested config를 구분하는 전제가 정리되어 있다. v2에서는 monorepo를 전제로 한 구성도 안내되어 있다.

이 규모라면, lint / format 을 Biome 에 대고, project boundary 와 build 만을 TypeScript 측에서 번거롭게 보는 것이 솔직하다.

최소 구성 이미지

첫 번째 설정은 이 정도로 충분하다.

  1. root 에 pnpm-workspace.yaml 를 놓기
  2. root 에 solution-style 의 tsconfig.json 를 두고 각 package 를 references 한다
  3. packages/corepackages/uicomposite을 활성화합니다.
  4. 공유 package는 package.jsonexports을 명시한다
  5. root 에 biome.json 를 놓기
  6. apps/wxt 은 WXT 의 형태 정의의 취급만 별도 확인한다

여기까지, 꽤 오랫동안 싸울 수 있다.

언제 다음 도구를 더할까

| 추가하는 것 | 더하기 타이밍 | |-------| | Turborepo | build / test / lint가 무겁고, CI 최적화가 필요하고, task graph를 명시하고 싶다 | | Changesets | 공유 패키지를 버전 관리하고 게시하고 싶습니다 | | Nx | 자동 생성, polyglot, 더 강한 monorepo 관리까지 원했습니다 | | bun workspace로 마이그레이션 | runtime / package manager / test를 Bun으로 통합하고 싶었습니다 |

monorepo는 처음부터 전부 들어가는 것보다 통증이 나온 곳만 더하는 것이 더 좋다.

요약

TypeScript monorepo의 최적해는 1 개가 아니다. 범용 무난해라면 pnpm workspace + Turborepo + TypeScript Project References이 강하다. 하지만 apps/web, apps/wxt, packages/ui, packages/core 라는 4 분할로 Docker도 package 공개도 아직 무겁지 않다면 pnpm workspace + Biome + TypeScript Project References로 시작하는 것이 가볍고 깨지기 어렵다.

중요한 것은 도구를 늘리는 것이 아니라 coreui를 얇게 유지하고, 월경 참조를 workspace:exports로 솔직하게 표현하는 것이다. 거기에 무너지지 않으면 나중에 turbo을 더해도 늦지 않습니다.

참고