테마
빌드타임 공유에서 런타임 공유로 (Phase 2~3)
패키지 매니저 변경과 NX 도입을 거쳐, 최종적으로 Module Federation 런타임 통합과 독립 배포 환경을 구축한다.
학습 목표
- 빌드타임 코드 공유에서 패키지 빌드 후 공유로 전환하는 이유를 이해한다.
- Phantom Dependency 제거를 위한 패키지 매니저 변경 과정을 파악한다.
- NX를 활용한 빌드 오케스트레이션의 필요성을 설명할 수 있다.
- Webpack Module Federation을 통한 런타임 통합 메커니즘을 이해한다.
- 마이크로 앱의 독립 개발 환경 구축과 배포 파이프라인을 설명할 수 있다.
본문
1. Phase 2: 빌드타임 공유에서 패키지 빌드 후 공유로
Phase 1에서 코드를 분리하고 의존성을 정리했지만, 여전히 모든 패키지는 소스코드 수준에서 직접 참조되고 있었다. 이제 인프라성 패키지를 실제로 빌드하여 결과물(dist/)을 통해 공유하는 단계로 넘어간다.
패키지가 늘어난 시점의 구조
코드 분리와 의존성 정리를 거치면서 패키지가 크게 늘어났다.
| 패키지 | 역할 | 레이어 |
|---|---|---|
| UIKit | 디자인 시스템 컴포넌트 라이브러리 (Emotion 기반) | Core |
| API | API 호출 유틸리티, HTTP Client | Core |
| Shared Core | 전체 서비스 공통 유틸리티 | Core |
| Main Service (Shell) | Application Shell, 라우팅 허브 | App |
| Drive, Mail, Task, ... | 각 도메인 서비스 앱 | App |
이 인프라 패키지들은 적절한 인터페이스만 열고 내부적으로 유지보수할 수 있도록 패키지 빌드 및 설정이 필요했다.
패키지 빌드 전환을 위해 필요한 세 가지 작업
2. 패키지 매니저 변경: Yarn Classic -> pnpm
변경 여정
Phantom Dependency를 해결하기 위해 패키지 매니저를 변경해야 했다.
| 단계 | 패키지 매니저 | 경험 |
|---|---|---|
| 초기 | Yarn Classic | Phantom Dependency 문제 존재 |
| 1차 변경 | Yarn Berry (PnP) | 저장소 무거움, 클론 느림, 비표준 모듈 해석 |
| 최종 변경 | pnpm | 표준 node_modules 흐름 + Phantom Dependency 해결 |
Yarn Berry의 Plug'n'Play(PnP)는 node_modules를 사용하지 않고 자체 모듈 리졸버를 사용하기 때문에, 많은 도구에서 비표준적인 설정을 추가해야 했다. 또한 워크스페이스와 패키지가 많아지면서 저장소가 무거워져 클론 시간이 크게 늘었다.
pnpm으로 최종 변경한 뒤의 이점:
node_modules의 자연스러운 흐름을 유지- 심볼릭 링크 기반으로 Phantom Dependency를 원천 차단
- 각 패키지의
package.json에 실제 사용하는 의존성만 정확하게 명시 가능
전환 후에는 각 패키지가 실제로 사용하는 의존성만
package.json에 남기고, 불필요한 의존성을 제거하는 대규모 정리 작업을 수행했다.
3. NX 도입: 빌드 오케스트레이션
패키지가 늘어나면서 빌드 순서를 사람이 직접 관리하기 어려워졌다.
예를 들어:
- Shared Core를 먼저 빌드해야 API 패키지가 빌드 가능
- API 패키지가 빌드되어야 Drive, Mail 등이 빌드 가능
- UIKit도 먼저 빌드되어야 App 레이어가 사용 가능
NX를 도입하여 다음 기능을 활용했다:
- 태스크 그래프: 패키지 간 의존 관계를 분석하여 빌드 순서를 자동 결정
- 병렬 실행: 독립적인 패키지는 동시에 빌드
- 캐싱: 변경되지 않은 패키지는 빌드를 건너뛰고 캐시된 결과물 사용
빌드 도구 분리
| 대상 | 빌드 도구 | 이유 |
|---|---|---|
| Core 패키지 (UIKit, API, Shared Core) | Vite | 라이브러리 빌드에 적합 |
| App 패키지 (Shell, Drive, Mail 등) | Webpack | Module Federation 플러그인 지원 |
기존에는 모든 것이 동일한 빌드 도구를 사용했지만, Module Federation은 Webpack 기반이므로 App 패키지는 Webpack으로 전환했다. Core 패키지는 Vite로 빌드하고, 그 결과물을 Webpack이 소비하는 구조를 만들었다.
이 과정에서 Vite 빌드 결과물이 Webpack에서 정상 동작하는지 지속적으로 테스트하며 점진적으로 적용해야 했다.
4. Phase 3: 런타임 모듈 페더레이션 전환
Phase 2까지는 빌드타임에 패키지를 공유했다. Phase 3에서는 런타임에 코드를 공유하여 각 서비스를 독립적으로 빌드하고 배포할 수 있도록 전환한다.
Module Federation 설정
기존에 TypeScript path alias로 참조하던 코드를 Module Federation의 remote/expose로 교체한다.
javascript
// Shell(Host) - webpack.config.js
new ModuleFederationPlugin({
name: 'shell',
remotes: {
drive: 'drive@/drive/remoteEntry.js',
mail: 'mail@/mail/remoteEntry.js',
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
},
});
// Drive(Remote) - webpack.config.js
new ModuleFederationPlugin({
name: 'drive',
filename: 'remoteEntry.js',
exposes: {
'./DriveRouter': './src/shared/DriveRouter',
'./DriveAttachButton': './src/shared/DriveAttachButton',
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
},
});핵심은 소스코드 자체에는 변경이 거의 없다는 점이다. Phase 1에서 TS path alias로 분리하고 lazy import를 적용해 두었기 때문에, Webpack 설정만 변경하면 런타임 통합으로 자연스럽게 전환된다.
5. 독립 개발 환경 구축
각 마이크로 앱이 독립적으로 개발할 수 있는 환경을 구축한다.
로컬 개발 환경 전략
| 시나리오 | 방법 |
|---|---|
| Drive 서비스 개발 | Shell + Drive 두 개의 dev server 실행 |
| 다른 서비스의 프래그먼트가 필요 없는 경우 | 에러 바운더리로 대체, 해당 서비스 띄우지 않음 |
| 다른 서비스의 프래그먼트가 필요한 경우 | 배포된 개발/프로덕션 서버의 remoteEntry.js를 참조 |
Drive 서비스를 개발할 때 Shell과 Drive만 로컬에서 실행하면 된다. 다른 서비스(Mail, Task 등)의 프래그먼트는 에러 바운더리로 감싸져 있으므로, 해당 서비스를 띄우지 않아도 Drive 개발에는 문제가 없다.
배포 전략
독립된 도메인이 아니라 동일한 Nginx 서버 내 하위 경로로 배포한다.
dooray.com/ -> Shell 정적 파일
dooray.com/drive/ -> Drive 정적 파일 (별도 디렉토리)
dooray.com/mail/ -> Mail 정적 파일 (별도 디렉토리)이 방식의 장점:
- 인증/세션 공유에 추가 작업이 필요 없음
- CORS 등 보안 이슈가 발생하지 않음
- 기존 인프라를 그대로 활용 가능
6. 전환 과정 요약: 각 Phase별 빌드/공유 방식 비교
| 구분 | Phase 1 | Phase 2 | Phase 3 |
|---|---|---|---|
| 코드 공유 방식 | TS path alias (소스 직접 참조) | 패키지 빌드 후 dist/ 참조 | Module Federation (런타임 로드) |
| 빌드 주체 | Main Service만 빌드 | Core 패키지 빌드 + App 빌드 | 각 서비스 독립 빌드 |
| 개발 서버 | Main Service 하나 | Main Service 하나 | Shell + 개별 서비스 서버 |
| 배포 단위 | 전체 SPA 하나 | 전체 SPA 하나 | 서비스별 독립 배포 |
| 패키지 매니저 | Yarn Classic | pnpm | pnpm |
| 빌드 도구 | Vite (전체) | Vite (Core) + Webpack (App) | Vite (Core) + Webpack (App) |
| 오케스트레이션 | 없음 | NX | NX |
7. 점진적 전환의 원칙
각 Phase를 넘어갈 때 다음 원칙을 지켰다:
- 소스코드 변경 최소화: 도구와 설정 변경이 주이며, 비즈니스 로직은 건드리지 않음
- 점진적 리스크 관리: 한 서비스(Drive)부터 전환하고, 안정화 후 다음 서비스(Mail) 진행
- 롤백 가능성 확보: 문제가 생기면 이전 단계로 돌아갈 수 있는 상태 유지
- 지속적 검증: 전환 후 반드시 전체 서비스가 정상 동작하는지 확인
핵심 정리
| 항목 | 내용 |
|---|---|
| Phase 2 목표 | 소스코드 직접 참조에서 패키지 빌드 결과물 공유로 전환 |
| 패키지 매니저 | Yarn Classic -> Yarn Berry -> pnpm (최종) |
| 오케스트레이션 | NX 도입 (빌드 순서 자동 결정, 캐싱) |
| 빌드 도구 분리 | Core(Vite) + App(Webpack) |
| Phase 3 목표 | Module Federation으로 런타임 통합, 독립 배포 |
| 배포 전략 | 동일 도메인 내 하위 경로 분리 (Nginx) |
| 전환 순서 | Drive 먼저 전환 -> 안정화 -> Mail 전환 -> 순차 확대 |
| 핵심 원칙 | 소스코드 변경 최소화, 점진적 리스크 관리 |
기억할 포인트:
- Phase 1에서 TS path alias + lazy import로 미리 준비해 두었기 때문에, Module Federation 전환 시 소스코드 변경이 최소화되었다.
- 패키지 매니저는 한 번에 정답을 찾지 못할 수 있다 (Yarn Berry -> pnpm 재전환).
- 독립 배포가 가능해지면서 각 서비스 팀이 자체 QA/배포 파이프라인을 갖게 되었다.
다음 단계
다음 문서에서는 이 프로젝트를 돌아보며 교훈과 회고를 정리한다. Redux를 필수 기술로 지정한 것에 대한 후회, 프래그먼트와 라우팅 분리의 필요성, CSS-in-JS 선택에 대한 고찰 등 실전 경험에서 얻은 통찰을 다룬다.