"워크스페이스는 한 저장소 안의 여러 패키지를 서로 모르는 폴더가 아니라 하나의 의존성 그래프로 다루게 해."
패키지가 여러 개가 되는 순간
처음에는 애플리케이션 하나로 충분해도, 시간이 지나면 웹 앱과 CLI, 공유 라이브러리를 따로 배포하거나 시험하고 싶어질 수 있어. 저장소를 각각 나누면 권한과 릴리스 주기를 독립적으로 관리하기 좋지만, 세 저장소를 함께 바꾸는 기능은 PR과 버전 갱신, 배포 순서를 모두 맞춰야 해.
관련 패키지를 한 저장소에 두는 모노레포는 이 변경을 한 커밋과 한 검토 단위로 묶어. 워크스페이스는 그 안의 패키지 목록과 서로의 의존 관계를 패키지 관리자에게 알려 주는 기능이야. 모노레포가 곧 워크스페이스는 아니지만, Node 생태계에서는 둘을 함께 쓰는 경우가 많아.
npm 워크스페이스 선언하기
루트 package.json의 workspaces에 패키지 경로를 적으면 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:로 가리키기
{
"dependencies": {
"@my/utils": "workspace:^",
"@my/ui": "workspace:*"
}
}workspace:^는 게시할 때 현재 내부 버전에 맞는 캐럿 범위로 바뀌어.workspace:~는 틸드 범위로 바뀌어.workspace:*는 현재 내부 버전을 그대로 사용해.
import 문장은 환경에 따라 달라질 필요가 없어.설치 순서와 빌드 순서는 다른 문제야
워크스페이스는 패키지를 설치하고 연결하지만, 각 도구의 빌드 결과가 무엇에 의존하는지까지 모두 알지는 못해. 예를 들어 웹 앱이 공유 라이브러리의 dist 출력을 가져온다면 라이브러리를 먼저 빌드해야 해.
pnpm -r run build는 기본적으로 워크스페이스 의존 관계를 고려해 스크립트를 실행할 수 있어. 입력과 출력에 따른 캐시, 바뀐 패키지만 골라 다시 실행하는 기능이 더 필요하면 Turborepo나 Nx 같은 작업 실행기를 얹어. 패키지가 몇 개 안 되고 전체 빌드가 빠르다면 먼저 패키지 관리자의 재귀 실행만으로 시작해도 충분해.
모노레포를 택하지 않아도 되는 경우
코드를 함께 바꿀 일이 거의 없고 배포 주기와 접근 권한이 완전히 다른 제품이라면 저장소를 나누는 편이 자연스러워. 앱 하나와 작은 내부 모듈만 있는데 미래 조직을 상상해서 복잡한 작업 실행기부터 넣을 필요도 없어.
판단 기준은 폴더 수가 아니라 변경 경계야. 여러 패키지를 늘 함께 바꾸고 버전 차이와 로컬 연결이 반복해서 문제를 만든다면 워크스페이스가 값을 해. 반대로 독립 배포가 핵심이면 저장소를 나눈 비용이 오히려 경계를 지켜 줘.
Pippa의 고백
workspace:로 관계를 선언하고 나니 개발 연결과 게시 버전을 한 계약으로 다룰 수 있었어.