Advanced (업데이트: 2026. 6. 1.)

Claude Code로 모노레포 관리하기: pnpm, Turborepo, Nx, CI 실전 가이드

Claude Code로 모노레포를 안전하게 관리하는 방법. repo map, pnpm workspace, affected tasks, CODEOWNERS, CI 예제 포함.

Claude Code로 모노레포 관리하기: pnpm, Turborepo, Nx, CI 실전 가이드

모노레포는 여러 앱과 라이브러리를 하나의 Git 저장소에서 관리하는 구조입니다. apps/web, apps/api, packages/ui, packages/shared처럼 나누면 타입, UI, 설정, CI를 재사용하기 좋습니다. 하지만 경계를 정하지 않으면 Claude Code가 좋은 의도로 만든 변경이 여러 앱을 동시에 깨뜨릴 수 있습니다.

실무 흐름은 명확합니다. 먼저 Claude Code에 repo map을 만들게 하고, package boundary를 정한 뒤, pnpm workspaceworkspace:*로 내부 의존성을 고정합니다. 그다음 Turborepo 또는 Nx의 affected tasks로 변경 영향이 있는 범위만 lint, test, build합니다. 공식 개념은 Nx monorepo 판단 기준, Nx affected, Nx mental model, pnpm, Turborepo docs를 기준으로 보면 됩니다.

먼저 구조를 그린다

코드를 고치기 전에 저장소 구조를 먼저 합의해야 합니다.

graph TD
  WEB["apps/web"] --> UI["packages/ui"]
  WEB --> SHARED["packages/shared"]
  API["apps/api"] --> SHARED
  UI --> CONFIG["packages/config"]
  SHARED --> CONFIG
  CI["CI affected tasks"] --> WEB
  CI --> API

