onschan.me
테마 변경

Module Federation 이해하기

2025-10-25

💡 Module Federation의 핵심 개념, 동작 원리, 그리고 주요 기술적 특징들을 분석합니다.

마이크로프론트엔드 아키텍처를 고민하다 보면 자연스럽게 Module Federation과 만나게 됩니다. 실무에서 사용하면서 느낀 점은, 이 기술이 단순히 코드를 분리하는 것 이상의 가치를 제공한다는 것입니다.

본 글에서는 Module Federation의 핵심 개념부터 실제 동작 원리, 그리고 현실적인 제약사항까지 깊이 있게 다뤄보겠습니다.


Module Federation이란 무엇인가

Module Federation의 'Federation'은 "연합"이라는 뜻으로, 하나의 큰 앱을 여러 모듈로 나누고 런타임에 통합하는 기술입니다.

이 개념은 Apollo GraphQL의 Federation에서 영감을 받았습니다. Apollo GraphQL Federation이 여러 GraphQL 마이크로서비스의 스키마를 조합하여 하나의 큰 GraphQL 스키마로 만드는 방식인 것처럼, Module Federation은 여러 JavaScript 애플리케이션을 런타임에 하나의 통합된 애플리케이션으로 결합합니다.

기존 방식과의 차이점

기존의 모놀리스 방식:

[모든 코드] → [하나의 번들] → [브라우저]

Module Federation:

[앱 A] ←→ [네트워크] ←→ [앱 B]
   ↓                     ↓
[번들 A]               [번들 B]
   ↓                     ↓
    [브라우저 런타임에서 통합]

빌드 시점에는 독립적으로 분리되어 있지만, 브라우저에서 실행될 때는 하나의 애플리케이션처럼 동작합니다.


핵심 개념 이해하기

기본적인 정의를 이해했다면, 이제 Module Federation을 실제로 다루기 위해 필요한 개념들을 살펴봐야 합니다.

전방향적 참조: 독특한 특징

Module Federation의 가장 독특한 점은 전방향적(omnidirectional) 참조가 가능하다는 것입니다. 일반적인 마이크로프론트엔드 패턴과 달리, 앱들이 서로를 참조할 수 있습니다.

// 하나의 앱이 동시에 호스트와 리모트 역할을 수행
new ModuleFederationPlugin({
  name: 'shopping-app',
  // 다른 앱의 컴포넌트를 사용 (호스트 역할)
  remotes: {
    'user-service': 'userService@http://localhost:3001/remoteEntry.js',
  },
  // 자신의 컴포넌트를 제공 (리모트 역할)
  exposes: {
    './ProductList': './src/components/ProductList',
    './ShoppingCart': './src/components/ShoppingCart',
  },
})

이는 remotesexposes 속성이 배타적이지 않기 때문입니다. 순환 참조도 가능하며, 이는 모듈 로드와 평가가 분리된 독특한 구조 덕분입니다.

용어 정리

Module Federation의 핵심 용어들:

  • 로컬 모듈: 현재 빌드에 포함된 일반적인 모듈
  • 원격 모듈: 다른 빌드에서 네트워크를 통해 가져오는 모듈
  • 컨테이너: 다른 앱에서 로드 가능한 단위, 노출된 모듈을 포함
  • 컨테이너 레퍼런스: 다른 앱을 import할 때 만들어지는 참조 관계
  • Share Scope: 공유 의존성이 유효한 범위

동작 원리 깊이 파보기

Module Federation이 브라우저에서 실제로 어떻게 동작하는지 알아야 문제가 생겼을 때 원인을 파악할 수 있습니다.

모듈 로드와 평가의 분리

Module Federation이 순환 참조를 해결할 수 있는 핵심은 모듈 접근을 두 단계로 분리한다는 점입니다:

  1. 모듈 로드 (비동기): 청크 로드 중 실행
  2. 모듈 평가 (동기): 모듈의 코드를 실행하고 결과를 생성
// 이런 import 구문이
const RemoteComponent = lazy(() => import('remote-app/Component'));

// 실제로는 이렇게 두 단계로 처리됨
// 1단계: 원격 모듈 로딩 (비동기)
const modulePromise = loadRemoteModule('remote-app/Component');
// 2단계: 모듈 실행 (동기)
const Component = await modulePromise.then(factory => factory());

