https://blog.bitsrc.io/how-to-develop-microfrontends-using-react-step-by-step-guide-47ebb479cacd
Rumesh Eranga Hapuarachchi, 2020, 7월 28일
마이크로서비스들의 향상으로, 커다란 애플리케이션들은 많은 면에서 혜택을 받았다. 효율적으로 개발하고, 배치하고, 애플리케이션 백엔드의 개별 조각들의 규모를 확장하는 것을 도와준다. 여전히, 많은 사람들은 프론트엔드에도 유사한 문제들이 존재한다는 것을 깨달았다. 그곳이 우리가 전형적으로 거대한 프론트엔드를 마이크로프론트엔드로 분해하기 시작하는 지점이다.
마이크로프론트엔드는 한 팀이 하나의 유닛으로 독립적으로 개발하고, 시험하고, 배포할 수 있는 프론트엔드의 조각이다. 그러나 우리는 이 조각들을 함께 붙여서 단일한 웹 애플리케이션으로 사용자에게 보여주는 것을 확실히 해야 한다.
마이크로프론트엔드들 조립을 위한 전략들
주로 두 개의 접근 방법이 있다. 하나는 각 마이크로프론트엔드를 구성하고 호스팅하는 단일한 컨테이너 앱을 사용하는 것이다. 또 다른 접근 방법은 각각 서로 다른 곳으로 찾아갈 수 있는 URL(통합 포인트)과 파라미터들을 알고 있어서 이것들을 따로따로 호스팅 하는 것이다. (웹사이트의 페이지들과 유사)
이 글에서는 컨테이너 앱 접근 방식을 사용했다. 마이크로엔드들을 컨테이너 앱으로 합치는 방법은 여러가지가 있다. 이러한 방법들은 컴포지션을 사용한 서버사이드에서부터 프론트엔드 빌드 타임에서 통합, 그리고 런타임에서 등 다양하다. 게다가 만약 당신이 런타입 통합을 택했다면 아이프레임을 사용한 애플리케이션 통합, 자바스크립트를 사용한 통합, 또는 웹 컴포넌트들을 사용한 통합 등 다수의 선택지를 가지게 된다.
이 글은 꽤 단순한 작업흐름을 증명해 보이는 것이지, 마이크로 프론트엔드 디자인을 실행할 때 예상되는 모든 도전들로 다 들어가진 않는다. 한 가지 분명한 도전은 저장소(각 마이크로 프론트엔드의 저장소) 간 컴포넌트들을 공유하는 것이다. 이것은 일관적인 UI와 유지보수 가능하고 확장성 있는 프로젝트를 위해 결정적이다.
이 도전은 보통 컴포넌트들을 공유하고 관리하는 도구이자 플랫폼인 빗-Bit(깃헙-Github)을 사용하여 해결한다. 빗은 모어떤 프로젝트에서 온 컴포넌트들이든 빠르고 쉽게 공유하게 해준다. 이것은 또 호스팅, 기록, 그리고 공유되는 컴포넌트들을 조직하는 플랫폼도 제공한다.
리액트를 사용하여 마이크로 프론트엔드 개발하기
현실 세계에서 마이크로 프론트엔드는 그 규모의 양이 꽤 된다. 하지만 이 예제에서는 무작위로 고양이들과 강아지들의 이미지를 보여주는 리액트로 만들어진 간단한 웹 애플리케이션을 살펴보자.
나는 위 앱의 코드를 마이크로프론트엔드로 분해하여 각 단계마다 관련 있는 코드 토막들을 자세히 보여주겠다. 그리고나서 우리는 컨테이너 앱을 기저를 이루는 호스트로 사용하여 그것들을 살펴볼 것이다.
1단계: 리액트를 사용하여 기저 프로젝트 구조 만들기
이 애플리케이션에서 고양이들과 강아지들 컴포넌트들을 분리하여 각각 마이크로 프론트엔드를 대표하게 한다. 이러한 접근은 이것들을 단일한 앱으로 붙이는 방법을 넓은 범위에서 고를 수 있게 더 많은 융통성을 준다.
이제 마이크로 프론트엔드의 개념에 익숙해졌으니 지금까지 배운 것들로 무언가를 실행해볼 시간이다. 우리는 여기서 같은 고양이들과 강아지들의 예시를 사용할 것이다. 리액트를 사용하여 컨테이너 앱, 고양이 앱, 강아지 앱을 만들 것이다.
맨 처음부터 시작해서 아래의 단계들을 밟으면 된다. 리액트 기초와 익숙하고 이러한 애플리케이션을 마이크로 프론트엔드들로 전환하는 작업을 하고 싶다면 이 링크를 사용하여 보일러플레이트를 다운 받아 마이크로 프론트엔드로 전환 섹션으로 건너뛰기 하면 된다.
1. create-react-app을 사용하여 세 개의 리액트 프로젝트들을 만든다. 나는 이것들을 container, cats, dogs로 이름 지을 것이다.
npx create-react-app container
npx create-react-app cats
npx create-react-app dogs
2. 이제 우리 앱에서 가장 작은 마이크로프론트엔드인 Dogs 앱에서 작업을 하자. 무작위로 강아지 사진을 보여주는 것이 이 프로젝트의 의도다. 사용자는 버튼을 클릭하여 새로운 강아지의 이미지를 얻을 수 있다.
먼저 App.css 파일의 모든 것을 지운다. 일단 여기서는 아무 스타일링도 필요하지 않다.
그리고나서 App.js의 내용을 다음과 같이 업데이트 한다.
import React, { useState, useEffect } from "react";
import logo from "./logo.svg";
import "./App.css";
function App() {
const [dogImg, setDogImg] = useState(null);
const fetchDoggo = () => {
setDogImg("");
fetch(`https://dog.ceo/api/breeds/image/random`)
.then((res) => res.json())
.then((dogInfo) => {
setDogImg(dogInfo.message);
});
};
useEffect(() => {
if (dogImg === null) {
fetchDoggo();
}
});
return (
<div>
<header>
<h3>Doggo of the day</h3>
<div>
<button onClick={() => fetchDoggo()}>New Doggo</button>
</div>
{dogImg !== "" ? (
<div>
<img src={dogImg} width="400px" alt="doggo" />
</div>
) : (
<div>Loading Image</div>
)}
</header>
</div>
);
}
export default App;
이제 애플리케이션을 시작하여 시험해볼 수 있다. 다음과 같은 페이지를 볼 수 있을 것이다.
yarn start
3. 이제 고양이 애플리케이션의 기능을 업데이트할 시간이다. 이 애플리케이션에서 특별한 점은 이것이 라우팅을 지원한다는 것이다. 우리 애플리케이션에 두 개의 루트가 있다. 하나는 뿌리 URL로, 고양이를 무작위로 보여줄 것이다. 그 다음 것은 cats/{greeting}을 듣고 greeting 파라미터와 함께 고양이의 이미지를 보여주어야 한다.
먼저, react-router-dom과 history를 추가하여 라우팅과 히스토리 지원을 더한다. 이를 위해, 고양이 애플리케이션으로 가서 다음을 실행시킨다.
yarn add react-router-dom history
이제 여기서 아무런 스타일링이 필요하지 않으니 App.css의 모든 것을 지운다. 그 후 RandomCat과 GreetingCat 이라는 두 개의 컴포넌트들을 src 디렉토리 안에 만든다.
import React, { useState, useEffect } from "react";
export default function RandomCat() {
const [randomCatImg, setRandomCatImg] = useState(null);
const fetchRandomCat = () => {
setRandomCatImg("");
fetch(`https://aws.random.cat/meow`)
.then((res) => res.json())
.then((catInfo) => {
setRandomCatImg(catInfo.file);
});
};
useEffect(() => {
if (randomCatImg === null) {
fetchRandomCat();
}
});
return (
<div>
<header>
<h3>Cat of the day</h3>
<div>
<button onClick={() => fetchRandomCat()}>New Cat</button>
</div>
{randomCatImg !== "" ? (
<div>
<img src={randomCatImg} width="400px" alt="Cat" />
</div>
) : (
<div>Loading Image</div>
)}
</header>
</div>
);
}
다음 단계로 GreetingCat 컴포넌트를 만들어야 한다. 이 컴포넌트의 목적은 greeting 파라미터를 듣고 그에 따라 이미지를 생성하는 것이다.
import React from "react";
export default function GreetingCat(props) {
const { greeting } = props.match.params;
const greetingCatUrl = `https://cataas.com/cat/says/${greeting}`;
return (
<div>
<header>
<h3>Greet me</h3>
{!greeting ? (
<div>Needs a greeting</div>
) : (
<div>
<img src={greetingCatUrl} width="400px" alt="Cat" />
</div>
)}
</header>
</div>
);
}
이제 고양이 앱에 라우팅을 더할 시간이다. 이를 위해 App.js를 수정하여 다음 내용을 추가한다.
import React from "react";
import { Router, Route } from "react-router-dom";
import { createBrowserHistory } from "history";
import GreetingCat from "./GreetingCat";
import RandomCat from "./RandomCat";
import "./App.css";
const defaultHistory = createBrowserHistory();
function App({ history = defaultHistory }) {
return (
<Router history={history}>
<Route exact path="/" component={RandomCat} />
<Route exact path="/cat/:greeting" component={GreetingCat} />
</Router>
);
}
export default App;
고양이 애플리케이션은 다음과 같이 보일 것이다.
이제 두 개의 애플리케이션이 있다. 다음 단계는 컨테이너 애플리케이션을 만드는 것이다. 우리의 목적은 강아지 앱과 고양이 앱(RandomCat 컴포넌트)을 뿌리에 두고 여기로 가면 GreetingCat 컴포넌트로 보여주게 되는 것이다.
먼저, react-router-dom과 history 패키지들을 컨테이너 앱에 더한다.
yarn add react-router-dom history
그 후 App.js 를 다음 내용으로 업데이트한다.
import React, { useState } from "react";
import { BrowserRouter, Switch, Route, Redirect } from "react-router-dom";
import { createBrowserHistory } from "history";
import "./App.css";
const defaultHistory = createBrowserHistory();
function Header() {
return (
<div className="banner">
<h1 className="banner-title">😻 Cats and Dogs 🐕</h1>
<h4>Random pics of cats and dogs</h4>
</div>
);
}
function Home({ history }) {
const [input, setInput] = useState("");
const handleOnClick = () => {
history.push(`/cat/${input}`);
};
return (
<div>
<Header />
<div className="home">
<input
placeholder="Insert a greeting"
value={input}
onBlur={(e) => setInput(e.target.value)}
/>
<button onClick={handleOnClick}>Greet Me</button>
</div>
<div className="home">
<div className="content">
<div className="cat">
<img width="400px" src="https://cataas.com/cat/says/hello" />
</div>
<div className="dog">
<img
width="400px"
src="https://random.dog/91474781-c254-4397-b658-d19b7f0a4f5b.jpeg"
/>
</div>
</div>
</div>
</div>
);
}
function App({ history = defaultHistory }) {
return (
<BrowserRouter>
<React.Fragment>
<Switch>
<Route exact path="/" render={() => <Home history={history} />} />
</Switch>
</React.Fragment>
</BrowserRouter>
);
}
export default App;
컨테이너 애플리케이션에 스타일링을 더 할 시간이다. App.css의 내용을 다음 스타일링들로 교체한다.
.banner {
background: #f5f5da;
padding: 30px;
text-align: center;
border-radius: 20px;
}
.banner-title {
color: #2d3d29;
}
.home {
margin-top: 30px;
width: 100%;
text-align: center;
}
.content {
margin: auto;
width: 70%;
padding: 10px 0px;
background: #f5f5da;
display: inline-block;
border-radius: 10px;
}
.cat {
display: inline-block;
width: 45%;
}
.dog {
display: inline-block;
width: 45%;
}
2단계: 마이크로 프론트엔드로 전환하기
우리가 해결해야 할 첫번째 문제는 컨테이너 앱이 어떻게 마이크로 프론트엔드를 알게하는 것이냐다. 이것을 제기하기 위해 우리는 .env 파일을 이용하여 마이크로 프론트엔드 목록을 유지보수 할 수 있다.
로컬 개발에서 모든 마이크로 프론트엔드들은 로컬호스트에서 작동할 것이니 우리는 MFE들에 여러 포트들을 배분할 필요가 있다. 다음 포트들을 사용하자.
- 컨테이너 앱: 포트 3000
- 강아지 앱: 포트 3001
- 고양이 앱: 포트 3002
컨테이너 앱의 뿌리 레벨에 다음 내용으로 .env 파일을 생성한다.
REACT_APP_DOGS_HOST=http://localhost:3001
REACT_APP_CATS_HOST=http://localhost:3002
이제 우리는 어디로부터 각 마이크로 프론트엔드를 가져올 수 있는지 안다. 그런데 컨테이너 앱은 마이크로 프론트엔드들을 앱의 연관 섹션에 넣는 방법을 어떻게 알 수 있는가?
이 질문을 언급하기 위해, 우리의 모든 마이크로 앱 개발 팀들은 입구를 어떻게 정의할 것인지와 앱들을 어떻게 발견할 것인지에 대해 동의해야 한다. 이 예시에서 우리는 이를 해결하는 방법은 빌드 스크립트로 생성된 asset-manifest.json을 사용하는 것이다.
{
"files": {
"main.css": "/static/css/main.5f361e03.chunk.css",
"main.js": "/static/js/main.81def56f.chunk.js",
"main.js.map": "/static/js/main.81def56f.chunk.js.map",
"runtime-main.js": "/static/js/runtime-main.f9edd30b.js",
"runtime-main.js.map": "/static/js/runtime-main.f9edd30b.js.map",
"static/js/2.fd646533.chunk.js": "/static/js/2.fd646533.chunk.js",
"static/js/2.fd646533.chunk.js.map": "/static/js/2.fd646533.chunk.js.map",
"index.html": "/index.html",
"precache-manifest.53c6aa224b68ac094481ecedc920549c.js": "/precache-manifest.53c6aa224b68ac094481ecedc920549c.js",
"service-worker.js": "/service-worker.js",
"static/css/main.5f361e03.chunk.css.map": "/static/css/main.5f361e03.chunk.css.map",
"static/js/2.fd646533.chunk.js.LICENSE.txt": "/static/js/2.fd646533.chunk.js.LICENSE.txt",
"static/media/logo.svg": "/static/media/logo.5d5d9eef.svg"
},
"entrypoints": [
"static/js/runtime-main.f9edd30b.js",
"static/js/2.fd646533.chunk.js",
"static/css/main.5f361e03.chunk.css",
"static/js/main.81def56f.chunk.js"
]
}
자세히 들여다 보면 files 객체 안에 main.js 파일 경로가 있음을 볼 수 있다. 빌드 스크립트가 전체 애플리케이션을 main.js로 묶을 것이다. 이 main.js 안에 컴포넌트를 구현하고 탈착할 함수를 가지고 있어야 한다. 다음 방식을 사용하자.
구현 함수 이름: render{ 앱 이름 }
탈착 함수 이름: unmount{ 앱 이름 }
예시: 강아지 앱 함수 이름들
renderDogs
unmountDogs
이제 우리는 컴포넌트를 어떻게 구현하고 탈착하는지 아니까, 컨테이너 앱 안에 이러한 마이크로 프론트엔드들을 보유하고 있을 포괄적인 컴포넌트를 생성할 시간이 되었다. 컨테이너 앱에 MicroFrontend 컴포넌트를 추가함으로써 시작하자. 여기서 나는 캠 잭슨의 마이크로 프론트엔드 컴포넌트를 기초로 사용하였다.
컨테이너 앱 안에 src/MicroFrontend.js 를 생성하기
import React, { useEffect } from "react";
function MicroFrontend({ name, host, history }) {
useEffect(() => {
const scriptId = `micro-frontend-script-${name}`;
const renderMicroFrontend = () => {
window[`render${name}`](`${name}-container`, history);
};
if (document.getElementById(scriptId)) {
renderMicroFrontend();
return;
}
fetch(`${host}/asset-manifest.json`)
.then((res) => res.json())
.then((manifest) => {
const script = document.createElement("script");
script.id = scriptId;
script.crossOrigin = "";
script.src = `${host}${manifest.files["main.js"]}`;
script.onload = () => {
renderMicroFrontend();
};
document.head.appendChild(script);
});
return () => {
window[`unmount${name}`] && window[`unmount${name}`](`${name}-container`);
};
});
return <main id={`${name}-container`} />;
}
MicroFrontend.defaultProps = {
document,
window,
};
export default MicroFrontend;
이 마이크로 프론트엔드 컴포넌트는 name, host, history를 파라미터로 받을 것이다. 17행을 보면 이 컴포넌트가 asset-manifest.json 파일을 호스트로부터 먼저 가져와서 main.js 파일을 사용하여 스크립트 객체를 만드는 것을 볼 수 있을 것이다. 그 후 이것은 render 함수를 이용하여 컴포넌트를 장착시킨다.
다음 단계는 이 컴포넌트를 사용하여 고양이 애플리케이션과 강아지 애플리케이션을 우리의 앱에 장착시킨다. 이를 위해 컨테이너의 App.js 파일을 다음과 같이 수정한다.
import React, { useState } from "react";
import { BrowserRouter, Switch, Route } from "react-router-dom";
import { createBrowserHistory } from "history";
import MicroFrontend from "./MicroFrontend";
import "./App.css";
const defaultHistory = createBrowserHistory();
const {
REACT_APP_DOGS_HOST: dogsHost,
REACT_APP_CATS_HOST: catsHost,
} = process.env;
function Header() {
return (
<div className="banner">
<h1 className="banner-title">😻 Cats and Dogs 🐕</h1>
<h4>Random pics of cats and dogs</h4>
</div>
);
}
function Dogs({ history }) {
return <MicroFrontend history={history} host={dogsHost} name="Dogs" />;
}
function Cats({ history }) {
return <MicroFrontend history={history} host={catsHost} name="Cats" />;
}
function GreetingCat({ history }) {
return (
<div>
<Header />
<div className="home">
<MicroFrontend history={history} host={catsHost} name="Cats" />
</div>
</div>
);
}
function Home({ history }) {
const [input, setInput] = useState("");
const handleOnClick = () => {
history.push(`/cat/${input}`);
};
return (
<div>
<Header />
<div className="home">
<input
placeholder="Insert a greeting"
defaultValue={input}
onBlur={(e) => setInput(e.target.value)}
/>
<button onClick={handleOnClick}>Greet Me</button>
</div>
<div className="home">
<div className="content">
<div className="cat">
<Cats />
</div>
<div className="dog">
<Dogs />
</div>
</div>
</div>
</div>
);
}
function App() {
return (
<BrowserRouter>
<React.Fragment>
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/cat/:greeting" component={GreetingCat} />
</Switch>
</React.Fragment>
</BrowserRouter>
);
}
export default App;
10행에서 13행까지는 .env 파일로부터 앱 서버들을 읽기 위한 목적이다. 24행에서 41행까지를 읽는다면 우리가 마이크로 프론트엔드들을 사용하여 세 가지 컴포넌트들을 만들었음을 알아챌 것이다.
81~82행은 애플리케이션에다 앱이 이 루트들을 구현해야 한다는 것을 알린다. 뿌리 경로에서 우리는 고양이 앱과 강아지 앱 둘 다를 구현한다. 그러나 /cat/:greeting 경로에서 우리는 오직 고양이 앱만 구현할 것이다. 같은 루트가 고양이 마이크로엔드를 위해서도 보일 것이다. 그래서 기대한대로 그것이 고양이의 이미지를 글자와 함께 구현할 것이다.
컨테이너는 이것이 다다. 이제 기존의 마이크로 프론트엔드드를 수정할 시간이다.
3단계: 고양이와 강아지 마이크로 프론트엔드 앱들을 수정하기
고양이 마이크로프론트엔드 앱을 수정하기
react-app-rewired, history, 그리고 react-router-dom을 반드시 고양이 애플리케이션에 더해야 한다.
yarn add react-app-rewired history react-router-dom
컨테이너 앱이 MFE를 로딩하려할 때 모든 것이 하나의 자바스크립트 파일에 있어야 한다. 그런데 위의 asset-manifest.json 파일을 보면 거기에 다수의 덩어리가 이용 가능하다는 것을 볼 수 있을 것이다. 우리는 이러한 덩어리들을 비활성화 시켜야 한다. 이를 위해 우리는 react-app-rewired 패키지를 사용할 것이다. 이것을 사용하여 우리는 앱을 꺼내지 않고 빌드 환경설정을 덮어쓰기 할 수 있다. 고양이 애플리케이션의 뿌리 레벨에 config-overrides.js를 생성하고 다음 내용을 넣는다.
module.exports = {
webpack: (config, env) => {
config.optimization.runtimeChunk = false;
config.optimization.splitChunks = {
cacheGroups: {
default: false,
},
};
config.output.filename = "static/js/[name].js";
config.plugins[5].options.filename = "static/css/[name].css";
config.plugins[5].options.moduleFilename = () => "static/css/main.css";
return config;
},
};
그 다음 단계는 package.json 파일에 이 덮어쓰기를 사용하라고 지시하는 것이다. script 섹션을 다음과 같이 업데이트 한다. 그러면 이것이 모든 덩어리들을 제거하고 모든 것을 main.js로 묶었음을 알 수 있다.
"scripts": {
"start": "PORT=3002 react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-scripts test",
"eject": "react-scripts eject"
}
우리의 모든 마이크로 프론트엔드 컨테이너들이 서로 다른 하위 도메인들에 호스팅되므로 우리는 마이크로 프론트엔드들 안에 CORS를 만드시 활성화시켜야 한다. 이를 위해 src/setupProxy.js를 생성하고 다음 내용을 더한다.
module.exports = (app) => {
app.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", "*");
next();
});
};
고양이 마이크로 프론트엔드를 위해 필요한 마지막 수정은 그것의 index.js를 render 함수와 unmount 함수와 함께 업데이트 하는 것이다. 이것을 위해 index.js를 다음 내용으로 변경한다.
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
window.renderCats = (containerId, history) => {
ReactDOM.render(
<App history={history} />,
document.getElementById(containerId),
);
serviceWorker.unregister();
};
window.unmountCats = containerId => {
ReactDOM.unmountComponentAtNode(document.getElementById(containerId));
};
if (!document.getElementById('Cats-container')) {
ReactDOM.render(<App />, document.getElementById('root'));
serviceWorker.unregister();
}
강아지 마이크로 프론트엔드 앱 수정하기
고양이 애플리케이션과 동일한 변화들을 반복해야 한다. 반드시 package.json 안에 있는 포트 주소를 3001로 바꾸고 index.js 안에 있는 함수 이름들을 변경한다.
모두 성공적으로 설정하였다면 아래의 최종 결과를 볼 수 있게 될 것이다.
복습
이 글에서 우리는 마이크로 프론트엔드들의 기초 및 리액트를 사용하여 마이크로 프론트엔드들을 실행하는 방법을 논의하였다.
먼저 우리는 어떻게 마이크로 프론트엔드를 컨테이너에 구현하고 탈착하는지 알고 있는 컨테이너 앱과 마이크로 프론트엔드라고 불리는 자리맡기 용 컴포넌트를 만들었다. 다음, 우리는 기존의 애플리케이션을 마이크로 프론트엔드로 전환하는 방법을 논의했다. 마지막으로 우리는 어떻게 주소창이 다른 마이크로엔드들과 소통하도록 사용하는지를 논의했다.
소스들은 다음 깃헙 저장소에서 찾을 수 있다.
- https://github.com/rehrumesh/react-microfrontend-container
- https://github.com/rehrumesh/react-microfrontend-container-cats-app.git
- https://github.com/rehrumesh/react-microfrontend-container-dogs-app
'웹개발' 카테고리의 다른 글
리덕스는 무슨 일을 할까? (그리고 언제 사용해야 할까?) (0) | 2020.08.08 |
---|---|
2020년에 웹 개발자가 되는 방법 - 완성 가이드 (0) | 2020.08.01 |
자바스크립트 개발자로서 알아야할 데이터 구조 (0) | 2020.07.28 |
자바스크립트로 더 깔끔한 코드를 쓰는 방법 (0) | 2020.07.27 |
리액트에서 스타일드-컴포넌츠를 사용하는 방법 (0) | 2020.07.24 |