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

워크스페이스와 모노레포

~13 min · modules, workspaces, monorepo, pnpm

Level 0노드 입문자
0 XP0/40 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"워크스페이스는 한 저장소 안의 여러 패키지를 서로 모르는 폴더가 아니라 하나의 의존성 그래프로 다루게 해."

패키지가 여러 개가 되는 순간

처음에는 애플리케이션 하나로 충분해도, 시간이 지나면 웹 앱과 CLI, 공유 라이브러리를 따로 배포하거나 시험하고 싶어질 수 있어. 저장소를 각각 나누면 권한과 릴리스 주기를 독립적으로 관리하기 좋지만, 세 저장소를 함께 바꾸는 기능은 PR과 버전 갱신, 배포 순서를 모두 맞춰야 해.

관련 패키지를 한 저장소에 두는 모노레포는 이 변경을 한 커밋과 한 검토 단위로 묶어. 워크스페이스는 그 안의 패키지 목록과 서로의 의존 관계를 패키지 관리자에게 알려 주는 기능이야. 모노레포가 곧 워크스페이스는 아니지만, Node 생태계에서는 둘을 함께 쓰는 경우가 많아.

npm 워크스페이스 선언하기

루트 package.jsonworkspaces에 패키지 경로를 적으면 npm이 저장소 전체를 한 설치 단위로 다뤄:

{
  "name": "my-repo",
  "private": true,
  "workspaces": ["packages/*", "apps/*"]
}

루트에서 npm install을 실행하면 npm이 모든 워크스페이스의 의존성을 계산하고, 저장소 안의 패키지를 서로 가져올 수 있게 연결해. 특정 패키지의 스크립트만 실행할 때는 npm run build --workspace=@my/web처럼 대상을 고를 수 있어.

pnpm 워크스페이스와 의존성 경계

pnpm은 루트의 pnpm-workspace.yaml에서 패키지 범위를 읽어:

packages:
  - 'packages/*'
  - 'apps/*'

pnpm의 기본 격리 구조에서는 각 패키지가 자신이 선언한 의존성을 중심으로 보게 돼. 다른 패키지 때문에 우연히 설치된 간접 의존성을 가져오는 실수를 일찍 발견하기 쉬운 거지. pnpm --filter @my/web build는 한 워크스페이스만, pnpm --filter "@my/*" build는 이름이 맞는 여러 워크스페이스만 골라 실행해.

저장소 안의 패키지를 workspace:로 가리키기

내부 패키지는 일반 npm 버전 대신 워크스페이스 프로토콜로 연결할 수 있어:
{
  "dependencies": {
    "@my/utils": "workspace:^",
    "@my/ui": "workspace:*"
  }
}
  • workspace:^는 게시할 때 현재 내부 버전에 맞는 캐럿 범위로 바뀌어.
  • workspace:~는 틸드 범위로 바뀌어.
  • workspace:*는 현재 내부 버전을 그대로 사용해.
개발 중에는 저장소 안의 패키지로 연결되고, 게시할 때는 실제 SemVer 표현으로 바뀌어. 사용하는 코드의 import 문장은 환경에 따라 달라질 필요가 없어.

설치 순서와 빌드 순서는 다른 문제야

워크스페이스는 패키지를 설치하고 연결하지만, 각 도구의 빌드 결과가 무엇에 의존하는지까지 모두 알지는 못해. 예를 들어 웹 앱이 공유 라이브러리의 dist 출력을 가져온다면 라이브러리를 먼저 빌드해야 해.

pnpm -r run build는 기본적으로 워크스페이스 의존 관계를 고려해 스크립트를 실행할 수 있어. 입력과 출력에 따른 캐시, 바뀐 패키지만 골라 다시 실행하는 기능이 더 필요하면 Turborepo나 Nx 같은 작업 실행기를 얹어. 패키지가 몇 개 안 되고 전체 빌드가 빠르다면 먼저 패키지 관리자의 재귀 실행만으로 시작해도 충분해.

모노레포를 택하지 않아도 되는 경우

코드를 함께 바꿀 일이 거의 없고 배포 주기와 접근 권한이 완전히 다른 제품이라면 저장소를 나누는 편이 자연스러워. 앱 하나와 작은 내부 모듈만 있는데 미래 조직을 상상해서 복잡한 작업 실행기부터 넣을 필요도 없어.

판단 기준은 폴더 수가 아니라 변경 경계야. 여러 패키지를 늘 함께 바꾸고 버전 차이와 로컬 연결이 반복해서 문제를 만든다면 워크스페이스가 값을 해. 반대로 독립 배포가 핵심이면 저장소를 나눈 비용이 오히려 경계를 지켜 줘.

Pippa의 고백

처음 모노레포를 만났을 때는 내부 패키지도 매번 게시한 뒤 버전을 올려야 하는 줄 알았어. 그래서 같은 저장소 안에서 고친 유틸리티를 시험하려고 임시 버전과 연결 명령을 계속 만들었지. 아빠가 “패키지 관리자가 이미 저장소 안의 관계를 알고 있는데 왜 모르는 척하게 해?”라고 물었어. workspace:로 관계를 선언하고 나니 개발 연결과 게시 버전을 한 계약으로 다룰 수 있었어.

Code

pnpm 워크스페이스의 기본 구조 만들기·bash
# 루트에서 기본 구조를 만든다.
mkdir my-repo
cd my-repo
git init
pnpm init

# package.json에는 "private": true를 넣는다.
# pnpm-workspace.yaml에는 packages/*와 apps/*를 등록한다.
mkdir -p packages/utils apps/web

# 각 폴더에서 package.json과 소스 파일을 만든 뒤
# 웹 앱에 내부 패키지를 직접 의존성으로 선언한다.
pnpm --filter @my/web add '@my/utils@workspace:^'

# 설치는 워크스페이스 루트에서 한 번 실행한다.
pnpm install
대상과 실행 방식에 맞춰 스크립트 돌리기·bash
# 패키지 하나만 실행
pnpm --filter @my/utils build

# 이름이 맞는 여러 패키지 실행
pnpm --filter "@my/*" test

# 모든 워크스페이스에서 의존 관계를 고려해 실행
pnpm -r run build

# 개발 서버처럼 함께 떠야 하는 작업은 병렬 실행
pnpm -r --parallel run dev

# 작업 그래프와 캐시가 필요할 때
pnpm turbo run build

External links

Exercise

packages/utils, packages/ui, apps/web 세 패키지로 pnpm 워크스페이스를 만들어. uiutils를, webuiworkspace:^로 의존하게 해. 루트에서 설치한 뒤 각 패키지에 build 스크립트를 만들고 pnpm -r run build의 실행 순서를 확인해. 마지막으로 한 내부 의존성 선언을 지워 가져오기가 어떻게 실패하는지도 시험해.
Hint
각 패키지에 고유한 name을 주고 루트의 pnpm-workspace.yaml에 두 경로를 모두 포함해. 재귀 실행은 기본적으로 워크스페이스 의존 관계를 고려하지만, --parallel을 붙이면 순서를 기다리지 않아. 내부 패키지를 실제로 가져오는 곳에서는 간접 연결에 기대지 말고 자신의 dependencies에 직접 선언해야 해.

Progress

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

댓글 0

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

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