이 분리 덕분에 복잡한 의존성 관계도 안전하게 처리할 수 있습니다.

런타임 청크의 역할

main.js

  • 최초 앱 초기화를 담당
  • 가장 상위의 마이크로앱 HTML에서 가장 먼저 불러와지는 청크
  • 브라우저에서 앱을 최초로 로드하는데 필요한 코드를 포함

remoteEntry.js

  • 리모트 앱을 초기화하는 청크
  • 특정 마이크로앱에서 다른 마이크로앱을 import할 때 가장 먼저 호출되는 청크
  • 원격 모듈에 접근할 수 있는 인터페이스 제공
// remoteEntry.js 내부 구조 예시
window.microApp = {
  get: (moduleName) => {
    // 요청된 모듈을 동적으로 반환
    return import(`./exposed-modules/${moduleName}`);
  },
  init: (sharedScope) => {
    // 공유 의존성 초기화
    Object.assign(__webpack_share_scopes__.default, sharedScope);
  }
};

호스트 앱 청크의 동작

실제 원격 모듈을 로드할 때의 코드는 다음과 같습니다:

// 호스트 앱 청크에 포함된 원격 모듈 로딩 코드
module.exports = new Promise((resolve, reject) => {
  if(typeof microApp !== "undefined") return resolve();

  __webpack_require__.l("http://localhost:3000/remoteEntry.js", (event) => {
    if(typeof microApp !== "undefined") return resolve();
    // 에러 처리 로직...
  }, "microApp");
}).then(() => (microApp));

공유 의존성

Module Federation을 실무에서 사용하다 보면 필연적으로 마주하게 되는 문제가 있습니다. 바로 여러 앱이 동일한 라이브러리를 사용할 때 이를 어떻게 관리할 것인가 하는 의존성 문제입니다.

이 문제의 해답이 바로 Share Scope입니다.

Share Scope의 정확한 이해

Share Scope는 공유 의존성이 유효한 범위를 의미하며, Module Federation의 핵심 메커니즘 중 하나입니다.

동작 과정

  1. 빌드 타임: 공유 의존성을 별도 청크로 분리하여 번들링
  2. 런타임: 먼저 로드되는 앱이 공유 의존성을 로드
  3. 최적화: 이후 로드되는 앱들은 이미 로드된 의존성을 재사용
  4. 버전 호환성: Share scope가 맞지 않으면 자체 번들 사용

중요한 점: 호스트 앱의 의존성이 무조건 사용되는 것이 아닙니다. 리모트 앱이 먼저 로드되면 리모트 앱의 의존성이 사용될 수 있습니다.

공유가 필요한 경우들

공유 의존성이 반드시 필요한 주요 상황들:

1. 전역 변수를 사용하는 라이브러리

// 문제가 되는 경우
if (typeof window !== 'undefined') {
  window.someLibraryInstance = new SomeLibrary();
}
// 인스턴스가 2개 이상이면 동시성 이슈 발생

2. 스타일 관련 라이브러리

// styled-components, emotion 등
shared: {
  'styled-components': { singleton: true },
  '@emotion/react': { singleton: true },
}
// 인스턴스가 여러 개면 스타일 충돌 발생

3. Context Provider 기반 라이브러리

// React Context를 사용하는 라이브러리들
const ThemeContext = createContext();

function HostApp() {
  return (
    <ThemeContext.Provider value={darkTheme}>
      <RemoteComponent /> {/* 같은 Context를 사용해야 함 */}
    </ThemeContext.Provider>
  );
}

의존성 분류 체계

의존성을 분류하는 방법:

const sharedDependencies = {
  // Core 라이브러리 - 반드시 singleton
  react: {
    singleton: true,
    requiredVersion: deps.react,
    eager: true, // 초기 청크에 포함
  },

  // 유틸리티 라이브러리 - 버전 호환성 고려
  lodash: {
    requiredVersion: deps.lodash,
    singleton: false, // 여러 버전 허용
  },

  // UI 라이브러리 - 스타일 충돌 방지
  '@mui/material': {
    singleton: true,
    requiredVersion: deps['@mui/material'],
  }
};

현실적 제약사항들

Module Federation이 아무리 강력해도 만능은 아닙니다. 도입을 고려한다면 이 기술이 가진 근본적인 한계를 알고 있어야 합니다.

트리쉐이킹의 한계

