Use Cases (업데이트: 2026. 6. 2.)

Claude Code와 pnpm workspace로 실전 Monorepo 만들기

Claude Code와 pnpm workspace로 작은 monorepo를 설계하고 의존성, filter, CI, 흔한 실수를 정리합니다.

Claude Code와 pnpm workspace로 실전 Monorepo 만들기

pnpm workspace는 코드를 복사하지 않기 위한 현실적인 선택입니다

claudecode-lab.com을 운영하는 Masa입니다.

작은 제품도 금방 여러 부분으로 나뉩니다. 사용자용 웹앱, 관리자 화면, 공통 UI, 설정, 메일 작업, 콘텐츠 생성 스크립트, 테스트 도구가 따로 생깁니다. 문제는 파일 수가 아니라 같은 버튼 문구, API 경로, 환경 변수 이름, validation schema를 여러 곳에 복사하기 시작할 때입니다.

pnpm workspace는 여러 package를 하나의 Git repository에서 관리하는 방식입니다. 공식 pnpm Workspace 문서는 workspace가 monorepo 안의 여러 프로젝트를 하나로 묶고, 루트의 pnpm-workspace.yaml이 필요하다고 설명합니다.

Claude Code는 단순 생성기보다 검토자 역할에 더 잘 맞습니다. workspace:*가 빠졌는지, packages/*apps/*를 역참조하는지, CI가 매번 전체 package를 build하는지 같은 문제를 잡게 하면 효과가 큽니다. 처음부터 거대한 구조를 만들기보다 작은 workspace를 만들고 Claude Code로 반복 점검하는 편이 안전합니다.

이 글은 pnpm 11.5.0 기준으로 작성했습니다. 큰 그림은 Claude Code monorepo 관리의존성 관리 가이드도 함께 보면 좋습니다.


목표 구조: 네 package로 시작하기

처음에는 두 앱과 두 공유 package면 충분합니다.

flowchart LR
  web["apps/web\n@acme/web"] --> ui["packages/ui\n@acme/ui"]
  web --> config["packages/config\n@acme/config"]
  admin["apps/admin\n@acme/admin"] --> ui
  admin --> config
acme-workspace/
  apps/web/src/main.ts
  apps/web/package.json
  apps/admin/src/main.ts
  apps/admin/package.json
  packages/config/src/index.ts
  packages/config/package.json
  packages/ui/src/index.ts
  packages/ui/package.json
  pnpm-workspace.yaml
  package.json
  .npmrc
  CLAUDE.md

대표적인 사용 사례는 세 가지입니다. 첫째, packages/ui에 공통 표시 helper와 작은 UI primitive를 두고 web/admin에서 공유합니다. 둘째, packages/config에 제품명, 공개 URL, feature flag 이름을 모아 앱마다 설정이 달라지는 문제를 막습니다. 셋째, 나중에 packages/contracts를 추가해 API 타입이나 Zod schema를 프론트엔드와 백엔드에서 함께 씁니다.

주의할 점은 packages/common 하나에 모든 것을 넣지 않는 것입니다. 공유 package는 어느 앱에서 호출해도 같은 의미여야 합니다. Claude Code에 요청할 때도 “중복 UI helper만 추출하고 billing 판단은 앱에 남겨라”처럼 경계를 명확히 씁니다.


복사해서 시작하는 최소 설정

루트에는 pnpm-workspace.yaml을 둡니다. 공식 pnpm-workspace.yaml 문서는 packages로 포함할 디렉터리를 지정하고 제외 패턴도 쓸 수 있다고 설명합니다.

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

catalog:
  typescript: ^5.8.3

루트 package.json은 각 package 명령을 호출하는 역할만 합니다.

{
  "name": "acme-workspace",
  "private": true,
  "packageManager": "pnpm@11.5.0",
  "scripts": {
    "check:web": "pnpm --filter @acme/web build",
    "build": "pnpm -r --sort --if-present build",
    "test": "pnpm -r --if-present test",
    "changed:test": "pnpm --filter \"...[origin/main]\" --if-present test"
  },
  "devDependencies": {
    "typescript": "catalog:"
  }
}

tsconfig.base.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "skipLibCheck": true,
    "noEmit": true
  }
}

.npmrc는 로컬 package 해석을 명확하게 만듭니다.

link-workspace-packages=false
save-workspace-protocol=rolling
shared-workspace-lockfile=true
strict-peer-dependencies=true
auto-install-peers=false

핵심은 내부 의존성을 workspace:*로 쓰는 것입니다. pnpm 문서는 workspace: protocol을 쓰면 로컬 workspace package가 아니면 해석하지 않는다고 설명합니다. 내부 package와 registry package 이름이 겹칠 때 특히 중요합니다.

packages/config/package.json:

{
  "name": "@acme/config",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "exports": {
    ".": {
      "types": "./src/index.ts",
      "import": "./src/index.ts"
    }
  }
}
export const appConfig = {
  productName: "Acme Workspace",
  supportEmail: "support@example.com",
  publicSiteUrl: "https://example.com"
} as const;

packages/ui는 config를 workspace 의존성으로 사용합니다.

{
  "name": "@acme/ui",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "dependencies": {
    "@acme/config": "workspace:*"
  },
  "exports": {
    ".": {
      "types": "./src/index.ts",
      "import": "./src/index.ts"
    }
  }
}
import { appConfig } from "@acme/config";

export function renderPrimaryButton(label: string): string {
  return `[${appConfig.productName}] ${label}`;
}

앱 package는 필요한 내부 package만 선언합니다.

{
  "name": "@acme/web",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "scripts": {
    "build": "tsc -p tsconfig.json"
  },
  "dependencies": {
    "@acme/config": "workspace:*",
    "@acme/ui": "workspace:*"
  }
}

apps/web/tsconfig.json:

{
  "extends": "../../tsconfig.base.json",
  "include": ["src"]
}
import { appConfig } from "@acme/config";
import { renderPrimaryButton } from "@acme/ui";

console.log(appConfig.publicSiteUrl);
console.log(renderPrimaryButton("Start trial"));

실행은 다음 순서입니다.

corepack pnpm install
corepack pnpm --filter @acme/web build
corepack pnpm -r --sort --if-present build

Claude Code에는 먼저 package 경계를 알려야 합니다

Claude Code 공식 문서에는 monorepos and large codebases 가이드가 있습니다. 큰 저장소에서는 관련 없는 파일 읽기가 context를 낭비합니다. pnpm workspace에서도 root에서 시작할 수는 있지만, 작업 범위를 좁혀야 합니다.

루트 CLAUDE.md에는 전체 규칙만 둡니다.

# Acme Workspace

This repository is a pnpm workspace.

Packages:
- apps/web: customer-facing TypeScript app
- apps/admin: internal admin app
- packages/ui: shared UI helpers
- packages/config: shared runtime constants

Rules:
- Use pnpm, not npm or yarn.
- Add internal dependencies with workspace:*.
- Run focused commands with pnpm --filter before full workspace commands.
- Do not move business logic into packages/ui.

package별 규칙은 해당 디렉터리의 CLAUDE.md에 둡니다. Claude Code memory 문서는 CLAUDE.md가 지속적인 지시로 사용된다고 설명합니다. 그래서 오래 유지할 수 있는 짧은 규칙이 좋습니다.

claude -p "
Inspect this pnpm workspace. Do not edit files yet.
List the package graph, scripts, and risky dependency directions.
Then propose the smallest change needed to share UI helpers between apps/web and apps/admin.
"

먼저 점검하고 나서 최소 변경을 제안하게 하면, Claude Code가 불필요한 추상화를 만드는 일을 줄일 수 있습니다.


filter를 쓰면 개발과 CI가 가벼워집니다

pnpm Filtering은 명령을 특정 package 집합에만 적용하는 기능입니다.

pnpm --filter @acme/web build
pnpm --filter @acme/web... build
pnpm --filter ...@acme/ui test
pnpm --filter "...[origin/main]" --if-present test

가장 흔한 실수는 ... 방향입니다. @acme/web...은 web과 web이 의존하는 package를 선택합니다. ...@acme/ui는 ui와 ui에 의존하는 앱을 선택합니다. UI를 바꾼 뒤 @acme/ui...만 실행하면 web/admin 테스트를 놓칠 수 있습니다.

CI는 영향을 받은 package만 검사하도록 만들 수 있습니다.

name: workspace-check

on:
  pull_request:

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: pnpm
      - run: corepack enable
      - run: corepack prepare pnpm@11.5.0 --activate
      - run: pnpm install --frozen-lockfile
      - run: pnpm --filter "...[origin/main]" --if-present test
      - run: pnpm --filter "...[origin/main]" --if-present build

자주 나는 실수와 수정 방법

첫 번째 실수는 내부 package를 일반 semver로 쓰는 것입니다.

{
  "dependencies": {
    "@acme/ui": "^0.1.0"
  }
}

workspace 내부라면 이렇게 씁니다.

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

두 번째 실수는 공유 package에 앱 전용 로직을 넣는 것입니다. packages/ui가 billing 조건을 판단하거나 admin 전용 API를 호출하면 경계가 무너진 것입니다. 세 번째 실수는 순환 의존성입니다. packages/uiapps/web을 import하면 설계 방향이 반대입니다.

정기적으로 Claude Code에 경계 점검을 맡기면 좋습니다.

claude -p "
Check this workspace for circular dependencies and misplaced imports.
Focus on packages/* importing from apps/*, duplicated config values,
and dependencies that should be workspace:*.
Return findings with file paths and minimal fixes.
"

공개 package가 필요하면 Changesets를 추가합니다.

pnpm add -Dw @changesets/cli
pnpm changeset init
pnpm changeset
pnpm changeset version
pnpm -r publish --access public

pnpm 문서도 workspace package versioning은 Changesets나 Rush 같은 도구로 다루는 흐름을 안내합니다. 앱 package는 보통 private: true를 유지합니다.


정리와 실제 확인 결과

pnpm workspace는 복잡한 빌드 시스템이 아니라, 공유 UI, 설정, 타입, 테스트를 명확한 의존성으로 다루기 위한 작은 기반입니다. Claude Code는 이 기반이 무너지지 않았는지 검토하고, 최소 변경을 제안하고, CI 실패를 좁히는 역할에 잘 맞습니다.

다음 단계로는 CLAUDE.md best practicesClaude Code testing strategies를 함께 보세요. 팀 도입이나 운영 규칙 설계가 필요하면 Claude Code training에서 상담할 수 있습니다.

이 글의 예시는 Windows, Node.js 22, Corepack, pnpm 11.5.0 기준으로 확인했습니다. 실제로는 workspace:* 누락과 filter 방향 착각이 가장 자주 발생합니다. 편집 전에 Claude Code가 package graph를 출력하게 하는 습관만으로도, 불필요한 공통화와 순환 의존성을 훨씬 빨리 발견할 수 있습니다.

#claude-code #pnpm #workspace #monorepo #typescript #ci
무료

무료 PDF: Claude Code 치트시트

이메일을 입력하면 명령, 리뷰 습관, 안전한 워크플로를 정리한 PDF를 받을 수 있습니다.

개인정보를 안전하게 관리하며 스팸을 보내지 않습니다.

Masa

작성자 소개

Masa

Claude Code 실무 워크플로와 팀 도입을 검증하는 엔지니어입니다.