Skip to content

워크스페이스 관리

모노레포에서 여러 패키지를 하나의 레포에서 관리하는 워크스페이스의 동작 원리(심볼릭 링크, 호이스팅)와 설정 방법, 패키지 간 의존성 선언과 해석 흐름을 이해한다.

학습 목표

  1. 워크스페이스의 개념과 패키지 매니저별 설정 방법을 이해한다.
  2. 심볼릭 링크를 통한 내부 패키지 연결 원리를 파악한다.
  3. 호이스팅(hoisting)의 개념과 동작 방식, 주의점을 이해한다.
  4. 패키지 간 의존성을 선언하고 해석하는 흐름을 설명할 수 있다.
  5. 워크스페이스 관리 시 자주 발생하는 문제와 해결 방법을 파악한다.

본문

1. 워크스페이스란?

워크스페이스는 하나의 Git 저장소 안에서 여러 독립적인 패키지를 관리할 수 있게 해주는 패키지 매니저의 기능이다.

워크스페이스가 없던 시절에는 하나의 레포에 여러 프로젝트가 있어도, 각 프로젝트의 의존성을 개별적으로 설치하고 연결해야 했다. 워크스페이스 기능을 사용하면 패키지 매니저가 자동으로 내부 패키지를 감지하고 연결해 준다.

워크스페이스의 핵심 역할:

역할설명
패키지 등록루트 설정에서 어떤 디렉터리가 워크스페이스인지 선언
내부 패키지 연결내부 의존성을 npm 레지스트리가 아닌 로컬 경로로 연결
의존성 통합 설치루트에서 한 번의 install 명령으로 모든 워크스페이스의 의존성을 설치
명령어 위임특정 워크스페이스에 대해 스크립트를 실행하는 명령어 제공

2. 패키지 매니저별 워크스페이스 설정

세 가지 주요 패키지 매니저(npm, yarn, pnpm) 모두 워크스페이스를 지원하지만, 설정 방식이 약간 다르다.

npm / yarn

package.jsonworkspaces 필드를 추가한다.

json
{
  "name": "my-monorepo",
  "private": true,
  "workspaces": [
    "apps/*",
    "packages/*"
  ]
}

