간혹 마이크로프론트엔드에서 CSS가 제대로 적용 안되는 상황이 발생한다. 특히 배포환경에서 자주 발생하는 문제이다. 이 문제는 배포환경에서 CSS를 빌드해서 사용하는 부분에서 class가 중첩되면서 발생하는 버그이다.
해결방법은 굉장히 단순 하다, 빌드시 classname이 중첩안되게 prefix를 설정해주면 된다.
import React from "react";
import { Switch, Route, BrowserRouter } from "react-router-dom";
import {
StylesProvider,
createGenerateClassName,
} from "@material-ui/core/styles";
import Landing from "./components/Landing";
import Pricing from "./components/Pricing";
const generateClassName = createGenerateClassName({
productionPrefix: "ma",
});
export default () => {
return (
<div>
<StylesProvider generateClassName={generateClassName}>
<BrowserRouter>
<Switch>
<Route exact path="/pricing" component={Pricing} />
<Route path="/" component={Landing} />
</Switch>
</BrowserRouter>
</StylesProvider>
</div>
);
};
import React from "react";
import { BrowserRouter } from "react-router-dom";
import { StylesProvider, createGenerateClassName } from "@material-ui/styles";
import MarketingApp from "./components/MarketingApp";
import Header from "./components/Header";
const generateClassName = createGenerateClassName({
productionPrefix: "co",
});
export default () => {
return (
<StylesProvider generateClassName={generateClassName}>
<BrowserRouter>
<div>
<Header />
<MarketingApp />
</div>
</BrowserRouter>
</StylesProvider>
);
};
프로젝트마다 다른 라우팅 라이브러리 및 기능을 쓰게될 경우 페이지 컨텐츠 자체는 바뀌지만 url은 안바뀌거나 그 반대로 url은 바뀌지만 컨텐츠는 안바뀌는 버그를 볼 수 있을 것이다. 이 문제는 프로젝트간의 라우팅이 sync가 안되어 있기 떄문이다. 아래처럼 marking의 routing방식을 바꿔서 일부로 위와 같은 상황을 만들어보자.
import React from "react";
import ReactDOM from "react-dom";
import { createMemoryHistory } from "history";
import App from "./App";
// Mount function to start up the app
const mount = (el) => {
const history = createMemoryHistory();
ReactDOM.render(<App history={history} />, el);
};
// If we are in development and in isolation,
// call mount immediately
if (process.env.NODE_ENV === "development") {
const devRoot = document.querySelector("#_marketing-dev-root");
if (devRoot) {
mount(devRoot);
}
}
// We are running through container
// and we should export the mount function
export { mount };
import React from "react";
import { Switch, Route, Router } from "react-router-dom";
import {
StylesProvider,
createGenerateClassName,
} from "@material-ui/core/styles";
import Landing from "./components/Landing";
import Pricing from "./components/Pricing";
const generateClassName = createGenerateClassName({
productionPrefix: "ma",
});
export default ({ history }) => {
return (
<div>
<StylesProvider generateClassName={generateClassName}>
<Router history={history}>
<Switch>
<Route exact path="/pricing" component={Pricing} />
<Route path="/" component={Landing} />
</Switch>
</Router>
</StylesProvider>
</div>
);
};
기존에 쓰던 방식이 BrowserRouter을 안쓰고 그냥 Router을 쓴다. 둘은 history를 통해 라우팅 하는 방식 vs memory를 통해 라우팅 하는 방식으로 나뉘기 때문에 위에 언급했던 버그가 발생한다.
마케팅 프로젝트에서 컨테이너의 라우팅 변경 또는 마케팅 프로젝트의 라우팅 변경을 감지해서 그 값을 비교하는 로직을 만들어준다.
import React from "react";
import ReactDOM from "react-dom";
import { createMemoryHistory, createBrowserHistory } from "history";
import App from "./App";
// Mount function to start up the app
const mount = (el, { onNavigate, defaultHistory }) => {
// container 일 경우는 메모리 히스토리 , 그게 아닐시 browserhistory를 사용한다.
const history = defaultHistory || createMemoryHistory();
if (onNavigate) {
history.listen(onNavigate);
}
ReactDOM.render(<App history={history} />, el);
return {
// container의 history를 가져와 이벤트 처리를 해준다.
onParentNavigate({ pathname: nextPathname }) {
const { pathname } = history.location;
if (pathname !== nextPathname) {
history.push(nextPathname);
}
},
};
};
// 개발 환경일 경우 아래를 실행한다.
if (process.env.NODE_ENV === "development") {
const devRoot = document.querySelector("#_marketing-dev-root");
if (devRoot) {
mount(devRoot, { defaultHistory: createBrowserHistory() });
}
}
export { mount };
아래는 컨테이너에서 마케팅 프로젝트에게 라우트 상태 받아오거나 보내기 위한 로직이다.
import { mount } from "marketing/MarketingApp";
import React, { useRef, useEffect } from "react";
import { useHistory } from "react-router-dom";
export default () => {
const ref = useRef(null);
const history = useHistory();
useEffect(() => {
const { onParentNavigate } = mount(ref.current, {
// marketing app 안에서 일어나는 routing 처리
onNavigate: ({ pathname: nextPathname }) => {
const { pathname } = history.location;
if (pathname !== nextPathname) {
history.push(nextPathname);
}
},
});
// container에서 일어나는 routing 처리
history.listen(onParentNavigate);
}, []);
return <div ref={ref} />;
};
대부분의 서비스를 유저의 인증정보를 필수적으로 요구하고 있다 그렇기에 마이크로프론트엔드 아키텍쳐를 구축하는 과정에서 인증이 어떻게 이루어지는지가 제일 궁금했다. 한번 로그인을 구축해보도록 하자.
먼저 auth라는 프로젝트를 marketing 프로젝트와 동일한 구조로 만들어주도록 하자. 그리고 로그인과 회원가입 component를 추가해주도록 하자.
import React from "react";
import { Switch, Route, Router } from "react-router-dom";
import {
StylesProvider,
createGenerateClassName,
} from "@material-ui/core/styles";
import SignUp from "./components/Signup";
import SignIn from "./components/Signin";
const generateClassName = createGenerateClassName({
productionPrefix: "au",
});
export default ({ history }) => {
return (
<div>
<StylesProvider generateClassName={generateClassName}>
<Router history={history}>
<Switch>
<Route path="/auth/signin" component={SignIn} />
<Route path="/auth/signup" component={SignUp} />
</Switch>
</Router>
</StylesProvider>
</div>
);
};
auth의 웹팩 설정을 8082로 바꿔주고 실행하면 아래와 같은 에러가 뜨는 것을 확인 할 수 있다. 이 에러는 앱이 로딩될 떄 존재하지 않는 path에서 main.js를 가져올려고 하다보니 발생하는 에러다.
여기서 두가지 궁금증이 생길 것 이다.
일단 해결 방법은 간단하다, 개발환경일때 웹팩설정에서 publicPath를 그 프로젝트의 baseURL로 설정해주면 된다. 마케팅 프로젝트에서 이 에러가 안뜨는 이유는 마케팅 프로젝트의 기본 라우팅이 "/" 로 시작하기 떄문이다. 기본적으로 publicPath를 설정하지 않으면 localhost을 바라보게 되는게 그게 우연히 잘 맞았던 것이다. 이 문제를 픽스하기 위해 아래와 같이 설정을 모두 프로젝트에 추가해주자.
const { merge } = require("webpack-merge");
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
const commonConfig = require("./webpack.common");
const packageJson = require("../package.json");
const devConfig = {
mode: "development",
output: {
publicPath: "http://localhost:8082/",
},
devServer: {
port: 8082,
historyApiFallback: {
index: "/index.html",
},
},
plugins: [
new ModuleFederationPlugin({
name: "auth",
filename: "remoteEntry.js",
exposes: {
"./AuthApp": "./src/bootstrap",
},
// 프로젝트간의 공통 모듈 공유
shared: packageJson.dependencies,
}),
],
};
module.exports = merge(commonConfig, devConfig);
로그인 및 로그아웃 상태를 판별하기 위해서 그에 해당하는 상태 값을 관리를 해야한다. MFA에서는 container에서 모든 인증 값을 관리하고 그걸 다른 프로젝트에서 내려받는 식으로 구현을 하면 된다. 일단 간단하게 리액트의 useState를 통해 상태값을 관리 해보자.
import React, { lazy, Suspense, useState } from "react";
import { BrowserRouter, Route, Switch } from "react-router-dom";
import { StylesProvider, createGenerateClassName } from "@material-ui/styles";
import Header from "./components/Header";
import Progress from "./components/Progress";
const MarketingLazy = lazy(() => import("./components/MarketingApp"));
const AuthLazy = lazy(() => import("./components/AuthApp"));
const generateClassName = createGenerateClassName({
productionPrefix: "co",
});
export default () => {
// 로그인 상태값 관리
const [isSignedIn, setIsSignedIn] = useState(false);
return (
<BrowserRouter>
<StylesProvider generateClassName={generateClassName}>
<div>
// 헤더에 있는 로그인 버튼 변화주기위해 상태값을 내려준다.
<Header
onSignOut={() => setIsSignedIn(false)}
isSignedIn={isSignedIn}
/>
<Suspense fallback={<Progress />}>
<Switch>
// auth 값에서 로그인 및 회원가입 버튼 이벤트 발생시 값을 변화시켜 주도록 한다.
<Route path="/auth">
<AuthLazy onSignIn={() => setIsSignedIn(true)} />
</Route>
<Route path="/" component={MarketingLazy} />
</Switch>
</Suspense>
</div>
</StylesProvider>
</BrowserRouter>
);
};
import React from "react";
import ReactDOM from "react-dom";
import { createMemoryHistory, createBrowserHistory } from "history";
import App from "./App";
// onSignIn을 컨테이너로 부터 넘겨받는다.
const mount = (el, { onSignIn, onNavigate, defaultHistory, initialPath }) => {
const history =
defaultHistory ||
createMemoryHistory({
initialEntries: [initialPath],
});
if (onNavigate) {
history.listen(onNavigate);
}
// onSignIn에 해당하는 이벤트를 발생시키기 위해 auth의 App.js로 값을 내려준다.
ReactDOM.render(<App history={history} onSignIn={onSignIn} />, el);
return {
onParentNavigate({ pathname: nextPathname }) {
const { pathname } = history.location;
if (pathname !== nextPathname) {
history.push(nextPathname);
}
},
};
};
if (process.env.NODE_ENV === "development") {
const devRoot = document.querySelector("#_auth-dev-root");
if (devRoot) {
mount(devRoot, { defaultHistory: createBrowserHistory() });
}
}
export { mount };
onSignIn 값이 필요한 컴포넌트에 값을 전해준다.
import React from "react";
import { Switch, Route, Router } from "react-router-dom";
import {
StylesProvider,
createGenerateClassName,
} from "@material-ui/core/styles";
import SignUp from "./components/Signup";
import SignIn from "./components/Signin";
const generateClassName = createGenerateClassName({
productionPrefix: "au",
});
export default ({ history, onSignIn }) => {
return (
<div>
<StylesProvider generateClassName={generateClassName}>
<Router history={history}>
<Switch>
<Route path="/auth/signin">
<SignIn onSignIn={onSignIn} />
</Route>
<Route path="/auth/signup">
<SignUp onSignIn={onSignIn} />
</Route>
</Switch>
</Router>
</StylesProvider>
</div>
);
};
이제 auth 프로젝트 배포 자동화를 위해 아래와 같이 설정해주자.
# auth actions
name: deploy-auth
on:
# auth 폴더안에 있는 내용이 변경이 main 브랜치에 push 되었을 떄 발동 된다.
push:
branches:
- main
paths:
- "auth/**"
# auth안에서 아래의 jobs에 해당하는 작업들이 이루어지며
defaults:
run:
working-directory: auth
# aws s3에 해당하는 명령어들 추가
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: npm install
- run: npm run build
- uses: shinyinc/action-aws-cli@v1.2
- run: aws s3 sync dist s3://${{ secrets.AWS_S3_BUCKET_NAME }}/auth/latest
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ap-northeast-2
- run: aws cloudfront create-invalidation --distribution-id ${{ secrets.AWS_DISTRIBUTION_ID }} --paths "/auth/latest/remoteEntry.js"
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ap-northeast-2