[Micro Frontend] 마이크로 프론트앤드 - 개념
독립적으로 제공한 프론트엔드 APP으로 더 큰 하나의 전체 APP을 구성하는 아키텍쳐 스타일
- 대규모 서비스를 개발할 때 용이하다.
- 마이크로 프론트엔드란 프론트엔드의 단일 구조를 개별적으로 개발/테스트 및 배포할 수 있는 작고 간단한 단위로 개발하는 패턴
- 전체화면을 작동할 수 있는 단위로 나누어 개발한 후 서로 조립하는 방식.
- 작동단위에 사용된 프레임워크의 종류에 상관하지않고 조합가능한 방법을 제공.
- 단일 팀에 의해 유지될정도의 작은 서비스의 경우에는 오버엔지니어링이 되지 않도록 주의한다.
MF로 개발 후 각 단위 APP을 어떻게 통합할지 고려가 필요하다.
각 화면을 조합하는 컨테이너 APP 과 그 내부에 들어가는 단위 APP이 존재.
- 컨테이너 어플리케이션 [ 각페이지에 대한 마이크로 어플리케이션들, .. ]
- Header / Footer 공통 페이지 엘리먼트 렌더링 → 인증 및 탐색 등 교차적 문제 해결 → 페이지에 다양한 마이크로 앱을 가저와서 각 앱에게 언제어디서 렌더링 할지 설정
다양한 통합방식이 존재하지만, 현재 마이크로프론트엔드에서 가장 추천되어지는 것은 JS를 통한 런타임 통합 방식입니다.
JavaScript를 통한 런타임 통합
가장 추천되는 접근방식
각 micro app은 <script>
태그를 사용하여 페이지에 삽입되고, 로드 시 전역 함수를 시작점으로 사용함
컨테이너 앱은 어떤 마이크로앱이 마운트 되어야 하는지를 결정하고 마이크로앱에게 언제 어디에서 렌더링해야할지를 알려주는 함수를 호출한다.
예제 : micro-frontends-demo
항상 컨테이너가 메인이되고 그 아래 마이크로 앱들이 존재하는것을 명심한다.
마이크로 앱을 선택하고 배치하는 방법은 App.js 를 통해 관리된다.
- react router를 사용하여 미리 정의된 router 목록과 현재 URL을 일치시켜 해당 컴포넌트를 렌더링 하는 방식
<Switch>
<Route exact path="/" component={Browse} />
<Route exact path="/restaurant/:id" component={Restaurant} />
</Switch>
Browse 및 Restaurant 컴포넌트
const Browse = ({ history }) => (
<MicroFrontend history={history} name="Browse" host={browseHost} />
);
const Restaurant = ({ history }) => (
<MicroFrontend history={history} name="Restaurant" host={restaurantHost} />
);
```
App.js 에서 마이크로앱을 선택하면 그것은 MicroFrontend.js에서 렌더링한다.
렌더링할때 마이크로 앱에 고유한 id를 가진 컨테이너 요소를 페이지에 넣으면 된다.
마이크로앱을 다운로드하고 마운트하기위한 트리거로 리액트의 componetDidMount
를 사용한다.
class MicroFrontend extends React.Component {
render() {
return <main id={`${this.props.name}-container`} />;
}
}
componentDidMount() {
const { name, host } = this.props;
const scriptId = `micro-frontend-script-${name}`;
if (document.getElementById(scriptId)) {
this.renderMicroFrontend();
return;
}
fetch(`${host}/asset-manifest.json`)
.then(res => res.json())
.then(manifest => {
const script = document.createElement('script');
script.id = scriptId;
script.src = `${host}${manifest['main.js']}`;
script.onload = this.renderMicroFrontend;
document.head.appendChild(script);
});
}
고유 id를 가진 관련 스크립트가 다운로드 되었는지 확인 (if문)
다운로드가 안되었다면, 메인스크립트의 전체 URL을 조회하기위해 해당 호스트로부터 asset-manifest.json
을 가져온다. 이 파일에서 스크립트의 URL을 가져와야한다.
스크립트의 URL을 설정하고 난후 onload핸들러를 이용해 Document에 마이크로프론트엔드를 렌더링한다.
renderMicroFrontend = () => {
const { name, history } = this.props;
window[`render${name}`](`${name}-container`, history);
// E.g.: window.renderBrowse('browse-container, history');
};
위 코드를 통해 방금 다운로드한 스크립트에 의해 배치된 전역함수 window.renderBrowse
를 호출한다. 마이크로프론트엔드가 렌더링해야하는 <main>
요소의 ID와 history
객체를 전달한다.
이 전역함수는 컨테이너 앱과 마이크로 앱 사이의 key contract로 통합이 이루어지는곳이다. 그러므로 새로운 마이크로 앱을 가볍게 추가될수 있고, 유지보수가 쉽도록 관리해야한다.
componentWillUnmount() {
const { name } = this.props;
window[`unmount${name}`](`${name}-container`);
}
마이크로 프론트엔드가 DOM에서 제거 될때 관련 마이크로앱도 언마운트 해야한다. 이를 위해 마이크로 프론트엔드 앱이 정의한 해당 전역함수를 생성하고, 적절한 react lifecycle 메소드에서 호출하게 한다
이후 각 마이크로프론트엔드에서 이루어지는 프로세스는 기존 React App의 방식과 동일하다고 보면 된다.
마이크로 프론트엔드 아키텍처를 도입하기위해 총 세가지 아키텍처에 대해 비교분석하고 고민하였음
모놀리식
마이크로서비스
- 토스는 위에서 나열한 모놀리식의 단점때문에 두번째 아키텍처인 마이크로서비스를 검토하게 됩니다.
- 마이크로서비스를 함으로써 모놀리식에 비해 얻을 수 있는 장점은 빌드 시간의 획기적인 감소입니다.
- 모놀리식 : 20분 이상
- 마이크로서비스 : 2~3분(작은 서비스의 빌드 시간)
- 모놀리식 아키텍처의 반대되는 이점이 존재함
- 패키지별로 자유로운 의존성을 선택할 수 있음
- 서비스가 서로 독립적으로 존재하여 영향을 미칠 수 없음
- 서비스별 배포 가능
하지만 이런 이점에도 불구하고 토스는 마이크로서비스의 단점으로 인해 마이크로서비스 도입을 하지않기로 결정함
모노레포
- 그 후 결정된 것이 모노레포
- 모노레포란?
- 하나의 git 레포지토리에 여러 패키지를 담는 것
- 하나의 레포지토리 안에서 모든 서비스와 라이브러리가 관리됨
- 예시
모노레포란 두 개 이상의 프로젝트 코드를 하나의 git 레포지토리에서 관리하는 기법이다.
페이스북이나 구글 마이크로소프트 등에서 사용하고있으며, 일부 인기있는 오픈 소스 프로젝트에서도 monorepo를 쓰고 있다.
모노레포는 모놀리식과 멀티레포의 단점을 보완하고 일부 장점을 섞은 기법