테마
모노레포 내 패키지 분리 (Phase 1)
TypeScript path alias를 활용한 과도기 전략으로 모놀리식 코드베이스를 서비스 단위로 물리적 분리한다.
학습 목표
- 초기 모노레포의 "가짜 패키지 분리" 상태를 정확히 진단할 수 있다.
- 이상적인 모노레포 구조와 과도기 구조의 차이를 설명할 수 있다.
- TypeScript path alias를 활용한 점진적 코드 이동 전략을 이해한다.
- Application Shell 패턴과 라우터/프래그먼트 분리 방식을 설명할 수 있다.
- 의존성 정리와 패키지 계층 분리의 필요성을 파악한다.
본문
1. 초기 모노레포 상태 진단
리뉴얼 재개 시점의 모노레포는 형태만 모노레포였지, 실질적으로는 단일 프로젝트와 다름없었다.
| 항목 | 실제 상태 |
|---|---|
| 패키지 수 | 2개 (UIKit, Main Service) |
| UIKit 빌드 여부 | 빌드 없음 (소스코드 직접 참조) |
| 패키지 인터페이스 | 없음 (TS path alias로 소스 직접 import) |
| 의존성 관리 | Phantom Dependency에 의존 |
| 서비스 분리 | Main Service 내 src/modules/{서비스명} 디렉토리로만 구분 |
| 스토어 분리 | Ducks 패턴으로 모듈화했으나 하나의 Redux Store |
"잘 정의된 관계를 가진 여러 개의 개별 프로젝트가 포함된 단일 레포지토리"라는 모노레포의 정의에 비추어 보면, 소스코드가 분리되어 있을 뿐 마구 가져다 쓰는 형태에 불과했다.
2. 이상적인 구조 vs 과도기 구조
바로 이상적인 형태로 가기에는 어려움이 있었다. 각 마이크로 앱이 독립 빌드되고 개발 서버를 띄울 수 있는 상태가 되려면 먼저 코드가 물리적으로 분리되어 있어야 했다.
| 구분 | 이상적 구조 | 과도기 구조 (Phase 1) |
|---|---|---|
| UIKit | 빌드 후 dist/ 인터페이스 제공 | 기존 그대로 (소스 참조) |
| 서비스 앱 | 독립 빌드, 독립 개발 서버 | TS path alias로 소스 참조 |
| 개발 서버 | 서비스별 개별 실행 | Main Service에서만 실행 |
| 의존성 | 패키지별 명확한 경계 | 아직 의존성 경계 불명확 |
과도기 전략의 핵심: 코드의 물리적 이동
Main Service 내부의 src/modules/drive/ 코드를 apps/drive/src/로 이동한다. Redux 모듈도 함께 이동한다. 그리고 tsconfig.json의 path alias를 새 위치로 변경한다.
// 변경 전
"paths": { "@drive/*": ["./src/modules/drive/*"] }
// 변경 후
"paths": { "@drive/*": ["../apps/drive/src/*"] }이 과정에서 TypeScript가 결정적 역할을 한다. import 경로가 잘못되면 즉시 컴파일 에러가 발생하므로, 코드 이동 후 문제를 빠르게 발견하고 수정할 수 있다.
3. Application Shell 패턴의 적용
코드를 물리적으로 분리한 뒤, Main Service를 점진적으로 Application Shell로 전환해야 했다.
Shell의 역할
- 모든 하위 서비스의 상위 애플리케이션 역할
- 들어오는 요청을 라우팅에 맞게 하위 서비스에 연결
- 공통 레이아웃 제공 (Dooray는 서비스 간 UI가 유사)
- 인증, 앱 전체 설정에만 관여
- 서비스의 비즈니스 로직을 최대한 포함하지 않음
라우터 분리
각 서비스는 Shell에 제공할 라우터를 별도로 export한다. Shell은 특정 URL path 이하를 해당 서비스의 라우터로 위임한다.
typescript
// apps/drive/src/shared/DriveRouter.tsx
export const DriveRouter = React.lazy(
() => import('@drive/routes')
);
// apps/main-service/src/shell/routes.tsx
<Route path="/drive/*" element={<DriveRouter />} />이렇게 분리하면 나중에 @drive/routes 부분만 Module Federation의 remote/expose로 교체하면 된다.
프래그먼트(Fragment) 분리
서비스 간 통합이 필요한 경우, 예를 들어 메일 작성 화면에서 드라이브 첨부 기능을 사용하려면, 드라이브 서비스가 프래그먼트(작은 컴포넌트 조각)를 제공해야 한다.
typescript
// apps/drive/src/shared/DriveAttachButton.tsx
export const DriveAttachButton = React.lazy(
() => import('@drive/fragments/attach-button')
);
// apps/mail/src/compose/ComposeToolbar.tsx
import { DriveAttachButton } from '@drive/shared/DriveAttachButton';프래그먼트는 다음 원칙을 따라야 한다:
- 불필요한 비즈니스 로직을 숨긴다
- 가급적 순수한 React API만 사용한다
- 에러 바운더리로 감싸서 실패 시에도 전체 서비스에 영향이 없도록 한다
4. 의존성 정리와 패키지 계층 분리
단순 코드 분할만으로는 부족하다. 분리된 패키지가 서로의 내부 코드를 자유롭게 참조하면, 나중에 런타임 통합으로 전환할 때 분리가 불가능해진다.
의존성 유형별 처리 전략
인프라성 코드 vs 비즈니스 코드
| 코드 유형 | 처리 방법 | 예시 |
|---|---|---|
| 인프라성 코드 | 별도 패키지로 분리하여 공유 | HttpClient, 로깅, 인증 유틸 |
| 비즈니스 코드 | 중복을 허용하여 각자 소유 | 날짜 포매터, 도메인 특화 유틸 |
인프라성 코드는 Main Service 하위에 두지 않고, 상위 레벨 패키지로 분리하여 모든 서비스가 동등하게 의존한다.
비즈니스 코드의 경우, 중복이 비효율적으로 보이더라도 각 서비스가 독립적으로 진화할 수 있도록 허용하는 것이 더 나은 경우가 많다. 서비스마다 요구사항이 다르게 변화하면 공통 코드를 억지로 유지하는 것이 오히려 더 큰 관리 비용을 만든다.
TypeScript의 역할
이 모든 코드 이동과 의존성 정리 과정에서 TypeScript는 핵심적인 안전장치 역할을 했다.
- path alias 변경 시 import 오류를 즉시 감지
- 인터페이스 변경 시 사용처에서 타입 에러 발생
- 코드 이동 후 참조 관계가 올바른지 컴파일 타임에 검증
- Phantom Dependency 문제 발견에도 도움
"TypeScript가 없었다면 이런 규모의 코드 이동을 안전하게 수행하지 못했을 것이다."
5. 공유 코드의 인터페이스 설계
각 서비스가 Shell 및 다른 서비스에 제공하는 코드는 명확한 인터페이스를 가져야 한다.
| 공유 대상 | 제공 형태 | 설명 |
|---|---|---|
| Shell에 제공 | Router (라우터) | 특정 URL path 이하를 담당하는 라우트 컴포넌트 |
| 타 서비스에 제공 | Fragment (프래그먼트) | 다른 서비스 화면에 삽입되는 작은 UI 조각 |
| 공유하지 않음 | 나머지 내부 코드 | 서비스의 비즈니스 로직, 내부 컴포넌트 등 |
이 인터페이스 경계를 명확히 하는 것이 나중에 Module Federation 전환을 수월하게 만드는 핵심이다.
핵심 정리
| 항목 | 내용 |
|---|---|
| Phase 1 목표 | 모놀리식 코드를 서비스별 워크스페이스로 물리적 분리 |
| 과도기 전략 | TS path alias로 소스코드 참조 유지하면서 점진적 이동 |
| Shell 패턴 | Main Service를 Application Shell로 전환 (라우팅 + 공통 레이아웃) |
| 프래그먼트 | 서비스 간 통합에 사용하는 작은 UI 조각 |
| 의존성 원칙 | 인프라 코드는 공유, 비즈니스 코드는 중복 허용 |
| 패키지 계층 | Core Layer(공유) + App Layer(서비스별) |
| TypeScript | 코드 이동의 안전장치 (컴파일 타임 참조 검증) |
기억할 포인트:
- "패키지 분리"는 코드 이동만이 아니라, 의존성 방향과 인터페이스 경계를 정하는 과정이다.
- 라우터와 프래그먼트는 나중에 분리해야 더 느슨한 결합을 달성할 수 있다 (04장에서 후술).
- App 레이어 간 직접 참조를 금지하는 것이 독립 배포의 전제 조건이다.
다음 단계
다음 문서에서는 빌드타임 공유에서 런타임 공유로의 전환 과정을 다룬다. 패키지 매니저 변경(Yarn Classic -> pnpm), NX 도입, 빌드 설정 변경, 그리고 최종적으로 Webpack Module Federation을 통한 런타임 통합까지 살펴본다.