Skip to content

npm 워크스페이스

npm의 워크스페이스 기능을 활용하여 하나의 저장소에서 여러 패키지를 관리하는 모노레포를 구성하고, 호이스팅과 의존성 해석의 동작 원리를 이해한다.

학습 목표

  1. npm 프로젝트의 구성과 package.json의 역할을 이해한다.
  2. npm link를 이용한 로컬 패키지 연결 방법과 한계를 파악한다.
  3. npm 워크스페이스의 설정 방법과 동작 원리를 익힌다.
  4. 호이스팅(hoisting)과 의존성 해석 메커니즘을 이해하고, Phantom Dependency 문제를 인식한다.
  5. 워크스페이스 명령어(npm install, npm run --workspace)를 활용할 수 있다.

1. npm 프로젝트 구조와 package.json

npm 프로젝트란 package.json 파일이 존재하는 디렉터리를 말한다. package.json은 프로젝트의 메타데이터, 의존성, 스크립트를 선언적으로 기록하는 핵심 파일이다.

json
{
  "name": "my-app",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "start": "node index.js",
    "build": "webpack --mode production",
    "test": "jest"
  },
  "dependencies": {
    "axios": "^1.6.0"
  },
  "devDependencies": {
    "webpack": "^5.89.0"
  }
}

require()의 모듈 탐색 알고리즘

Node.js에서 require('package-name')을 호출하면 다음 순서로 모듈을 찾는다.

이 탐색 메커니즘이 워크스페이스에서 호이스팅이 작동하는 이유이자, Phantom Dependency가 발생하는 근본 원인이다.


2. npm link로 로컬 패키지 연결하기

외부 레지스트리에서 다운로드하지 않고, 로컬에 있는 패키지를 다른 프로젝트에서 사용하려면 심볼릭 링크를 활용해야 한다.

심볼릭 링크는 다른 파일이나 디렉터리를 가리키는 특별한 파일이다. 원본이 변경되면 링크를 통해 접근하는 내용도 함께 변경된다.

bash
# 심볼릭 링크 생성
ln -s /actual/path/to/package-b ./node_modules/package-b

npm link는 심볼릭 링크를 자동으로 생성해 주는 편의 명령어이다.

bash
# 1단계: 패키지를 글로벌에 링크 등록
cd packages/b
npm link

# 2단계: 다른 프로젝트에서 링크된 패키지 사용
cd packages/a
npm link b

2.3 npm link의 한계

문제설명
수동 관리패키지마다 npm link를 개별적으로 실행해야 함
글로벌 오염글로벌 node_modules에 링크가 등록되어 다른 프로젝트에 영향 가능
선언적이지 않음package.json에 기록되지 않아 팀원이 동일 환경을 재현하기 어려움
CI 환경CI/CD 파이프라인에서 매번 link 명령을 수동 실행해야 함

이 한계를 해결하기 위해 워크스페이스 기능이 등장했다.


3. npm 워크스페이스 설정 방법

npm v7부터 도입된 워크스페이스 기능은 package.jsonworkspaces 필드로 선언적으로 모노레포를 구성할 수 있게 한다.

3.1 프로젝트 구조

monorepo-root/
  package.json              # 루트 package.json (workspaces 선언)
  package-lock.json         # 통합 lock 파일
  node_modules/             # 호이스팅된 의존성 + 워크스페이스 심볼릭 링크
    a -> ../packages/a      # 심볼릭 링크
    b -> ../packages/b      # 심볼릭 링크
    axios/                  # 호이스팅된 외부 패키지
  packages/
    a/
      package.json
      index.js
    b/
      package.json
      index.js

3.2 설정 단계

1) 루트 프로젝트 초기화

bash
mkdir npm-workspaces-example && cd npm-workspaces-example
npm init -y

2) 워크스페이스 선언 (package.json)

json
{
  "name": "npm-workspaces-example",
  "version": "1.0.0",
  "private": true,
  "workspaces": [
    "packages/*"
  ]
}

"packages/*" 글로브 패턴을 사용하면 packages/ 하위의 모든 디렉터리를 워크스페이스로 인식한다.

3) 워크스페이스 생성

bash
# npm v7+ 에서 -w 플래그로 워크스페이스 초기화
npm init -w packages/a -y
npm init -w packages/b -y

4) 의존성 설치

bash
# 전체 워크스페이스 의존성 일괄 설치
npm install

# 특정 워크스페이스에 의존성 추가
npm install axios -w a

npm install을 루트에서 실행하면 모든 워크스페이스의 의존성이 루트 node_modules호이스팅되어 설치된다.


4. 호이스팅(Hoisting)과 의존성 해석

4.1 호이스팅이란?

npm 워크스페이스에서 npm install을 실행하면, 각 워크스페이스의 의존성을 루트 node_modules로 끌어올리는(hoist) 작업이 수행된다.

[호이스팅 전 - 개념적 구조]          [호이스팅 후 - 실제 구조]

packages/a/                          node_modules/
  node_modules/                        a/ (symlink)
    axios/                             b/ (symlink)
