본문 바로가기
C.W.K.
Stream
Lesson 05 of 05 · published

모노레포 — pnpm 워크스페이스와 Turborepo

~12 min · tooling, monorepo, pnpm, turborepo

Level 0노드 입문자
0 XP0/40 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"모노레포는 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 워크스페이스로 의존성 경계 지키기

의존성을 위로 끌어올리는 설치 구조에서는 package.json에 선언하지 않은 간접 의존성도 우연히 가져올 수 있어. 이런 유령 의존성 가져오기는 설치 배치나 버전이 달라지면 갑자기 깨져. 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를 실행하면 공유 라이브러리를 먼저 만들고 그 결과에 의존하는 앱을 뒤이어 빌드해. 다음 실행에서는 바뀐 입력과 그 영향을 받는 패키지만 다시 처리하므로, 저장소가 커질수록 전체 재빌드보다 차이가 커져.

모노레포가 필요하지 않은 경우

한 사람이 앱 하나와 작은 라이브러리 하나를 관리한다면 워크스페이스 도구가 주는 복잡성이 이익보다 클 수 있어. 릴리스 주기와 접근 권한이 완전히 다른 제품을 한 저장소에 모으는 것도 맞지 않아.

처음부터 조직 규모를 상상해 모노레포를 만들기보다, 실제로 코드를 함께 바꿀 일이 잦고 버전 차이와 로컬 연결이 반복해서 문제를 일으킬 때 옮겨. 저장소 구조는 유행이 아니라 변경 경계와 배포 경계를 반영해야 해.

Pippa의 고백

cwkPippa에는 프런트엔드와 백엔드가 함께 있지만 워크스페이스 모노레포는 아니야. 아빠는 두 부분이 패키지 단위로 코드를 나누어 쓸 만큼 강하게 얽혀 있지 않다고 판단했어. 시간이 지나도 그 결정은 맞았지. 모노레포는 여러 패키지를 함께 바꿔야 하는 문제를 푸는 도구지, 새 프로젝트의 기본 모양은 아니야.

Code

워크스페이스 범위와 루트 스크립트 설정·yaml
# pnpm-workspace.yaml — at repo root
packages:
  - 'apps/*'
  - 'packages/*'
  - '!packages/legacy'   # exclude something explicitly

# package.json at the root
# {
#   "name": "my-monorepo",
#   "private": true,
#   "packageManager": "pnpm@9.10.0",
#   "scripts": {
#     "build": "turbo run build",
#     "test":  "turbo run test",
#     "lint":  "turbo run lint"
#   },
#   "devDependencies": {
#     "turbo": "^2",
#     "typescript": "^5"
#   }
# }
pnpm과 Turbo를 사용하는 일상 명령·bash
# Working in a pnpm + turbo monorepo

# Install everything from root
pnpm install

# Add a dep to a specific workspace
pnpm --filter @my/web add react

# Run dev for one app
pnpm --filter @my/web dev

# Build the whole repo, dependency-ordered, cached
pnpm turbo run build

# Run tests, only for packages that changed since main
pnpm turbo run test --filter=...[main]

# Clear turbo cache
pnpm turbo prune

# Run a script across every workspace
pnpm -r run lint

External links

Exercise

apps/cli, packages/utils, packages/db 세 패키지로 작은 모노레포를 만들어. dbutils를 사용하고 clidb를 사용하게 한 뒤 pnpm 워크스페이스와 Turbo를 설정해. 처음 빌드한 다음 utils만 고치고 다시 빌드해서 어떤 패키지가 다시 실행되고 어떤 결과가 캐시에서 나오는지 확인해.
Hint
각 패키지에 @my/<name> 형식의 이름을 주고 내부 의존성은 workspace:^로 연결해. 루트에는 pnpm-workspace.yamlturbo.json이 필요해. 두 번째 실행 뒤 Turbo 로그를 보면 의존성 때문에 다시 빌드된 패키지와 그대로 재사용된 작업을 구분할 수 있어.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고
💛 by 똘이warm

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.