테크 강연에서 주로 나오던 마이크로 프론트엔드 개념을 정리할겸 작성한 글입니다. ( 아래 레포에 예시도 있으니 참고해주세요. )
마이크로 프론트엔드(Micro Frontend)는 대규모 프론트엔드 애플리케이션을 여러 개의 독립적인 모듈로 나누어 각각의 팀이 독립적으로 개발, 배포, 유지보수할 수 있도록 하는 아키텍처입니다.
위의 그림과 같이 서로 독립된 형태의 프론트엔드 애플리케이션을 통합해 하나의 큰 프론트엔드 애플리케이션으로 구현하는 것입니다.
서로 독립된 형태의 프론트엔드 애플리케이션의 배포 단위를 쪼갤 수 있는데요. 배포 단위를 컴포넌트 단위로 해도되고, 페이지 단위 또는 UI 컴포넌트 별로 쪼갤 수 있습니다.
이로 인해 마이크로 프론트엔드는 유연성, 확장성, 독립적 개발에 도움이 될 수 있습니다. 핵심 특징은 다음과 같습니다.
하지만, 마이크로프론트엔드 방식은 프로젝트 상황을 고려해 도입 시점을 정하는 것이 좋습니다. 서비스를 개발하는데 집중해야할 때 즉, 프로젝트 규모가 작을 때는 가급적이면 도입하지 않는 것을 권장합니다. 단순한 웹사이트에는 오버스택일 수 있는 부분이 있고, 개발 프로세스를 복잡하게 만들 수 있습니다.
어느 정도 윤곽이 잡히고, 프로젝트가 커졌을 때 설계와 유지 관리 리소스가 충분할 때 마이크로 프론트엔드를 도입하는 것이 더 효과적일 것 같습니다.
5가지 방법이 있습니다. (https://martinfowler.com/articles/micro-frontends.html)
1,2 는 페이지 별 배포 단위로 볼 수 있고(빌드 타임에 통합), 3, 4, 5번부터는 페이지 단위 이하로 배포 단위를 분리할 수 있는 방법입니다.
3번은 iframes 관련 보안 문제로 이슈가 있을 수 있을 것이고,
4번, 5번이 비교적 최신 방식이며 동적인 앱이면 런타임 통합 방식에 적합 할 거 같습니다.
런타임 통합을 통해 Web Components를 사용하는 방식은 각 마이크로 프론트엔드를 독립적인 Web Component로 구현하고, 이를 통합 애플리케이션에서 동적으로 로드하여 사용합니다.
상태 공유는 여전히 쉽지 않을 수 있습니다. web-components는 커스텀한 HTML 태그의 형태를 가지고 있어서 속성 값을 통해서만 데이터를 공유 받을 수 있기 때문입니다.
물론 대안은 있지만, iframe 과 웹 컴포넌트의 경우 이미 그것으로 완결되어 런타임에서는 다른 컴포넌트와 상호작용할 필요가 없는 소수의 UI 컴포넌트들을 런타임에 통합시키는데 사용하기 좋은 방식일 거 같습니다. (공통 레이어 header나 navigation 정도..?)
JavaScript를 통한 런타임 통합 방식은 각 마이크로프론트엔드 모듈을 독립적으로 개발하고, 배포하며, 런타임에 JavaScript 코드를 통해 동적으로 통합하는 방법입니다. 이는 다양한 프레임워크나 라이브러리를 사용하여 구현될 수 있으며, 각 마이크로프론트엔드의 독립성과 유연성을 보장합니다.
이를 구현하기 위한 여러가지 방안이 있습니다.
Javascript로 된 Framework를 사용하고 있는 입장에서는 자바스크립트 런타임 통합방식이 적합해보이는데요. 이를 구현하는 방식 중 주로 웹팩으로 빌드를 하고 있기때문에 Webpack > Module Federation 에 대해 알아보도록 하겠습니다.
하나의 앱을 독립적인 배포가 가능한 모듈 단위(webpack 에서의 청크)로 나누어 브라우저 런타임에 합체시키는 개념입니다. ( micro frontend 를 구현하는 방법 중 하나 )
- webpack5 의 새로운 기능입니다.
- 양방향(Bidirectional)으로 Module Federation이 가능하다. 예를 들어 A 빌드에서 B 빌드에 있는 코드를 실행시킬 수 있고, B빌드에서도 A빌드에 있는 코드를 실행시킬 수 있다는 것이다.
- Code Splitting 과 비슷하지만, Module Federation은 별도의 Webpack 애플리케이션의 기능을 독립된 애플리케이션으로 분리할 수 있게 해줍니다. 덕분에 개별적으로 개발하고 배포할 수 있게 되어 유연성과 확장성을 높일 수 있습니다.
다른 마이크로 앱에서 로드 가능한 단위입니다. A앱을 Host에서 불러와 쓸 수 있다면, ‘A앱은 Container를 포함하고 있다’ 라고 말할 수 있습니다.
new ModuleFederationPlugin({
name: 'microApp',
filename: 'remoteEntry.js',
// exposes 설정을 가지고 있다면 conatiner를 포함한다.
exposes: {
'./AppA': './src/AppA.tsx',
'./AppB': './src/AppB.tsx',
},
}),
import 구현체 from '{container 이름}/{exposes설정값의 key}';
// 정적 import
import MicroAppA from 'microApp/AppA';
특정 앱에서 다른 마이크로 앱을 Import 할 때 만들어지는 참조 관계 입니다. . A 앱에서 B 앱을 import해서 쓰고 있다면 “A앱은 B앱에 대한 Container Refrences가 존재한다.”고 말할 수 있습니다.
위의 그림에서는 “Host” 영역으로 보시면 됩니다.
new ModuleFederationPlugin({
name: 'microAppA',
// remotes 설정을 가지고 있다면 conatiner reference를 포함한다.
remotes: {
microAppB: 'microAppB@http://localhost:3002/remoteEntry.js',
},
}),
공유되는 의존성이 유효한 하나의 scope입니다. 마이크로 앱간 의존성 공유를 할 때 공유 의존성에 대한 설정, 버전을 체크하고 이를 설정한 바에 맞게 각 Micro App에서 로드해서 쓰는 방식을 결정하게 하는 역할을 합니다.
new ModuleFederationPlugin({
shared: {
// react를 default shared scope 범위에서 공유 모듈로 추가합니다.
react: {
requiredVersion: deps.react,
singleton: true,
shareScope: 'default'
},
},
}),
최적화할 수 있는 방안에 대한 옵션들이 많기 때문에 이는 다음에 알아보도록 하자..
module.exports = {
...,
plugins: [
...,
new ModuleFederationPlugin({
// 원격 모듈 이름
name: "microfrontend1",
filename: "remoteEntry.js",
// ./App이라는 path로 './src/App' 를 expose 함
exposes: {
"./App": "./src/App",
},
shared: {
react: {
eager: true,
singleton: true,
requiredVersion: "^18.2.0",
},
"react-dom": {
eager: true,
singleton: true,
},
},
}),
]
}
module.exports = {
...,
plugins: [
...,
new ModuleFederationPlugin({
name: "app_shell",
remotes: {
microfrontend1: "microfrontend1@http://localhost:3001/remoteEntry.js",
},
shared: {
react: {
eager: true,
singleton: true,
requiredVersion: "^18.2.0",
},
"react-dom": {
eager: true,
singleton: true,
requiredVersion: "^18.2.0",
},
},
}),
]
}
module.exports = {
...,
plugins: [
...,
new ModuleFederationPlugin({
...,
shared: {
react: {
eager: true, // 추가!
singleton: true,
requiredVersion: "^18.2.0",
},
},
})
]
}