테마
워크스페이스 관리
모노레포에서 여러 패키지를 하나의 레포에서 관리하는 워크스페이스의 동작 원리(심볼릭 링크, 호이스팅)와 설정 방법, 패키지 간 의존성 선언과 해석 흐름을 이해한다.
학습 목표
- 워크스페이스의 개념과 패키지 매니저별 설정 방법을 이해한다.
- 심볼릭 링크를 통한 내부 패키지 연결 원리를 파악한다.
- 호이스팅(hoisting)의 개념과 동작 방식, 주의점을 이해한다.
- 패키지 간 의존성을 선언하고 해석하는 흐름을 설명할 수 있다.
- 워크스페이스 관리 시 자주 발생하는 문제와 해결 방법을 파악한다.
본문
1. 워크스페이스란?
워크스페이스는 하나의 Git 저장소 안에서 여러 독립적인 패키지를 관리할 수 있게 해주는 패키지 매니저의 기능이다.
워크스페이스가 없던 시절에는 하나의 레포에 여러 프로젝트가 있어도, 각 프로젝트의 의존성을 개별적으로 설치하고 연결해야 했다. 워크스페이스 기능을 사용하면 패키지 매니저가 자동으로 내부 패키지를 감지하고 연결해 준다.
워크스페이스의 핵심 역할:
| 역할 | 설명 |
|---|---|
| 패키지 등록 | 루트 설정에서 어떤 디렉터리가 워크스페이스인지 선언 |
| 내부 패키지 연결 | 내부 의존성을 npm 레지스트리가 아닌 로컬 경로로 연결 |
| 의존성 통합 설치 | 루트에서 한 번의 install 명령으로 모든 워크스페이스의 의존성을 설치 |
| 명령어 위임 | 특정 워크스페이스에 대해 스크립트를 실행하는 명령어 제공 |
2. 패키지 매니저별 워크스페이스 설정
세 가지 주요 패키지 매니저(npm, yarn, pnpm) 모두 워크스페이스를 지원하지만, 설정 방식이 약간 다르다.
npm / yarn
package.json에 workspaces 필드를 추가한다.
json
{
"name": "my-monorepo",
"private": true,
"workspaces": [
"apps/*",
"packages/*"
]
}apps/*는 apps/ 디렉터리 아래의 모든 하위 디렉터리를 워크스페이스로 등록한다는 의미다. 각 하위 디렉터리에는 반드시 자체 package.json이 있어야 한다.
pnpm
별도의 pnpm-workspace.yaml 파일을 사용한다.
yaml
packages:
- "apps/*"
- "packages/*"pnpm은 package.json의 workspaces 필드를 무시하고, 이 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_modules에 react가 3번 설치된다. 호이스팅을 적용하면 루트 node_modules에 1번만 설치하고, Node.js의 모듈 해석 알고리즘에 의해 각 워크스페이스에서 접근할 수 있다.
Node.js 모듈 해석과 호이스팅
Node.js는 require() 또는 import를 만나면 현재 디렉터리의 node_modules부터 시작하여 상위 디렉터리로 올라가며 모듈을 탐색한다. 이 원리 덕분에 루트에 설치된 패키지를 하위 워크스페이스에서 사용할 수 있다.
탐색 순서 예시 (apps/web-app/src/index.ts에서 react를 import할 때):
apps/web-app/src/node_modules/react-> 없음apps/web-app/node_modules/react-> 없음apps/node_modules/react-> 없음(루트)/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는 lodash를 package.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 명령을 실행하면 다음 순서로 의존성을 해석한다.
- 루트의 워크스페이스 목록 파악:
package.json의workspaces또는pnpm-workspace.yaml에서 워크스페이스 경로를 읽는다. - 각 워크스페이스의
package.json분석: 이름, 버전, 의존성 목록을 수집한다. - 내부 의존성 매칭: 의존성 이름이 다른 워크스페이스의 이름과 일치하면 심볼릭 링크로 연결.
- 외부 의존성 설치: 내부에서 매칭되지 않는 의존성은 npm 레지스트리에서 다운로드하여 설치.
- 호이스팅 결정: 패키지 매니저의 정책에 따라 공통 의존성을 루트로 끌어올릴지 결정.
6. 워크스페이스 관리 시 주의사항
순환 의존성 방지
패키지 A가 B를 의존하고, B가 다시 A를 의존하는 순환 관계는 빌드 순서를 결정할 수 없게 만든다. 순환 의존성이 발생하면 해당 모듈을 분리하거나 공통 부분을 별도 패키지로 추출해야 한다.
버전 일관성 유지
여러 워크스페이스에서 같은 외부 패키지의 서로 다른 버전을 사용하면 번들 크기가 커지고 런타임 오류가 발생할 수 있다. react처럼 싱글턴이어야 하는 패키지는 특히 버전을 통일해야 한다.
워크스페이스별 명령 실행
각 패키지 매니저는 특정 워크스페이스에서 스크립트를 실행하는 방법을 제공한다.
| 패키지 매니저 | 명령어 예시 |
|---|---|
| npm | npm run build -w @my-org/ui-kit |
| yarn | yarn workspace @my-org/ui-kit build |
| pnpm | pnpm --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에는 호이스팅된 외부 의존성과 내부 패키지로의 심볼릭 링크가 함께 존재한다.
핵심 정리
- 워크스페이스는 패키지 매니저가 제공하는 모노레포 핵심 기능으로, 하나의 레포에서 여러 독립 패키지를 등록하고 연결하고 통합 설치한다.
- 심볼릭 링크가 내부 패키지 연결의 핵심이다. npm 퍼블리시 없이 로컬 패키지를 직접 참조하므로 수정 사항이 즉시 반영된다.
- 호이스팅은 공통 의존성을 루트로 끌어올려 디스크 공간을 절약하지만, 유령 의존성 문제를 유발할 수 있다. pnpm은 기본적으로 이를 방지한다.
workspace:*같은 프로토콜을 사용하여 내부 의존성임을 명시적으로 선언하는 것이 권장된다.- 순환 의존성 방지, 버전 일관성 유지, 패키지 매니저별 명령어 차이를 이해하는 것이 실무에서 중요하다.
다음 단계
워크스페이스의 동작 원리를 이해했으므로, 다음으로 프론트엔드 프로젝트의 패키지 매니저별 구체적인 사용법과 실습을 진행한다.