packages/b/                            axios/       <-- 호이스팅됨
  node_modules/
    axios/

호이스팅의 장점은 같은 버전의 패키지를 중복 설치하지 않아 디스크를 절약한다는 것이다.

4.2 호이스팅이 동작하는 이유

Node.js의 require() 탐색 알고리즘 덕분이다. packages/b/index.js에서 require('axios')를 호출하면:

  1. packages/b/node_modules/axios -- 없음
  2. packages/node_modules/axios -- 없음
  3. node_modules/axios -- 발견! (루트에 호이스팅된 axios)

이렇게 상위 디렉터리로 올라가며 탐색하기 때문에 루트에 설치된 패키지를 하위 워크스페이스에서 사용할 수 있다.

4.3 Phantom Dependency 문제

호이스팅의 부작용으로, package.json에 선언하지 않은 패키지도 사용할 수 있게 된다.

javascript
// packages/a/index.js
const b = require('b');       // OK - package.json에 b를 선언함
const axios = require('axios'); // 위험! - a의 package.json에 axios를 선언하지 않았는데 동작함

axiosb의 의존성인데, 루트 node_modules에 호이스팅되어 있으므로 a에서도 접근 가능하다. 이것이 Phantom Dependency이며, baxios를 제거하면 a가 갑자기 깨진다.


5. 워크스페이스 명령어

5.1 의존성 관리

bash
# 특정 워크스페이스에 의존성 추가
npm install axios -w a
npm install lodash -w b

# 특정 워크스페이스에서 의존성 제거
npm uninstall axios -w a

# 내부 워크스페이스를 의존성으로 추가
# a의 package.json에 "b": "*" 추가 후
npm install -w a

5.2 스크립트 실행

bash
# 특정 워크스페이스의 스크립트 실행
npm run start -w a

# 모든 워크스페이스에서 스크립트 실행
npm run build --workspaces

# 스크립트가 없는 워크스페이스는 건너뛰기
npm run build --workspaces --if-present

5.3 정보 조회

bash
# 모든 워크스페이스 목록 확인
npm ls --workspaces

# 특정 워크스페이스의 의존성 트리 확인
npm ls -w a

# 워크스페이스 간 의존 관계 확인
npm explain b

6. 워크스페이스 내부 패키지 간 의존 관계 설정

워크스페이스 a에서 워크스페이스 b를 사용하려면, apackage.json에 명시적으로 선언해야 한다.

json
// packages/a/package.json
{
  "name": "a",
  "version": "1.0.0",
  "dependencies": {
    "b": "*"
  }
}

"b": "*"은 로컬 워크스페이스 b를 의존성으로 사용하겠다는 의미이다. npm install을 실행하면 node_modules/bpackages/b로의 심볼릭 링크가 자동 생성된다.

실습 예제

javascript
// packages/b/index.js
const axios = require('axios');
module.exports = async function fetchUsers() {
  const response = await axios.get('https://api.github.com/users');
  return response.data;
};

// packages/a/index.js
const fetchUsers = require('b');
(async function main() {
  const users = await fetchUsers();
  console.log(users.map(u => u.login).join(', '));
})();

이때 axiosbpackage.json에 선언하는 것이 올바르다. apackage.json에만 선언하고 b에서 사용하는 것은 호이스팅 덕분에 동작하지만, 의미적으로 잘못된 의존 관계이다.


7. npm 워크스페이스의 한계

한계설명
Phantom Dependency평탄한 node_modules 구조로 인해 선언하지 않은 의존성 사용 가능
느린 설치 속도pnpm이나 Yarn Berry에 비해 설치 속도가 느림
디스크 비효율같은 패키지를 여러 프로젝트에서 사용해도 프로젝트마다 별도 복사
제한된 필터링pnpm의 --filter 같은 고급 필터링 기능이 부족
버전 호환성npm v7 이상에서만 워크스페이스 지원

핵심 정리

  1. npm 프로젝트package.json이 있는 디렉터리이며, require()node_modules를 상위 디렉터리로 탐색하는 알고리즘을 사용한다.
  2. npm link는 심볼릭 링크로 로컬 패키지를 연결하지만, 선언적이지 않고 수동 관리가 필요하다.
  3. npm 워크스페이스package.jsonworkspaces 필드로 모노레포를 선언적으로 구성하며, npm install 한 번으로 모든 의존성과 워크스페이스 링크를 설정한다.
  4. 호이스팅은 의존성을 루트 node_modules로 끌어올려 중복을 줄이지만, 선언하지 않은 패키지에 접근 가능한 Phantom Dependency 문제를 야기한다.
  5. npm 워크스페이스의 한계(Phantom Dependency, 속도, 디스크 효율)는 pnpm과 Yarn Berry가 해결하고자 한 핵심 과제이다.

다음 단계

  • 03. Yarn 워크스페이스: Yarn Classic과 Berry의 워크스페이스 기능을 비교하고, PnP 모드의 동작 원리를 이해한다.