apps/*apps/ 디렉터리 아래의 모든 하위 디렉터리를 워크스페이스로 등록한다는 의미다. 각 하위 디렉터리에는 반드시 자체 package.json이 있어야 한다.

pnpm

별도의 pnpm-workspace.yaml 파일을 사용한다.

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

pnpm은 package.jsonworkspaces 필드를 무시하고, 이 YAML 파일만 참조한다.


3. 심볼릭 링크를 통한 패키지 연결

워크스페이스의 핵심 동작 원리는 **심볼릭 링크(symlink)**다.

외부 패키지(예: react)는 npm 레지스트리에서 다운로드하여 node_modules에 설치된다. 하지만 내부 패키지(예: @my-org/ui-kit)는 다운로드할 필요가 없다. 이미 같은 레포에 소스 코드가 있기 때문이다.

패키지 매니저는 내부 의존성을 발견하면, node_modules 안에 해당 패키지로의 심볼릭 링크를 생성한다.

심볼릭 링크 덕분에 web-app에서 import { Button } from '@my-org/ui-kit'을 작성하면, 실제로는 packages/ui-kit/ 디렉터리의 코드를 직접 참조하게 된다.

심볼릭 링크의 장점

  • 즉시 반영: 라이브러리 코드를 수정하면 앱에서 바로 반영된다 (npm 퍼블리시 불필요).
  • 단일 소스: 코드가 복사되는 것이 아니라 원본을 참조하므로 디스크 공간 절약.
  • 개발 편의: npm link를 수동으로 설정할 필요 없이 패키지 매니저가 자동 관리.

pnpm의 특수한 심볼릭 링크 방식

pnpm은 **콘텐츠 어드레서블 저장소(content-addressable store)**를 사용한다. 모든 패키지를 글로벌 스토어에 한 번만 저장하고, node_modules에는 하드 링크와 심볼릭 링크를 조합하여 연결한다. 이 방식은 디스크 공간을 크게 절약하고, 여러 프로젝트에서 동일 버전의 패키지를 공유할 수 있게 한다.


4. 호이스팅 (Hoisting)

호이스팅은 여러 워크스페이스에서 공통으로 사용하는 의존성을 루트의 node_modules로 끌어올리는 동작이다.

호이스팅이 필요한 이유

워크스페이스 A, B, C가 모두 react@18.3.0을 사용한다고 가정하자. 호이스팅 없이는 각 워크스페이스의 node_modulesreact가 3번 설치된다. 호이스팅을 적용하면 루트 node_modules에 1번만 설치하고, Node.js의 모듈 해석 알고리즘에 의해 각 워크스페이스에서 접근할 수 있다.

Node.js 모듈 해석과 호이스팅

Node.js는 require() 또는 import를 만나면 현재 디렉터리의 node_modules부터 시작하여 상위 디렉터리로 올라가며 모듈을 탐색한다. 이 원리 덕분에 루트에 설치된 패키지를 하위 워크스페이스에서 사용할 수 있다.

탐색 순서 예시 (apps/web-app/src/index.ts에서 react를 import할 때):

  1. apps/web-app/src/node_modules/react -> 없음
  2. apps/web-app/node_modules/react -> 없음
  3. apps/node_modules/react -> 없음
  4. (루트)/node_modules/react -> 발견! (호이스팅으로 여기에 설치됨)

패키지 매니저별 호이스팅 동작

패키지 매니저호이스팅 동작특징
npm기본적으로 호이스팅 수행가장 적극적으로 호이스팅. nohoist 설정으로 일부 제외 가능
yarn (classic)기본적으로 호이스팅 수행nohoist 패턴으로 세밀하게 제어 가능
yarn (berry/v2+)PnP 모드에서는 호이스팅 없음Plug'n'Play 방식으로 node_modules 자체를 사용하지 않을 수 있음
pnpm기본적으로 호이스팅하지 않음엄격한 의존성 관리. package.json에 선언되지 않은 패키지 접근 차단

호이스팅의 위험성: 유령 의존성 (Phantom Dependencies)

호이스팅에는 유령 의존성이라는 부작용이 있다.

워크스페이스 A가 lodash를 의존하고, 이것이 루트로 호이스팅되었다고 하자. 워크스페이스 B는 lodashpackage.json에 선언하지 않았지만, 루트에 설치되어 있으므로 실제로는 접근이 가능하다. 이 상태로 개발하다가 워크스페이스 A에서 lodash 의존을 제거하면, 워크스페이스 B도 갑자기 동작하지 않게 된다.

이런 문제를 방지하기 위해:

  • pnpm은 기본적으로 엄격 모드를 적용하여 선언되지 않은 의존성 접근을 차단한다.
  • npm/yarn 사용 시에도 package.json에 실제로 사용하는 모든 의존성을 명시적으로 선언해야 한다.

5. 워크스페이스 의존성 선언과 해석

의존성 선언 방법

내부 패키지에 대한 의존성은 package.json에 다음과 같이 선언한다.

json
{
  "name": "@my-org/web-app",
  "dependencies": {
    "@my-org/ui-kit": "workspace:*",
    "@my-org/utils": "workspace:^1.0.0",
    "react": "^18.3.0"
  }
}

workspace: 프로토콜은 패키지 매니저에게 "이 의존성은 npm 레지스트리가 아닌 로컬 워크스페이스에서 찾아라"고 지시한다.

프로토콜의미
workspace:*워크스페이스의 현재 버전을 그대로 사용
workspace:^1.0.0워크스페이스 버전이 ^1.0.0 범위 내인지 확인 후 사용
workspace:~1.0.0워크스페이스 버전이 ~1.0.0 범위 내인지 확인 후 사용

npm 워크스페이스에서는 workspace: 프로토콜 대신 일반 버전 범위를 사용해도 패키지 매니저가 자동으로 로컬 워크스페이스를 우선 매칭한다. pnpm과 yarn에서는 workspace: 프로토콜을 명시적으로 사용하는 것이 권장된다.

의존성 해석 흐름

패키지 매니저가 install 명령을 실행하면 다음 순서로 의존성을 해석한다.

  1. 루트의 워크스페이스 목록 파악: package.jsonworkspaces 또는 pnpm-workspace.yaml에서 워크스페이스 경로를 읽는다.
  2. 각 워크스페이스의 package.json 분석: 이름, 버전, 의존성 목록을 수집한다.
  3. 내부 의존성 매칭: 의존성 이름이 다른 워크스페이스의 이름과 일치하면 심볼릭 링크로 연결.
  4. 외부 의존성 설치: 내부에서 매칭되지 않는 의존성은 npm 레지스트리에서 다운로드하여 설치.
  5. 호이스팅 결정: 패키지 매니저의 정책에 따라 공통 의존성을 루트로 끌어올릴지 결정.

6. 워크스페이스 관리 시 주의사항

순환 의존성 방지

패키지 A가 B를 의존하고, B가 다시 A를 의존하는 순환 관계는 빌드 순서를 결정할 수 없게 만든다. 순환 의존성이 발생하면 해당 모듈을 분리하거나 공통 부분을 별도 패키지로 추출해야 한다.

버전 일관성 유지

여러 워크스페이스에서 같은 외부 패키지의 서로 다른 버전을 사용하면 번들 크기가 커지고 런타임 오류가 발생할 수 있다. react처럼 싱글턴이어야 하는 패키지는 특히 버전을 통일해야 한다.

워크스페이스별 명령 실행

각 패키지 매니저는 특정 워크스페이스에서 스크립트를 실행하는 방법을 제공한다.

패키지 매니저명령어 예시
npmnpm run build -w @my-org/ui-kit
yarnyarn workspace @my-org/ui-kit build
pnpmpnpm --filter @my-org/ui-kit build

-w, workspace, --filter 같은 옵션으로 특정 워크스페이스를 지정하거나, 전체 워크스페이스에 일괄 실행할 수도 있다.


7. 실제 프로젝트 디렉터리 구조 예시

마이크로프론트엔드 모노레포의 전형적인 디렉터리 구조:

my-microfrontend/
├── package.json              # 루트: workspaces 정의, 관리 도구
├── turbo.json                # Turborepo 설정 (또는 nx.json)
├── tsconfig.base.json        # 공통 TypeScript 설정
├── .eslintrc.js              # 공통 ESLint 설정
├── apps/
│   ├── shell/                # 호스트 앱 (마이크로프론트엔드 컨테이너)
│   │   ├── package.json
│   │   └── src/
│   ├── product/              # 마이크로 앱: 상품
│   │   ├── package.json
│   │   └── src/
│   └── checkout/             # 마이크로 앱: 결제
│       ├── package.json
│       └── src/
├── packages/
│   ├── ui-kit/               # 공통 UI 컴포넌트 라이브러리
│   │   ├── package.json
│   │   ├── src/
│   │   └── dist/
│   ├── utils/                # 공통 유틸리티
│   │   ├── package.json
│   │   ├── src/
│   │   └── dist/
│   ├── eslint-config/        # 공유 ESLint 설정 패키지
│   │   └── package.json
│   └── tsconfig/             # 공유 TypeScript 설정 패키지
│       └── package.json
└── node_modules/             # 호이스팅된 공통 의존성
    ├── react/
    ├── typescript/
    ├── @my-org/ui-kit -> ../../packages/ui-kit  (심볼릭 링크)
    └── @my-org/utils -> ../../packages/utils    (심볼릭 링크)

이 구조에서 apps/는 배포 가능한 앱들을, packages/는 내부에서 공유되는 라이브러리들을 담는다. 루트의 node_modules에는 호이스팅된 외부 의존성과 내부 패키지로의 심볼릭 링크가 함께 존재한다.


핵심 정리

  1. 워크스페이스는 패키지 매니저가 제공하는 모노레포 핵심 기능으로, 하나의 레포에서 여러 독립 패키지를 등록하고 연결하고 통합 설치한다.
  2. 심볼릭 링크가 내부 패키지 연결의 핵심이다. npm 퍼블리시 없이 로컬 패키지를 직접 참조하므로 수정 사항이 즉시 반영된다.
  3. 호이스팅은 공통 의존성을 루트로 끌어올려 디스크 공간을 절약하지만, 유령 의존성 문제를 유발할 수 있다. pnpm은 기본적으로 이를 방지한다.
  4. workspace:* 같은 프로토콜을 사용하여 내부 의존성임을 명시적으로 선언하는 것이 권장된다.
  5. 순환 의존성 방지, 버전 일관성 유지, 패키지 매니저별 명령어 차이를 이해하는 것이 실무에서 중요하다.

다음 단계

워크스페이스의 동작 원리를 이해했으므로, 다음으로 프론트엔드 프로젝트의 패키지 매니저별 구체적인 사용법과 실습을 진행한다.

다음: ../04-패키지-매니저/01-프론트엔드-프로젝트-구조.md