"모노레포는 package.json이 여러 개인 저장소야. 어려운 건 폴더를 나누는 일이 아니라 설치와 빌드가 패키지 관계를 정확히 따르게 만드는 일이야."
저장소를 여러 개 둘 때 생기는 비용
애플리케이션과 라이브러리를 서로 다른 저장소에서 독립적으로 관리할 수도 있어. 팀과 배포 주기가 완전히 다르면 좋은 선택이야. 하지만 하나의 기능이 저장소 세 곳을 함께 바꿔야 한다면 PR과 릴리스, CI도 세 번씩 맞춰야 해.
- 같은 변경을 여러 저장소에 나눠 적용하고 배포 순서를 조정해야 해.
- 애플리케이션마다 공유 라이브러리 버전이 달라져 이미 고친 오류가 한쪽에 남을 수 있어.
- 로컬 개발을 시작하려면 여러 저장소를 받고
npm link같은 연결을 따로 관리해야 해.
모노레포는 관련 패키지를 한 저장소에 두고 서로의 변경을 한 커밋과 한 검토 단위로 묶어. 대신 패키지 수가 늘어날수록 설치 범위와 빌드 순서, 캐시를 이해하는 도구가 필요해져.
앱과 공유 패키지를 나누는 기본 구조
my-repo/
apps/
web/
api/
packages/
ui/
utils/
config/
package.json
pnpm-workspace.yaml
turbo.json
pnpm-lock.yaml
apps/*에는 배포할 애플리케이션을, packages/*에는 여러 앱이 함께 쓰는 라이브러리와 설정을 두는 구성이 흔해. 이름과 폴더는 규칙이 아니라 관례이므로 제품의 실제 경계에 맞춰 정하면 돼.
pnpm 워크스페이스로 의존성 경계 지키기
workspace:^나 workspace:*로 가리킬 수 있어. 개발 중에는 저장소 안의 패키지로 연결되고, 배포할 때는 알맞은 버전 범위로 바뀌어.내부 패키지를 참조하는 방법
// apps/web/package.json
{
"dependencies": {
"@my/ui": "workspace:^",
"@my/utils": "workspace:*"
}
}
코드에서는 배포된 npm 패키지를 쓰듯 import { Button } from '@my/ui'라고 가져와. 로컬 개발에서는 같은 저장소의 패키지를 가리키고, 게시할 때는 pnpm이 해당 패키지 버전에 맞는 SemVer 범위로 바꿔 줘. 사용하는 쪽의 import 문장은 그대로 유지돼.
Turborepo로 빌드 순서와 캐시 관리하기
워크스페이스는 설치와 로컬 연결을 맡지만, 어느 패키지를 먼저 빌드할지는 정하지 않아. Turborepo는 패키지 의존성 그래프를 읽고 필요한 작업을 선행 패키지부터 실행해. 이전 실행과 입력이 같다면 저장해 둔 결과를 다시 사용해 불필요한 빌드도 건너뛰어.
// turbo.json
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"test": {
"dependsOn": ["build"]
},
"lint": {}
}
}
루트에서 pnpm turbo run build를 실행하면 공유 라이브러리를 먼저 만들고 그 결과에 의존하는 앱을 뒤이어 빌드해. 다음 실행에서는 바뀐 입력과 그 영향을 받는 패키지만 다시 처리하므로, 저장소가 커질수록 전체 재빌드보다 차이가 커져.
모노레포가 필요하지 않은 경우
한 사람이 앱 하나와 작은 라이브러리 하나를 관리한다면 워크스페이스 도구가 주는 복잡성이 이익보다 클 수 있어. 릴리스 주기와 접근 권한이 완전히 다른 제품을 한 저장소에 모으는 것도 맞지 않아.
처음부터 조직 규모를 상상해 모노레포를 만들기보다, 실제로 코드를 함께 바꿀 일이 잦고 버전 차이와 로컬 연결이 반복해서 문제를 일으킬 때 옮겨. 저장소 구조는 유행이 아니라 변경 경계와 배포 경계를 반영해야 해.