pnpm으로 모노레포 관리하기

pnpm으로 모노레포 관리하기

여러 패키지를 한 저장소에서 함께 관리하는 모노레포(monorepo) 는 프론트엔드와 백엔드, 공용 라이브러리를 한 흐름으로 다룰 수 있어 팀 생산성을 크게 높여준다. 이 글에서는 pnpm 의 워크스페이스 기능을 활용해 모노레포를 깔끔하게 구성하는 방법을, 실제 프로젝트에 적용하며 정리한 순서대로 소개한다.

모노레포란 무엇인가

모노레포는 여러 프로젝트(패키지)를 하나의 Git 저장소에 두고 관리하는 방식이다. 멀티레포와 달리 코드 공유·일괄 리팩터링·원자적 커밋이 쉽고, 버전 불일치로 인한 "내 로컬에선 되는데" 문제를 줄여준다.

왜 pnpm인가

npm·yarn 도 워크스페이스를 지원하지만, pnpm 은 디스크 효율엄격한 의존성 격리 에서 확실한 강점을 가진다.

디스크 효율

pnpm 은 전역 콘텐츠 주소 저장소(content-addressable store)에 패키지를 한 번만 저장하고, 각 프로젝트의 node_modules 에는 하드링크만 만든다. 그래서 같은 의존성을 수십 개 패키지가 써도 디스크에는 한 벌만 존재한다.

엄격한 의존성 격리

pnpm 의 node_modules 는 평탄화(flatten)되지 않는다. 즉, package.json 에 선언하지 않은 패키지는 import 할 수 없다. 이 "유령 의존성(phantom dependency)" 차단 덕분에 배포 후에야 터지는 누락 의존성 버그를 개발 단계에서 잡을 수 있다.

워크스페이스 설정

모노레포의 뼈대는 워크스페이스 정의 파일과 폴더 구조다.

pnpm-workspace.yaml

저장소 루트에 아래 파일을 두면 pnpm 이 각 폴더를 하나의 패키지로 인식한다.

packages:
  - "apps/*"
  - "packages/*"

패키지 구조

애플리케이션은 apps/, 공용 라이브러리는 packages/ 로 나누는 관례가 널리 쓰인다.

.
├─ apps/
│  ├─ web/        # 프론트엔드 (Vite + React)
│  └─ api/        # 백엔드
├─ packages/
│  ├─ ui/         # 공용 컴포넌트
│  └─ config/     # 공용 ESLint·TS 설정
├─ package.json
└─ pnpm-workspace.yaml

자주 쓰는 명령어

특정 패키지에만 명령을 실행하려면 --filter 를 쓴다.

# web 앱에만 의존성 추가
pnpm --filter web add axios

# 모든 패키지의 test 스크립트 실행
pnpm -r test

# web 이 의존하는 패키지까지 함께 빌드
pnpm --filter web... build

의존성 관리 전략

모노레포가 커질수록 의존성 버전을 어떻게 맞추느냐가 유지보수 난이도를 좌우한다.

공통 의존성

TypeScript·ESLint 같은 개발 도구는 루트 package.json 에 한 번만 두고, -w(workspace root) 플래그로 설치해 버전을 통일한다.

pnpm add -Dw typescript eslint prettier

내부 패키지 참조

내부 패키지는 workspace:* 프로토콜로 참조하면, 배포 시 실제 버전으로 자동 치환된다.

{
  "dependencies": {
    "@myorg/ui": "workspace:*"
  }
}

CI 파이프라인 최적화

모노레포 CI 의 핵심은 바뀐 패키지만 빌드·테스트 하는 것이다. pnpm 의 --filter "...[origin/main]" 구문은 특정 커밋 이후 변경된 패키지와 그 의존 관계만 골라낸다.

pnpm --filter "...[origin/main]" test

여기에 pnpm store 캐시를 CI 에 얹으면 설치 시간이 극적으로 줄어든다.

자주 겪는 문제와 해결

마치며

pnpm 워크스페이스는 "빠르고, 디스크를 아끼며, 실수를 미리 막아주는" 모노레포 도구다. 작게 시작해 apps/packages/ 두 폴더만 나눠도 효과를 바로 체감할 수 있다. 이 글의 목차를 따라 위에서부터 하나씩 적용해 보자.