테마
Module Federation 프래그먼트 설정
소비자(포스팅 앱) 측에서 webpack.config.js에 프래그먼트를 remote로 등록하고, React.lazy와 Suspense로 프래그먼트를 로딩하며, 에러 처리와 폴백 UI를 구현한다. importRemote를 활용한 동적 리모트 로딩 패턴도 학습한다.
학습 목표
- 소비자 측 webpack.config.js에서 프래그먼트를 remote로 등록하는 방법을 이해한다
- tsconfig.json의 paths 설정으로 TypeScript가 remote 모듈을 인식하게 하는 방법을 익힌다
- React.lazy와 Suspense로 프래그먼트를 지연 로딩하고 폴백 UI를 제공할 수 있다
@module-federation/utilities의importRemote로 동적 리모트 로딩을 구현하여 장애 격리를 달성할 수 있다
1. 소비자 측 Webpack 설정
포스팅 앱이 두 프래그먼트를 remote로 등록한다.
javascript
// apps/posting/webpack.config.js
new ModuleFederationPlugin({
name: "posting",
filename: "remoteEntry.js",
remotes: {
// 프래그먼트 1: 별도 런타임 (추천 1촌)
fragment_recommend_connections:
"fragment_recommend_connections@http://localhost:5001/remoteEntry.js",
// 프래그먼트 2: 기존 앱 확장 (추천 채용 공고)
job: "job@http://localhost:3004/remoteEntry.js"
},
exposes: {
"./injector": "./src/injector.tsx"
},
shared: { /* ... */ }
})2. TypeScript paths 설정
TypeScript가 remote 모듈의 import 경로를 인식할 수 있도록 tsconfig.json에 paths를 등록한다.
json
// apps/posting/tsconfig.json
{
"compilerOptions": {
"paths": {
// 기존 마이크로앱 remote
"posting/injector": ["../posting/src/injector.tsx"],
// 프래그먼트 remote
"fragment_recommend_connections/container": [
"../../fragments/fragment-recommend-connections/src/containers/RecommendConnectionsContainer.tsx"
],
"job/fragment-recommend-jobs": [
"../job/src/fragments/RecommendJobsContainer.tsx"
]
}
}
}주의사항:
- webpack remotes의 키 이름(
fragment_recommend_connections)과 exposes의 키 이름(./container)이 결합되어 import 경로가 된다 - 실제 tsconfig paths는 타입 체크 용도이며, 런타임에서는 Module Federation이 처리한다
- 언더스코어(
_)와 대시(-)를 혼동하지 않도록 주의한다
3. React.lazy로 프래그먼트 로딩
typescript
// apps/posting/src/pages/PageHome.tsx
import React, { Suspense } from "react";
import styles from "./PageHome.module.css";
// 프래그먼트를 React.lazy로 지연 로딩
const RecommendConnectionsContainer = React.lazy(
() => import("fragment_recommend_connections/container")
);
const RecommendJobsContainer = React.lazy(
() => import("job/fragment-recommend-jobs")
);
const PageHome: React.FC = () => {
// ... 기존 포스팅 로직
return (
<div className={styles.wrapper}>
<div className={styles.mainContent}>
{/* 기존 포스팅 콘텐츠 */}
</div>
<div className={styles.sidebar}>
{/* 프래그먼트 1: 추천 1촌 */}
<Suspense fallback={<div>로딩 중...</div>}>
<RecommendConnectionsContainer />
</Suspense>
{/* 프래그먼트 2: 추천 채용 공고 */}
<Suspense fallback={<div>로딩 중...</div>}>
<RecommendJobsContainer />
</Suspense>
</div>
</div>
);
};
export default PageHome;4. 에러 처리와 폴백 UI
프래그먼트 서버가 다운되거나 네트워크 장애 시, 전체 포스팅 앱이 깨지지 않도록 ErrorBoundary로 감싼다.
typescript
// apps/posting/src/components/FragmentErrorBoundary.tsx
import React, { Component, type ReactNode } from "react";
interface Props {
fallback: ReactNode;
children: ReactNode;
}
interface State {
hasError: boolean;
}
class FragmentErrorBoundary extends Component<Props, State> {
state: State = { hasError: false };
static getDerivedStateFromError(): State {
return { hasError: true };
}
componentDidCatch(error: Error) {
console.error("프래그먼트 로딩 실패:", error);
}
render() {
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}
}
export default FragmentErrorBoundary;typescript
// 사용 예시
<FragmentErrorBoundary fallback={<div>추천 1촌을 불러올 수 없습니다.</div>}>
<Suspense fallback={<div>로딩 중...</div>}>
<RecommendConnectionsContainer />
</Suspense>
</FragmentErrorBoundary>Suspense vs ErrorBoundary 역할 분담:
| 상황 | 처리 주체 | 표시 내용 |
|---|---|---|
| 청크 다운로드 중 | Suspense | "로딩 중..." 폴백 |
| 네트워크 에러 / 서버 다운 | ErrorBoundary | "불러올 수 없습니다" 폴백 |
| 프래그먼트 내부 런타임 에러 | ErrorBoundary | "불러올 수 없습니다" 폴백 |
5. 동적 리모트 로딩 (importRemote)
정적 remote 설정은 Shell 빌드 시점에 URL이 고정된다. **@module-federation/utilities의 importRemote**를 사용하면 런타임에 URL을 결정하고, 서버가 복구되었을 때 자동으로 다시 로드할 수 있다.
bash
pnpm --filter @career-up/shell add @module-federation/utilitiestypescript
// apps/shell/src/components/AppPosting.tsx
import React, { useEffect, useRef } from "react";
import { importRemote } from "@module-federation/utilities";
import type { InjectFunctionType } from "@career-up/shell-router";
const AppPosting: React.FC = () => {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
importRemote<{ default: InjectFunctionType }>({
url: "http://localhost:3001",
scope: "posting",
module: "./injector",
remoteEntryFileName: "remoteEntry.js"
})
.then((module) => {
const inject = module.default;
inject({
routerType: "memory",
rootElement: containerRef.current!
});
})
.catch((error) => {
console.error("포스팅 마이크로앱 로드 실패:", error);
});
}, []);
return <div ref={containerRef} id="app-posting" />;
};
export default AppPosting;importRemote의 주요 파라미터:
| 파라미터 | 설명 | 예시 |
|---|---|---|
url | Remote 서버 기본 URL | http://localhost:3001 |
scope | MF 앱 이름 (webpack name) | posting |
module | expose된 모듈 이름 | ./injector |
remoteEntryFileName | 엔트리 파일명 | remoteEntry.js |
6. 전체 프래그먼트 아키텍처 종합
7. 실행 순서와 포트 매핑
커리어업 프로젝트 전체를 실행하려면 아래 서비스를 모두 기동해야 한다.
| 서비스 | 포트 | 역할 |
|---|---|---|
| API Server | 4000 | json-server Mock API |
| Shell | 3000 | Host, 라우팅, 인증 |
| Posting | 3001 | 포스팅 + 프래그먼트 소비 |
| Education | 3002 | 교육 콘텐츠 |
| Network | 3003 | 인맥 관리 |
| Job | 3004 | 채용 공고 + 추천 채용 프래그먼트 |
| Fragment Recommend Connections | 5001 | 추천 1촌 프래그먼트 |
bash
# 전체 실행 (모노레포 루트에서)
pnpm dev핵심 정리
- 소비자 측 webpack의
remotes에 프래그먼트 이름과remoteEntry.jsURL을 등록하면import()로 접근할 수 있다 - tsconfig의
paths는 타입 체크 전용이며,remotes키 +exposes키가 결합된 경로를 실제 파일 위치로 매핑한다 React.lazy와Suspense로 프래그먼트를 지연 로딩하고 로딩 폴백을 제공한다ErrorBoundary로 네트워크 에러나 런타임 에러를 격리하여 호스트 앱 전체가 깨지는 것을 방지한다importRemote는 런타임에 URL을 결정하므로, 서버가 복구되면 탭 전환만으로 자동 재로드가 가능하다- 프래그먼트 아키텍처는
ErrorBoundary>Suspense>프래그먼트 Container순으로 래핑하는 것이 안전한 패턴이다
다음 단계
- ../14-배포와-CI-CD/01-CI-CD-파이프라인-구축.md: 모듈 페더레이션 기반 마이크로프론트엔드의 독립 배포 파이프라인을 구축하고, 특정 마이크로앱만 변경 시 해당 앱만 빌드/배포하는 전략을 학습한다