Skip to content

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/utilitiesimportRemote로 동적 리모트 로딩을 구현하여 장애 격리를 달성할 수 있다

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/utilitiesimportRemote**를 사용하면 런타임에 URL을 결정하고, 서버가 복구되었을 때 자동으로 다시 로드할 수 있다.

bash
pnpm --filter @career-up/shell add @module-federation/utilities
typescript
// 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의 주요 파라미터:

파라미터설명예시
urlRemote 서버 기본 URLhttp://localhost:3001
scopeMF 앱 이름 (webpack name)posting
moduleexpose된 모듈 이름./injector
remoteEntryFileName엔트리 파일명remoteEntry.js

6. 전체 프래그먼트 아키텍처 종합


7. 실행 순서와 포트 매핑

커리어업 프로젝트 전체를 실행하려면 아래 서비스를 모두 기동해야 한다.

서비스포트역할
API Server4000json-server Mock API
Shell3000Host, 라우팅, 인증
Posting3001포스팅 + 프래그먼트 소비
Education3002교육 콘텐츠
Network3003인맥 관리
Job3004채용 공고 + 추천 채용 프래그먼트
Fragment Recommend Connections5001추천 1촌 프래그먼트
bash
# 전체 실행 (모노레포 루트에서)
pnpm dev

핵심 정리

  1. 소비자 측 webpack의 remotes에 프래그먼트 이름과 remoteEntry.js URL을 등록하면 import()로 접근할 수 있다
  2. tsconfig의 paths는 타입 체크 전용이며, remotes 키 + exposes 키가 결합된 경로를 실제 파일 위치로 매핑한다
  3. React.lazySuspense로 프래그먼트를 지연 로딩하고 로딩 폴백을 제공한다
  4. ErrorBoundary로 네트워크 에러나 런타임 에러를 격리하여 호스트 앱 전체가 깨지는 것을 방지한다
  5. importRemote는 런타임에 URL을 결정하므로, 서버가 복구되면 탭 전환만으로 자동 재로드가 가능하다
  6. 프래그먼트 아키텍처는 ErrorBoundary > Suspense > 프래그먼트 Container 순으로 래핑하는 것이 안전한 패턴이다

다음 단계