apps/*는 배포되는 애플리케이션이고, packages/*는 재사용되는 모듈입니다. package boundary는 어떤 패키지가 어떤 패키지에 의존해도 되는지 정한 규칙입니다. Claude Code에게 이 규칙을 명시해야 합니다.

Claude Code 첫 프롬프트

이 저장소를 모노레포로 분석해 주세요.

전제:
- apps/web 은 Next.js 앱입니다
- apps/api 는 API 서버입니다
- packages/ui 는 공통 UI입니다
- packages/shared 는 타입, 검증, 순수 함수만 둡니다
- packages/config 는 ESLint, TypeScript, Prettier, 테스트 설정입니다

규칙:
- apps/* 는 apps/* 에 직접 의존하지 않습니다
- packages/* 는 apps/* 에 의존하지 않습니다
- 내부 패키지 의존성은 workspace:* 를 사용합니다
- 변경 후에는 affected tasks로 lint/test/build를 확인합니다

먼저 의존성, 위험한 순환 의존성, 과도하게 공유된 파일, CI 확인 명령을 repo map으로 정리해 주세요.
아직 파일은 수정하지 마세요.

마지막 문장이 중요합니다. 모노레포에서는 한 파일의 수정이 아니라 의존성 방향이 문제를 만듭니다. 먼저 지도와 위험 목록을 받은 뒤 수정 범위를 정하는 편이 안전합니다.

pnpm workspace 기본 설정

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

루트 package.json에는 사람이든 Claude Code든 같은 명령을 쓰도록 스크립트를 고정합니다.

{
  "name": "acme-monorepo",
  "private": true,
  "packageManager": "pnpm@10.12.1",
  "scripts": {
    "build": "turbo run build",
    "lint": "turbo run lint",
    "test": "turbo run test",
    "typecheck": "turbo run typecheck",
    "ci:affected": "turbo run lint test build --affected",
    "check:deps": "node scripts/check-workspace-deps.cjs"
  }
}

내부 패키지는 반드시 workspace:*로 선언합니다.

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

Claude Code에는 이렇게 요청합니다.

apps/web에서 @acme/ui와 @acme/shared를 사용할 수 있게 해 주세요.
package.json은 workspace:*를 사용하고, ../../packages 경로 import는 만들지 마세요.
수정 후 pnpm check:deps와 pnpm ci:affected로 확인 가능한 상태로 만들어 주세요.

Turborepo와 Nx affected

Turborepo는 기존 package scripts를 병렬 실행하고 캐시하는 데 적합합니다.

{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**", "!.next/cache/**"]
    },
    "lint": {
      "dependsOn": ["^build"]
    },
    "test": {
      "dependsOn": ["^build"],
      "outputs": ["coverage/**"]
    },
    "typecheck": {
      "dependsOn": ["^build"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}

프로젝트 그래프와 영향 범위 계산을 더 엄격하게 하고 싶다면 Nx를 추가합니다.

pnpm dlx nx@latest init
pnpm nx affected -t lint test build --base=origin/main --head=HEAD

중요한 점은 “전체를 다 돌려”가 아니라 “변경 영향 범위만 확인해”라고 지시하는 것입니다. 그래야 CI 시간과 비용이 커지지 않습니다.

CODEOWNERS와 의존성 정책

/apps/web/ @acme/frontend
/apps/api/ @acme/backend
/packages/ui/ @acme/design-system
/packages/shared/ @acme/platform
/packages/config/ @acme/platform
/pnpm-workspace.yaml @acme/platform
/turbo.json @acme/platform

의존성 정책은 코드로 검사합니다. 다음 파일을 scripts/check-workspace-deps.cjs로 저장하면 됩니다.

const fs = require("node:fs");
const path = require("node:path");

const ROOT = process.cwd();
const WORKSPACE_DIRS = ["apps", "packages"];
const DEP_FIELDS = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"];

function readJson(file) {
  return JSON.parse(fs.readFileSync(file, "utf8"));
}

function findPackageDirs(baseDir) {
  const absoluteBase = path.join(ROOT, baseDir);
  if (!fs.existsSync(absoluteBase)) return [];
  return fs
    .readdirSync(absoluteBase, { withFileTypes: true })
    .filter((entry) => entry.isDirectory())
    .map((entry) => path.join(absoluteBase, entry.name))
    .filter((dir) => fs.existsSync(path.join(dir, "package.json")));
}

const packages = WORKSPACE_DIRS.flatMap(findPackageDirs).map((dir) => {
  const manifest = readJson(path.join(dir, "package.json"));
  return { dir, name: manifest.name, manifest };
});

const byName = new Map(packages.map((pkg) => [pkg.name, pkg]));
let failed = false;

for (const pkg of packages) {
  for (const field of DEP_FIELDS) {
    const deps = pkg.manifest[field] || {};
    for (const [name, range] of Object.entries(deps)) {
      const internal = byName.get(name);
      if (!internal) continue;
      const fromDir = path.relative(ROOT, pkg.dir).replace(/\\/g, "/");
      const toDir = path.relative(ROOT, internal.dir).replace(/\\/g, "/");

      if (!String(range).startsWith("workspace:")) {
        console.error(`${pkg.name}: ${name} must use workspace:* in ${field}`);
        failed = true;
      }
      if (toDir.startsWith("apps/")) {
        console.error(`${pkg.name}: ${fromDir} must not depend on app package ${toDir}`);
        failed = true;
      }
    }
  }
}

if (failed) process.exit(1);
console.log(`Checked ${packages.length} workspace packages.`);

CI 체크리스트

name: monorepo-ci

on:
  pull_request:
  push:
    branches: [main]

jobs:
  checks:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: pnpm/action-setup@v4
        with:
          version: 10
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm check:deps
      - run: pnpm ci:affected

fetch-depth: 0은 affected 계산에 필요합니다. Git 히스토리가 얕으면 변경 범위를 정확히 판단하기 어렵습니다.

실무 유스케이스

  1. packages/ui의 Button에 loading 상태를 추가합니다. 공개 API를 유지하고, 영향을 받는 apps/web 화면을 Claude Code가 목록화하게 합니다.

  2. API와 프런트엔드가 공유하는 DTO를 packages/shared로 옮깁니다. DTO는 전송용 데이터 형태이며, DB 모델이나 framework 전용 코드는 넣지 않습니다.

  3. TypeScript, Next.js, 테스트 도구를 업그레이드합니다. packages/config부터 수정하고 affected 결과로 영향을 받은 앱을 확인합니다.

  4. 결제, 검색, 온보딩처럼 여러 팀이 걸린 기능은 Claude Code에게 PR 분할안을 먼저 만들게 합니다.

흔한 함정

첫째, packages/shared가 잡동사니 폴더가 되는 것입니다. 안정적이고 일반적이며 테스트하기 쉬운 코드만 넣어야 합니다.

둘째, ../../packages/shared/src 같은 상대 경로 import입니다. 빌드 순서와 공개 API 검토를 망칩니다.

셋째, Turborepo와 Nx를 동시에 깊게 도입하는 것입니다. 먼저 하나의 실행 모델을 정하고 필요할 때 확장하세요.

넷째, 로컬에서 한 앱만 통과한 것을 안전하다고 보는 것입니다. PR에는 변경 패키지, 영향 앱, 실행 명령, 남은 리스크를 적어야 합니다.

리뷰 프롬프트

이번 diff를 모노레포 관점에서 리뷰해 주세요.

확인 항목:
- apps/* 에서 apps/* 로 직접 의존하지 않는가
- packages/* 에서 apps/* 로 의존하지 않는가
- 내부 의존성이 workspace:* 인가
- packages/shared 에 안정적인 공유 코드만 있는가
- affected lint/test/build가 충분한가
- CODEOWNERS 리뷰 담당이 명확한가

출력:
- blocker
- 수정 권장
- 확인한 명령
- PR 본문에 넣을 영향 범위

관련 글로는 Claude Code와 Nx workspace, Claude Code와 pnpm workspace, Claude Code와 Turborepo, Claude Code 팀 협업을 함께 보면 좋습니다.

팀에서 Claude Code를 모노레포에 도입하려면 도구보다 먼저 경계, owner, CI, review 템플릿을 정해야 합니다. ClaudeCodeLab의 Claude Code 교육 및 상담에서는 실제 저장소 기준으로 CLAUDE.md, CODEOWNERS, CI, PR 흐름까지 함께 설계할 수 있습니다.

정리

Claude Code는 모노레포에서 강력하지만, 제약이 명확할 때만 안전합니다. repo map, package boundary, pnpm workspace, Turborepo/Nx affected tasks, CODEOWNERS, 의존성 검사, CI 체크리스트를 먼저 두면 결과물이 리뷰 가능한 PR로 바뀝니다.

실제로 적용했을 때 가장 효과가 컸던 것은 workspace:* 강제와 pnpm ci:affected 표준화였습니다. 특히 packages/shared 변경 시 영향 앱을 Claude Code가 먼저 나열하게 하니 리뷰 누락과 불필요한 전체 CI가 줄었습니다.

#Claude Code #monorepo #pnpm workspace #Turborepo #Nx
무료

무료 PDF: Claude Code 치트시트

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

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

Masa

작성자 소개

Masa

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