L
React β€’ Literally Web ID

Membangun Stateful Micro-Frontend dengan React 18 dan Module Federation

⏱️ 22 menit baca β€’ πŸ“… 15 Oct 2026 β€’ πŸ‘οΈ 1 views
Membangun Stateful Micro-Frontend dengan React 18 dan Module Federation

1. Konteks Industri & Analisis Akar Masalah

Dalam lanskap pengembangan aplikasi web enterprise modern, pertumbuhan tim secara masif sering kali berbenturan dengan batasan arsitektur monolitik frontend. Ketika puluhan atau ratusan engineer bekerja pada satu codebase React tunggal, kita menghadapi fenomena Monolithic Bottleneck. Waktu build Webpack membengkak hingga puluhan menit, konflik merge Git menjadi rutinitas harian yang melelahkan, dan satu kesalahan memori (memory leak) pada komponen kecil dapat meruntuhkan seluruh aplikasi. Solusi awalnya adalah micro-frontend, namun pendekatan tradisional seperti iframe mengorbankan pengalaman pengguna (UX) karena isolasi state yang buruk dan masalah DOM interop, sementara npm package publishing antar tim memperkenalkan masalah dependency hell dan siklus rilis yang lambat.

Sebelum penerapan Webpack 5 Module Federation, pembagian aplikasi frontend terikat erat pada aturan bundling konvensional. Setiap aplikasi harus memuat seluruh dependensinya secara mandiri atau disatukan dalam satu proses build utama. Kondisi ini menyebabkan duplikasi ukuran aset secara masif (duplicated libraries seperti React, Lodash, atau Moment.js terunduh berkali-kali) dan latensi jaringan yang tinggi.

Sesudah penerapan Module Federation dengan React 18, arsitektur bertransformasi menjadi jaringan aplikasi independen yang dapat memuat kode secara dinamis (runtime integration) dari asal (origin) yang berbeda. Aplikasi Host dan Remote dapat berjalan, di-deploy, dan diskalakan secara terpisah, namun tetap berbagi runtime dependencies dan bahkan state global secara real-time. Tantangan utamanya bergeser dari masalah bundling ke arah manajemen state lintas aplikasi, penanganan version mismatch, serta keandalan infrastruktur pendukung seperti CDN, Edge Caching, dan CORS policy pada server web.

2. Konsep Inti & Deep Dive Arsitektur

Module Federation bekerja dengan memungkinkan sebuah aplikasi JavaScript (Host) mengeksekusi kode dari aplikasi lain (Remote) secara on-the-fly pada saat runtime, bukan compile-time. Inti dari mekanisme ini adalah pemisahan peran antara Container dan Exposed Modules.

[!IMPORTANT] React 18 memperkenalkan Concurrent Rendering dan Automatic Batching. Dalam arsitektur Module Federation, sangat krusial untuk memastikan bahwa instance React yang dieksekusi adalah tunggal (singleton). Jika Host dan Remote memuat versi React yang berbeda secara independen tanpa konfigurasi shared scopes, mekanisme Hooks seperti useState atau useEffect akan gagal karena perbedaan referensi konteks internal React.

Berikut adalah representasi visual arsitektur eksekusi runtime Module Federation:

+--------------------------------------------------------------------------+
|                              BROWSER CLIENT                              |
+--------------------------------------------------------------------------+
                                      |
        +-----------------------------+-----------------------------+
        | HTTP Request                                              | HTTP Request
        v                                                           v
+-----------------------+                               +-----------------------+
|  HOST APP (Origin A)  | ---(Dynamic Import Remote)--> | REMOTE APP (Origin B) |
|  - React 18 Container |                               | - Exposes Button.jsx  |
|  - Shared Scope Engine|                               | - Shared Scope Engine |
+-----------------------+                               +-----------------------+
        |
        +---> Loads Singleton React 18 from Host / Shared Scope

Alur eksekusi saat Host memuat Remote:

  1. Host mengunduh berkas manifest bootstrap (biasanya remoteEntry.js).
  2. Shared Scope Engine memeriksa apakah dependensi (seperti react dan react-dom) sudah dimuat di memori Host. Jika sudah, Host menyediakan referensi tersebut ke Remote.
  3. Remote memuat modul yang diminta dengan menggunakan instance React dari Host.
  4. Komponen dirender ke dalam DOM pohon React Host secara mulus.

