➜ mkdir module-federation-redux-example
➜ cd module-federation-redux-example
➜ pnpm init
➜ corepack use [email protected]
➜ code .
# module-federation-redux-example/pnpm-workspace.yaml
packages:
- "apps/*"
2개의 워크스페이스 생성
➜ mkdir apps
➜ cd apps
➜ pnpm create mf-app # (main-app, 3000)
➜ pnpm create mf-app # (remote-app, 3001)
➜ cd ...
➜ pnpm i
{
"name": "main-app",
"version": "1.0.0",
"scripts": {
"build": "webpack --mode production",
"build:dev": "webpack --mode development",
"build:start": "cd dist && PORT=3000 npx serve",
"start": "webpack serve --open --mode development",
"start:live": "webpack serve --open --mode development --live-reload --hot"
},
"license": "MIT",
"author": {
"name": "Jack Herrington",
"email": "[email protected]"
},
"devDependencies": {
"@babel/core": "^7.15.8",
"@babel/plugin-transform-runtime": "^7.15.8",
"@babel/preset-env": "^7.15.8",
"@babel/preset-react": "^7.14.5",
"@babel/preset-typescript": "^7.10.4",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"autoprefixer": "^10.1.0",
"babel-loader": "^8.2.2",
"css-loader": "^6.3.0",
"html-webpack-plugin": "^5.3.2",
"postcss": "^8.2.1",
"postcss-loader": "^4.1.0",
"style-loader": "^3.3.0",
"typescript": "^4.5.2",
"webpack": "^5.57.1",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.3.1"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}
{
"name": "remote-app",
"version": "1.0.0",
"scripts": {
"build": "webpack --mode production",
"build:dev": "webpack --mode development",
"build:start": "cd dist && PORT=3001 npx serve",
"start": "webpack serve --open --mode development",
"start:live": "webpack serve --open --mode development --live-reload --hot"
},
"license": "MIT",
"author": {
"name": "Jack Herrington",
"email": "[email protected]"
},
"devDependencies": {
"@babel/core": "^7.15.8",
"@babel/plugin-transform-runtime": "^7.15.8",
"@babel/preset-env": "^7.15.8",
"@babel/preset-react": "^7.14.5",
"@babel/preset-typescript": "^7.10.4",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"autoprefixer": "^10.1.0",
"babel-loader": "^8.2.2",
"css-loader": "^6.3.0",
"html-webpack-plugin": "^5.3.2",
"postcss": "^8.2.1",
"postcss-loader": "^4.1.0",
"style-loader": "^3.3.0",
"typescript": "^4.5.2",
"webpack": "^5.57.1",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.3.1"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}
➜ pnpm i
➜ pnpm --filter main-app add @reduxjs/toolkit react-redux
➜ pnpm --filter remote-app add @reduxjs/toolkit react-redux
main-app 워크스페이스
// module-federation-redux-example/apps/main-app/src/redux/modules/counter.ts
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
value: 0,
};
export const { reducer, actions } = createSlice({
name: "counter",
initialState,
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
},
});
export const { increment, decrement } = actions;
export default reducer;
// module-federation-redux-example/apps/main-app/src/redux/store.ts
import { configureStore, Reducer, combineReducers } from "@reduxjs/toolkit";
import counter from "./modules/counter";
const createStore = () => {
const staticReducers = {
counter,
};
const asyncReducers: { [index: string]: Reducer } = {};
const store = configureStore({
reducer: {
...staticReducers,
},
});
function injectReducer(key: string, asyncReducer: Reducer) {
asyncReducers[key] = asyncReducer;
store.replaceReducer(
combineReducers({
...staticReducers,
...asyncReducers,
})
);
}
return {
store,
injectReducer,
};
};
export default createStore;
// module-federation-redux-example/apps/main-app/src/App.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import createStore from "./redux/store";
import { Provider, useDispatch, useSelector } from "react-redux";
import "./index.css";
import { decrement, increment } from "./redux/modules/counter";
const { store, injectReducer } = createStore();
const App = () => {
const dispatch = useDispatch();
const counter = useSelector<ReturnType<typeof store.getState>, number>(
(state) => state.counter.value
);
return (
<div className="container">
<div>Name: main-app</div>
<div>Framework: react</div>
<div>Language: TypeScript</div>
<div>CSS: Empty CSS</div>
<div>{counter}</div>
<div>
<button onClick={() => dispatch(increment())}>+</button>
<button onClick={() => dispatch(decrement())}>-</button>
</div>
</div>
);
};
ReactDOM.createRoot(document.getElementById("app")!).render(
<Provider store={store}>
<App />
</Provider>
);
➜ pnpm --filter main-app start:live
➜ pnpm --filter main-app add @babel/runtime
➜ pnpm --filter main-app start:live
remote-app 워크스페이스
// module-federation-redux-example/apps/remote-app/src/redux/modules/name.ts
import { createSlice } from "@reduxjs/toolkit";
export const remoteAppScopeName = "@remote-app/name";
const initialState = {
value: "remoteApp",
};
export const { reducer, actions } = createSlice({
name: remoteAppScopeName,
initialState,
reducers: {
setName: (state, action) => {
state.value = action.payload.name;
},
},
});
export const { setName } = actions;
export default reducer;
// module-federation-redux-example/apps/remote-app/src/App.tsx
import React, { useEffect } from "react";
import reducer, { remoteAppScopeName, setName } from "./redux/modules/name";
import { Provider, useSelector, useDispatch, useStore } from "react-redux";
import "./index.css";
import { Reducer, type Store } from "@reduxjs/toolkit";
const App = () => {
const dispatch = useDispatch();
const name = useSelector<
{
[remoteAppScopeName]?: {
value: string;
};
},
string | undefined
>((state) => state?.[remoteAppScopeName]?.value);
const store = useStore();
return (
<div className="container">
<div>Name: remote-app</div>
<div>Framework: react</div>
<div>Language: TypeScript</div>
<div>CSS: Empty CSS</div>
<div>
<button
onClick={() => {
dispatch(setName({ name: "리모트앱" }));
}}
>
change
</button>
</div>
{name && <div>{name}</div>}
</div>
);
};
const RemoteAppWrapper: React.FC<
React.PropsWithChildren<{
store: Store;
injectReducer: (scope: string, reducer: Reducer) => void;
}>
> = ({ store, injectReducer }) => {
useEffect(() => {
injectReducer(remoteAppScopeName, reducer);
}, []);
return (
<Provider store={store}>
<App />
</Provider>
);
};
export default RemoteAppWrapper;
// module-federation-redux-example/apps/remote-app/src/webpack.config.js
const HtmlWebPackPlugin = require("html-webpack-plugin");
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
const deps = require("./package.json").dependencies;
module.exports = (_, argv) => ({
output: {
publicPath: "<http://localhost:3001/>",
},
resolve: {
extensions: [".tsx", ".ts", ".jsx", ".js", ".json"],
},
devServer: {
port: 3001,
historyApiFallback: true,
},
module: {
rules: [
{
test: /\\.m?js/,
type: "javascript/auto",
resolve: {
fullySpecified: false,
},
},
{
test: /\\.(css|s[ac]ss)$/i,
use: ["style-loader", "css-loader", "postcss-loader"],
},
{
test: /\\.(ts|tsx|js|jsx)$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
},
},
],
},
plugins: [
new ModuleFederationPlugin({
name: "remote_app",
filename: "remoteEntry.js",
remotes: {},
exposes: {
"./RemoteApp": "./src/App",
},
shared: {
...deps,
react: {
singleton: true,
requiredVersion: deps.react,
},
"react-dom": {
singleton: true,
requiredVersion: deps["react-dom"],
},
},
}),
new HtmlWebPackPlugin({
template: "./src/index.html",
}),
],
});
main-app 과 remote-app 통합
import React, { Suspense } from "react";
import ReactDOM from "react-dom/client";
import createStore from "./redux/store";
import { Provider, useDispatch, useSelector } from "react-redux";
import "./index.css";
import { decrement, increment } from "./redux/modules/counter";
const RemoteApp = React.lazy(() => import("remote_app/RemoteApp"));
const { store, injectReducer } = createStore();
const App = () => {
const dispatch = useDispatch();
const counter = useSelector<ReturnType<typeof store.getState>, number>(
(state) => state.counter.value
);
return (
<div className="container">
<div>Name: main-app</div>
<div>Framework: react</div>
<div>Language: TypeScript</div>
<div>CSS: Empty CSS</div>
<div>{counter}</div>
<div>
<button onClick={() => dispatch(increment())}>+</button>
<button onClick={() => dispatch(decrement())}>-</button>
</div>
<Suspense fallback={<div>Loading...</div>}>
<RemoteApp store={store} injectReducer={injectReducer} />
</Suspense>
</div>
);
};
ReactDOM.createRoot(document.getElementById("app")!).render(
<Provider store={store}>
<App />
</Provider>
);
{
"name": "module-federation-router-example",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "pnpm --parallel start:live",
"build": "pnpm --parallel build",
"serve": "pnpm --parallel build:start"
},
"keywords": [],
"author": "",
"license": "ISC",
"packageManager": "[email protected]+sha1.77d568bacf41eeefd6695a7087c1282433955b5c"
}
➜ pnpm dev