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
useStateatauuseEffectakan 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:
- Host mengunduh berkas manifest bootstrap (biasanya
remoteEntry.js). - Shared Scope Engine memeriksa apakah dependensi (seperti
reactdanreact-dom) sudah dimuat di memori Host. Jika sudah, Host menyediakan referensi tersebut ke Remote. - Remote memuat modul yang diminta dengan menggunakan instance React dari Host.
- 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
-
shared: { react: { singleton: true, requiredVersion: '^18.2.0' } }Baris ini adalah jantung dari stabilitas React 18. Konfigurasisingleton: truememaksa Webpack untuk menggunakan satu instance React yang sama di seluruh aplikasi Host dan Remote. Tanpa ini, React akan melempar errorInvalid hook callkarena adanya konflik context.requiredVersionmemastikan manajemen versi otomatis jika terjadi perbedaan minor patch. -
remotes: { remoteApp: 'remoteApp@http://localhost:3001/remoteEntry.js' }Mendefinisikan titik sambung (endpoint) di sisi Host. FormatremoteName@URLmemberitahu Webpack di mana harus mencari manifest modul saat fungsiimport()asinkron dipanggil. -
import('remoteApp/CounterWidget')denganReact.lazy&SuspenseKarena modul remote dimuat secara asinkron lewat jaringan (network request), komponen wajib dibungkus denganReact.lazydanSuspenseagar 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.jstidak boleh di-cache secara agresif di CDN. JikaremoteEntry.jster-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
-
Anti-Pattern: Hardcoding State Antar Aplikasi Tanpa Kontrak API
- Masalah:* Mengakses variabel global
windowsecara 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.
- Masalah:* Mengakses variabel global
-
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
shareddengan parameterstrictVersion: trueuntuk memaksa kegagalan build jika terjadi inkonsistensi versi.
-
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: truedi Webpack. - Berkas
remoteEntry.jsdiatur dengan headerCache-Control: no-cachepada 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
Suspenseuntuk 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.