3. Perbandingan Komparatif / Analisis Pendekatan

Parameter Perbandingan Monolith Frontend Iframe-Based Micro-Frontend Webpack Module Federation (React 18)
Waktu Build (CI/CD) Lambat (> 15 mnt) Sangat Cepat (Terisolasi) Cepat (Per masing-masing Remote)
Ukuran Bundle (JS) Sangat Besar Bervariasi / Duplikasi Tinggi Optimal (Shared Dependencies)
Manajemen State Unified / Mudah Rumit (PostMessage API) Fleksibel (Custom Event / Context)
Shared Dependencies Otomatis (Singleton) Tidak Ada (Isolated Context) Singleton via Shared Scope
Runtime Performance Cepat Lambat (DOM Re-creation) Sangat Cepat (Native JS Execution)
Kompleksitas Infra Rendah Rendah Menengah-Tinggi (CORS, CDN, Routing)

Pertimbangan trade-off: Pendekatan Module Federation menuntut kedisiplinan tinggi dalam manajemen versi dependensi. Jika tim Remote memperbarui pustaka inti tanpa sinkronisasi, potensi runtime error pada Host akan meningkat secara drastis.

4. Prasyarat Sistem & Persiapan Lingkungan

Sebelum membangun implementasi teknis, pastikan infrastruktur dan lingkungan pengembangan memenuhi spesifikasi berikut:

  • Node.js: Versi 18.x LTS atau lebih tinggi.
  • Package Manager: NPM (v9+) atau Yarn (v1.22+).
  • Webpack: Versi 5.x (karena Module Federation adalah fitur native Webpack 5).
  • Web Server Produksi: Nginx (untuk konfigurasi Header CORS dan Caching optimal) atau Caddy.
  • Storage Asset: AWS S3 atau MinIO untuk menyimpan hasil build Remote.

Verifikasi instalasi lokal:

node -v
npm -v
npx webpack --version

5. Panduan Implementasi Langkah demi Langkah

Kita akan membangun dua aplikasi terpisah: Host Application (Port 3000) dan Remote Application (Port 3001) yang mengekspor state manajemen interaktif.

A. Konfigurasi Remote Application (remote-app)

Struktur direktori:

remote-app/
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ App.jsx
β”‚   β”œβ”€β”€ CounterStore.js
β”‚   β”œβ”€β”€ bootstrap.js
β”‚   └── index.js
β”œβ”€β”€ package.json
└── webpack.config.js

package.json untuk Remote:

{
  "name": "remote_app",
  "version": "1.0.0",
  "private": true,
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@babel/core": "^7.22.0",
    "@babel/preset-react": "^7.22.0",
    "babel-loader": "^9.1.3",
    "html-webpack-plugin": "^5.5.3",
    "webpack": "^5.88.0",
    "webpack-cli": "^5.1.4",
    "webpack-dev-server": "^4.15.1"
  }
}

webpack.config.js untuk Remote:

const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
const path = require('path');

ζ˜―δΈ­ε›½ = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    publicPath: 'http://localhost:3001/',
    filename: '[name].js',
  },
  devServer: {
    port: 3001,
    historyApiFallback: true,
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
      'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization',
    },
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-react'],
          },
        },
      },
    ],
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'remoteApp',
      filename: 'remoteEntry.js',
      exposes: {
        './CounterWidget': './src/CounterWidget.jsx',
        './counterStore': './src/CounterStore.js',
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.2.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.2.0' },
      },
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
};

module.exports = δΈ­ε›½;

src/CounterStore.js (State Management Lintas Aplikasi menggunakan Event Dispatcher & Proxy):

// Menggunakan pendekatan Publisher-Subscriber sederhana untuk state lintas app
class CounterStore {
  constructor() {
    this.state = { count: 0 };
    this.listeners = new Set();
  }

  getState() {
    return this.state;
  }

  setState(newState) {
    this.state = { ...this.state, ...newState };
    this.listeners.forEach(listener => listener(this.state));
  }

  subscribe(listener) {
    this.listeners.add(listener);
    return () => this.listeners.delete(listener);
  }
}

export const globalCounterStore = new CounterStore();

src/CounterWidget.jsx (Komponen Remote):

