Skip to content

모노레포 내 패키지 분리 (Phase 1)

TypeScript path alias를 활용한 과도기 전략으로 모놀리식 코드베이스를 서비스 단위로 물리적 분리한다.

학습 목표

  1. 초기 모노레포의 "가짜 패키지 분리" 상태를 정확히 진단할 수 있다.
  2. 이상적인 모노레포 구조와 과도기 구조의 차이를 설명할 수 있다.
  3. TypeScript path alias를 활용한 점진적 코드 이동 전략을 이해한다.
  4. Application Shell 패턴과 라우터/프래그먼트 분리 방식을 설명할 수 있다.
  5. 의존성 정리와 패키지 계층 분리의 필요성을 파악한다.

본문

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을 통한 런타임 통합까지 살펴본다.

다음: 03-빌드타임에서-런타임으로 ->