Module Federation을 사용하면서 가장 먼저 체감하게 되는 제약사항이 바로 트리쉐이킹의 한계입니다.

// 일반적인 애플리케이션에서는 가능
import { debounce } from 'lodash'; // 사용하는 함수만 번들에 포함

// Module Federation에서는 불가능
// lodash 전체가 공유 모듈로 번들에 포함됨
shared: {
  'lodash': { /* 전체 라이브러리가 포함 */ }
}

웹팩 메인테이너의 말을 빌리면: "개별 분리될 마이크로앱에서 사용할 모듈과 구현체를 모르는데 어떻게 트리쉐이킹을 하겠는가?"

이는 앞으로도 해결되기 어려운 근본적 한계입니다.

타입 안전성 문제

// 타입 정의가 별도로 필요
declare module 'remote-app/UserProfile' {
  interface UserProfileProps {
    userId: string;
    theme?: 'light' | 'dark';
  }
  const UserProfile: React.FC<UserProfileProps>;
  export default UserProfile;
}

// 실제 사용
import UserProfile from 'remote-app/UserProfile';
// 타입 검사는 되지만 런타임 호환성은 별도 보장 필요

SSR과 Module Federation

런타임에 동적으로 모듈을 불러오는 Module Federation과 빌드 타임에 모든 게 결정되어야 하는 SSR은 근본적으로 충돌합니다. 이는 두 기술의 작동 방식이 서로 반대 방향을 향하고 있기 때문입니다. 하지만 완전히 불가능한 것은 아닙니다.

SSR의 근본적 문제

Module Federation은 런타임에 원격 모듈을 동적으로 로드하는 방식인데, 이는 SSR의 기본 전제와 충돌합니다:

// 서버에서는 이런 코드가 문제가 됨
const RemoteComponent = lazy(() => import('remote-app/Component'));
// 서버는 http://localhost:3001/remoteEntry.js에 접근할 수 없음

SSR 구현 패턴

그렇다면 이를 어떻게 해결할 수 있을까요? 상황에 따라 세 가지 접근법을 고려해볼 수 있습니다.

1. 서버에서 원격 모듈을 직접 로드하는 방식

호스트 앱에서 SSR을 수행하고 리모트 앱은 CSR로 처리하는 패턴입니다.

  • 서버와 클라이언트용 분리 빌드: 리모트 앱을 Node.js 환경용과 브라우저용 두 가지로 빌드
  • 서버에서 동적 모듈 로드: CommonJS 형태로 빌드된 리모트 모듈을 서버에서 import
  • 스트리밍 SSR 활용: Suspense와 함께 사용하여 원격 모듈 로드 중에도 다른 부분을 먼저 렌더링
// 핵심 아이디어: 서버에서 리모트 모듈 동적 로드
const remoteModule = await import('http://localhost:3001/server/remoteEntry.js');
const RemoteButton = await remoteModule.get('./Button');

2. 분산 SSR 아키텍처

각 리모트 앱이 자체적으로 SSR을 수행하는 방식입니다.

  • 리모트 앱에 SSR 엔드포인트 추가: 각 리모트 앱이 /ssr-component 같은 엔드포인트를 통해 렌더링된 HTML을 제공
  • 호스트 앱에서 fetch로 HTML 가져오기: 서버에서 리모트 앱의 SSR 엔드포인트를 호출하여 HTML을 받아옴
  • 하이드레이션 처리: 클라이언트에서 실제 React 컴포넌트로 하이드레이션
// 핵심 아이디어: 리모트 앱의 SSR 결과를 fetch로 가져오기
const response = await fetch('http://localhost:3001/ssr-component');
const { html, css } = await response.json();

중요한 점: 하이드레이션을 위해 remoteEntry.js 파일은 여전히 필요합니다.

3. 하이브리드 접근 방식

서버와 클라이언트의 역할을 적절히 분리하는 방식입니다.

  • 서버에서는 플레이스홀더 HTML만 렌더링
  • 클라이언트에서 실제 원격 컴포넌트 로드
  • 로딩 상태와 에러 처리를 통한 사용자 경험 개선

Modern.js에서 제공하는 통합 솔루션

세 가지 패턴을 직접 구현하는 것은 상당히 복잡한 작업입니다. 다행히 Modern.js 같은 프레임워크는 이러한 문제를 프레임워크 레벨에서 해결해줍니다.