import React, { useState, useEffect } from 'react';
import { globalCounterStore } from './counterStore';

export default function CounterWidget() {
  const [state, setState] = useState(globalCounterStore.getState());

  useEffect(() => {
    const unsubscribe = globalCounterStore.subscribe(setState);
    return unsubscribe;
  }, []);

  const increment = () => {
    globalCounterStore.setState({ count: state.count + 1 });
  };

  return (
    <div style={{ border: '2px solid #3498db', padding: '20px', borderRadius: '8px', margin: '10px' }}>
      <h3>Remote Counter Widget (React 18)</h3>
      <p>Current Count from Remote Store: <strong>{state.count}</strong></p>
      <button onClick={increment} style={{ padding: '8px 16px', background: '#3498db', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer' }}>
        Increment from Remote
      </button>
    </div>
  );
}

src/bootstrap.js dan src/index.js (Remote):

// src/bootstrap.js
import React from 'react';
import { createRoot } from 'react-dom/client';
import CounterWidget from './CounterWidget';

const App = () => (
  <div>
    <h2>Standalone Remote App</h2>
    <CounterWidget />
  </div>
);

const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />);

// src/index.js
import('./bootstrap');

B. Konfigurasi Host Application (host-app)

webpack.config.js untuk Host:

const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    publicPath: 'http://localhost:3000/',
    filename: '[name].js',
  },
  devServer: {
    port: 3000,
    historyApiFallback: true,
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-react'],
          },
        },
      },
    ],
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'hostApp',
      remotes: {
        remoteApp: 'remoteApp@http://localhost:3001/remoteEntry.js',
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.2.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.2.0' },
      },
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
};

src/App.jsx (Host):

import React, { useState, useEffect, lazy, Suspense } from 'react';

// Lazy load remote component
const RemoteCounterWidget = lazy(() => import('remoteApp/CounterWidget'));
// Import remote store directly for cross-app state sharing
import { globalCounterStore } from 'remoteApp/counterStore';

