architecture2025-04-10·8·124/348

npm workspaces 멀티 패키지 관리

Managing monorepo projects with npm workspaces including shared dependencies, cross-package scripts, and publishing.

npm workspaces 멀티 패키지 관리

Introduction

As projects grow, monorepo structures with multiple packages become necessary. npm workspaces provide a built-in way to manage multiple packages in a single repository with shared dependencies and scripts. This post covers setting up and using npm workspaces effectively.

Environment

node --version
# v20.11.0

npm --version
# 10.2.4

Problem

Managing multiple packages with separate repositories leads to:

  1. Dependency duplication across packages
  2. Version conflicts between packages
  3. Complex cross-package development
  4. Difficult publishing workflows
project/
├── package-a/
│   ├── node_modules/  ← Duplicate dependencies
│   └── package.json
├── package-b/
│   ├── node_modules/  ← More duplicates
│   └── package.json
└── package.json       ← No workspace management

Analysis

npm workspaces solve these problems by:

  1. Hoisting shared dependencies to the root node_modules
  2. Creating symlinks for local packages
  3. Running scripts across multiple packages
  4. Managing versions consistently

The workspace structure:

project/
├── packages/
│   ├── package-a/
│   │   └── package.json
│   └── package-b/
│       └── package.json
├── node_modules/        ← Shared dependencies
├── package.json         ← Workspace root
└── package-lock.json    ← Single lockfile

Solution

Step 1: Create workspace structure

// root package.json
{
  "name": "my-monorepo",
  "version": "1.0.0",
  "private": true,
  "workspaces": [
    "packages/*"
  ],
  "scripts": {
    "build": "npm run build --workspaces",
    "test": "npm run test --workspaces",
    "lint": "npm run lint --workspaces",
    "clean": "npm run clean --workspaces"
  }
}

Step 2: Create workspace packages

// packages/package-a/package.json
{
  "name": "@myorg/package-a",
  "version": "1.0.0",
  "main": "src/index.js",
  "scripts": {
    "build": "tsc",
    "test": "jest"
  },
  "dependencies": {
    "lodash": "^4.17.21"
  }
}
// packages/package-b/package.json
{
  "name": "@myorg/package-b",
  "version": "1.0.0",
  "main": "src/index.js",
  "dependencies": {
    "@myorg/package-a": "^1.0.0",
    "axios": "^1.6.0"
  }
}

Step 3: Install dependencies

# Install all dependencies
npm install

# Add dependency to specific workspace
npm install lodash --workspace=@myorg/package-a

# Add dev dependency to root
npm install jest --save-dev

# List workspaces
npm workspaces list
# packages/package-a
# packages/package-b

Step 4: Run scripts across workspaces

# Run build in all workspaces
npm run build --workspaces

# Run build in specific workspace
npm run build --workspace=@myorg/package-a

# Run test in all workspaces
npm run test --workspaces

# Run command in all workspaces
npm exec --workspaces -- command

Step 5: Cross-package dependencies

// packages/package-b/package.json
{
  "name": "@myorg/package-b",
  "dependencies": {
    "@myorg/package-a": "*"
  }
}
// packages/package-b/src/index.js
const packageA = require('@myorg/package-a');

console.log(packageA.doSomething());

Step 6: Workspace-specific configuration

// packages/package-a/package.json
{
  "name": "@myorg/package-a",
  "scripts": {
    "build": "tsc",
    "test": "jest",
    "lint": "eslint src/",
    "prepublishOnly": "npm run build"
  },
  "files": [
    "dist",
    "src"
  ],
  "publishConfig": {
    "access": "public"
  }
}

Step 7: Publishing workspaces

# Publish all workspaces
npm publish --workspaces

# Publish specific workspace
npm publish --workspace=@myorg/package-a

# Dry run
npm publish --dry-run --workspaces

Step 8: Advanced workspace scripts

// root package.json
{
  "scripts": {
    "dev": "npm run dev --workspaces --if-present",
    "build:all": "npm run build --workspaces --if-present",
    "test:all": "npm run test --workspaces --if-present",
    "version:patch": "npm version patch --workspaces --no-git-tag-version",
    "clean:all": "rm -rf node_modules packages/*/node_modules",
    "lint:fix": "npm run lint:fix --workspaces --if-present"
  }
}

Lessons Learned

  1. Use scoped packages - @myorg/package-name prevents naming conflicts
  2. Keep private: true in root - The root package should not be published
  3. Use --if-present - Prevents errors when scripts do not exist in all workspaces
  4. Lock versions with package-lock.json - A single lockfile for the entire monorepo
  5. Consider Turborepo or Nx for complex build pipelines

This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.