// modern.config.ts - 간단한 설정으로 MF + SSR 지원
export default defineConfig({
  server: {
    ssr: { mode: 'stream' },
  },
  runtime: {
    features: { federation: true },
  },
});

Modern.js가 해결한 문제들:

  • 서버/클라이언트 빌드 자동 분리
  • 스트리밍 SSR과 Module Federation 통합
  • 데이터 페칭과 SSR 자동 연동
  • 하이드레이션 불일치 방지

Next.js와 Module Federation

Next.js는 React 생태계에서 가장 많이 사용되는 프레임워크입니다. 하지만 Module Federation과 함께 사용하는 것은 다른 프레임워크들에 비해 상당한 제약이 따릅니다.

현재 상황 (#3153)

2024년 11월부터 Next.js 지원이 유지보수 모드로 전환되었습니다.

  • Pages Router: 2026년 중반까지 버그 수정만 제공
  • App Router: 실험 단계, 프로덕션 사용 비권장

왜 어려운가?

이러한 제한이 생긴 이유는 Next.js의 구조적 특성 때문입니다:

  1. App Router의 복잡한 구조: 서버 컴포넌트(RSC), SSR, 클라이언트 컴포넌트 등 10개 이상의 레이어가 있고, 각각 다른 React 버전을 사용합니다.

  2. 번들러 변경: Webpack에서 TurboPack으로 전환 중인데, TurboPack은 Module Federation을 지원하지 않습니다.

  3. 기술적 충돌: Next.js의 진입점 구조가 Module Federation의 동적 로딩 방식과 맞지 않습니다.

실무에서 사용하려면?

Pages Router 사용 (안정적 옵션)

// next.config.js
const { NextFederationPlugin } = require('@module-federation/nextjs-mf');

module.exports = {
  webpack(config) {
    config.plugins.push(
      new NextFederationPlugin({
        name: 'host',
        remotes: { remote: 'remote@http://localhost:3001/remoteEntry.js' },
        shared: { react: { singleton: true } },
      })
    );
    return config;
  },
};

App Router는?

실험 단계이며 메인테이너가 vercel 팀과 협력하며 지원을 위해 힘쓰고는 있지만 현재 프로덕션에서 사용하기엔 안정적이지 않습니다.

관련 진행 사항이 더 궁금하다면 이슈에서 직접 확인해보는게 큰 도움이 될 것 같ㄴ습니다.

추천 방향

그렇다면 실무에서는 어떤 선택을 해야 할까요?

지금 Next.js를 쓰고 있다면:

  • Pages Router를 유지하고 2026년 이전에 마이그레이션 계획 수립
  • Module Federation 대신 Multi-Zones나 Monorepo 고려

새 프로젝트를 시작한다면:

  • Module Federation이 필수라면 Modern.js, Remix 같은 대안 검토
  • Next.js를 선택한다면 Module Federation 없이 진행

Next.js App Router와 Module Federation을 함께 사용하려면 많은 제약과 불확실성을 감수해야 합니다. 프로덕션 환경에서는 더 안정적인 대안을 고려하는 것이 좋습니다.

핵심 특징 정리

  • 분산 시스템 관점: 네트워크 통신, 버전 불일치, 장애 상황을 항상 고려해야 합니다
  • 런타임 동적 해결: 빌드 타임이 아닌 런타임에 모듈이 결합됩니다
  • 의존성 협상: 서로 다른 버전의 라이브러리도 Share Scope를 통해 조율됩니다
  • 독립적 배포: 각 팀이 독립적으로 배포할 수 있습니다

적용 시 고려사항

Module Federation은 강력하지만 복잡도가 높은 기술입니다. 번들 크기 증가, 네트워크 지연, 버전 관리 등 추가로 신경 써야 할 부분이 많습니다.

하지만 다음과 같은 상황이라면 도입을 고려해볼 만합니다:

  • 여러 팀이 독립적으로 개발하고 배포해야 하는 경우
  • 각 팀의 기술 스택이나 배포 주기가 다른 경우
  • 레거시 앱과 신규 앱을 점진적으로 통합해야 하는 경우

반대로 팀 규모가 작거나 단일 애플리케이션으로 충분하다면 굳이 도입할 필요는 없습니다. 아키텍처는 문제를 해결하기 위한 도구일 뿐입니다.

Profile picture

온승찬 | Frontend Developer