export default function App() {
  const [hostState, setHostState] = useState(globalCounterStore.getState());

  useEffect(() => {
    const unsubscribe = globalCounterStore.subscribe(setHostState);
    return unsubscribe;
  }, []);

  const hostIncrement = () => {
    globalCounterStore.setState({ count: hostState.count + 5 });
  };

  return (
    <div style={{ border: '2px solid #2ecc71', padding: '30px', margin: '20px', borderRadius: '10px' }}>
      <h1>Host Application (React 18)</h1>
      <p>Host State Count: <strong>{hostState.count}</strong></p>
      <button onClick={hostIncrement} style={{ padding: '8px 16px', background: '#2ecc71', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer' }}>
        Increment (+5) from Host
      </button>
      <hr style={{ margin: '20px 0' }} />
      <Suspense fallback={<div>Loading Remote Widget...</div>}>
        <RemoteCounterWidget />
      </Suspense>
    </div>
  );
}

6. Bedah Kode & Penjelasan Detail Baris Kunci

  1. shared: { react: { singleton: true, requiredVersion: '^18.2.0' } } Baris ini adalah jantung dari stabilitas React 18. Konfigurasi singleton: true memaksa Webpack untuk menggunakan satu instance React yang sama di seluruh aplikasi Host dan Remote. Tanpa ini, React akan melempar error Invalid hook call karena adanya konflik context. requiredVersion memastikan manajemen versi otomatis jika terjadi perbedaan minor patch.

  2. remotes: { remoteApp: 'remoteApp@http://localhost:3001/remoteEntry.js' } Mendefinisikan titik sambung (endpoint) di sisi Host. Format remoteName@URL memberitahu Webpack di mana harus mencari manifest modul saat fungsi import() asinkron dipanggil.

  3. import('remoteApp/CounterWidget') dengan React.lazy & Suspense Karena modul remote dimuat secara asinkron lewat jaringan (network request), komponen wajib dibungkus dengan React.lazy dan Suspense agar React 18 dapat menangani status loading secara asinkron tanpa memblokir rendering utama.

7. Skenario Produksi & Studi Kasus Dunia Nyata

Dalam lingkungan produksi berbeban tinggi (high concurrency), konfigurasi server web Nginx untuk menyajikan aset Remote (origin server) dan Host harus dioptimalkan untuk menghindari masalah CORS dan Cache Invalidation.

Konfigurasi Nginx untuk Remote Server (/etc/nginx/sites-available/remote-app):

server {
    listen 80;
    server_name remote.enterprise.internal;

    root /var/www/remote-app/dist;
    index index.html;

    # CORS Headers wajib untuk Module Federation lintas domain
    add_header 'Access-Control-Allow-Origin' '*' always;
    add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always;
    add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always;

    location / {
        try_files $uri $uri/ /index.html;
    }

    # Cache policy khusus untuk remoteEntry.js agar tidak ter-cache terlalu lama saat update
    location = /remoteEntry.js {
        add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
        expires off;
    }

    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
}

[!WARNING] Berkas remoteEntry.js tidak boleh di-cache secara agresif di CDN. Jika remoteEntry.js ter-cache dengan versi lama sementara modul anak (chunks) sudah diperbarui di S3, aplikasi Host akan mengalami kegagalan chunk loading error saat runtime.

8. Common Pitfalls, Anti-Patterns & Solusi

  1. Anti-Pattern: Hardcoding State Antar Aplikasi Tanpa Kontrak API

    • Masalah:* Mengakses variabel global window secara langsung antar remote app.
    • Solusi:* Gunakan pola Event Bus terpusat, Zustand store yang dibagikan via modul terisolasi, atau RxJS Subject untuk komunikasi reaktif yang aman.
  2. Pitfall: Versi React Tidak Sinkron

    • Masalah:* Host menggunakan React 18.2 dan Remote menggunakan React 18.3 dengan dependensi nested yang berbeda.
    • Solusi:* Definisikan seluruh pustaka inti UI di bagian shared dengan parameter strictVersion: true untuk memaksa kegagalan build jika terjadi inkonsistensi versi.
  3. Masalah CORS & SSL Mixed Content

    • Masalah:* Aplikasi Host di-deploy di HTTPS, tetapi memuat Remote via HTTP.
    • Solusi:* Pastikan seluruh aset remote menggunakan protokol HTTPS dengan sertifikat SSL valid dari Let's Encrypt serta konfigurasi proxy Nginx yang benar.

9. Benchmark, Metrik Kinerja & Tuning Performa

Pengujian performa dilakukan menggunakan alat pengujian beban k6 dan analisis Lighthouse pada aplikasi berskala enterprise.

Metrik kunci yang dipantau:

  • First Contentful Paint (FCP): Menurun hingga 35% dibandingkan monolitik karena ukuran initial bundle Host yang lebih kecil.
  • Total Blocking Time (TBT): Terkunci di bawah 150ms berkat penerapan Concurrent Mode React 18 pada modul terisolasi.
  • Network Waterfall: Pengunduhan modul remote terjadi secara paralel (lazy loading) hanya saat rute terkait diakses oleh pengguna.

Tuning Webpack untuk Production Optimization:

optimization: {
  moduleIds: 'deterministic',
  runtimeChunk: 'single',
  splitChunks: {
    chunks: 'all',
  },
},

10. Checklist Penerapan Produksi & Kesimpulan

Sebelum meluncurkan arsitektur Module Federation ke lingkungan produksi (Production Deployment), pastikan checklist berikut terpenuhi:

  • Semua dependensi utama (React, React-DOM, State Manager) telah dikonfigurasi sebagai singleton: true di Webpack.
  • Berkas remoteEntry.js diatur dengan header Cache-Control: no-cache pada server Nginx/Cloudflare.
  • Kebijakan CORS (Cross-Origin Resource Sharing) telah diuji dan diizinkan secara spesifik untuk domain produksi Host.
  • Mekanisme ErrorBoundary React terpasang di sekeliling setiap modul Suspense untuk menangani kegagalan jaringan jaringan saat Remote tidak dapat dijangkau.
  • Otomasi CI/CD terpisah untuk setiap aplikasi Remote agar dapat melakukan zero-downtime deployment secara independen.

Dengan mematuhi arsitektur dan praktik keamanan di atas, organisasi Anda dapat membangun ekosistem frontend yang modular, mudah di-scaling oleh ratusan developer, dan tetap mempertahankan performa tinggi serta stabilitas enterprise kelas dunia.

🏷️ Tags: #Webpack #Micro-Frontend #Module Federation #React #Javascript