들어가며
과거에 비하여 브라우저 화면에 복잡하고 다양한 기능들을 요구하게 되었다. 이로 인해 서비스의 규모가 점점 커지고 종속성이 생기면서 불필요한 비용들이 발생하게 되었고 백엔드의 마이크로 서비스(MSA)처럼 여러 서비스를 분리해서 개발, 관리하고자 하는 움직임이 생기게 되었다. 이런 요구들을 해소하기 위해 생겨난 마이크로 프론트엔드 아키텍처에 대해 소개하고 Webpack5 Module Federation 기술을 예제를 통해 설명한다.
마이크로 프론트엔드
마이크로 프론트엔드는 마이크로 서비스(MSA)를 프론트엔드에 적용한 것으로 전체 화면을 작동할 수 있는 단위로 나누어 개발한 후 서로 조립하는 방식의 패턴을 말한다.
장점
- 독립적인 개발 및 배포가 가능
- 각 마이크로 앱은 다른 마이크로 앱의 기술 스펙에 영향을 받지 않음
- 작고, 응집력 있는 유지보수성을 가지는 코드 베이스를 가질 수 있음
- 기능 확장의 용이함
단점
- 기능 및 스타일 중첩에 대한 위험
- 마이크로 앱별로 중복된 종속성이 여러 번 로드되거나 네트워크 트래픽 증가로 성능저하
- 데이터 공유와 상태 관리의 어려움
- 여러 앱으로 나누어짐에 따른 운영 관리가 복잡해짐
통합 방법
마이크로 프론트엔드는 마이크로 앱들을 나눠서 개발하고 그것을 하나로 보여주기 위해 컨테이너 앱을 통해 통합해야 한다. 여기서 컨테이너 앱이란 각 화면을 조합하는 앱을 말하여 그 하부에 들어가는 단위 앱을 마이크로 앱이라고 한다.
컨테이너 앱으로 통합하는 다양한 방법이 존재하며 대표적인 5가지 방식이 있다.
- SSR식 통합: 각 서버 별로 html을 요청해서 최종 응답 서버와 결과물들을 하나로 합쳐서 내려주는 방법으로 서버 측에서 최종 화면을 조합
- 빌드 타임 통합: 마이크로 앱을 패키지로 배포하고 컨테이너 애플리케이션에서 라이브러리 디펜던시에 포함시키는 방식으로 디펜던시가 존재하기 때문에 빌드 시간 증가 가능
- iframe을 통한 런타임 통합: 각 요소 부분을 iframe으로 구분하고 해당 요소마다 별도의 URL를 호출하여 화면을 조합하는 방식으로 앱간 통신 등이 복잡해질 수 있음
- Javascript를 통한 런타임 통합: 각각의 소스를 javascript 파일로 생성하여 컨테이너 파일에서 로딩을 한 뒤 각각의 마이크로 앱 진입 시점에 렌더링하는 방식
- web component를 통한 런타임 통합: Javascript를 통한 런타임 통합방식과 비슷하지만 전역 함수를 컨테이너에서 호출하는 것과는 달리 커스텀 엘리먼트를 사용하는 방식
Module Federation
Javascript 앱을 위한 정적 모듈 번들러인 Webpack이 2020년 10월 Webpak5 버전에 공식적으로 릴리즈한 기능으로 여러 분리된 빌드들을 이용하여 하나의 앱을 통합하여 구성할 수 있도록 도와준다. 개별 앱들은 독립적으로 배포되어 실행될 수 있기 때문에 마이크로 프론트엔드를 쉽게 구현해 볼 수 있으며 마이크로 프론트엔드의 장단점 또한 경험할 수 있을 것으로 기대된다. 배포된 앱은 런타임에 동적으로 특정 앱에 노출된 모듈 코드를 불러와 실행할 수 있게 해준다. 다른 앱들에게 노출된 모듈을 공유하고 공유 받을 수 있는 구조를 제공하며 사용하려는 의존성 패키지 또한 서로 공유하여 사용 가능하다.
- 모듈: Webpack으로 빌드할 때 사용하는 코드를 포함한 모든 리소스
- Host: Module Federation을 통해 다른 앱을 동적으로 사용하는 앱
- Remote: Module Federation을 통해 노출되는 앱
- Expose: Remote가 노출할 컴포넌트 같은 모듈
앱간 Host와 Remote를 정하고, Remote 모듈을 Expose하는 방식으로 사용하며 단 방향뿐만 아니라 양방향으로 모듈 노출이 가능하다.
이제부터는 React를 사용하여 컨테이너 앱(Container)에서 마이크로 앱(App1)에서 제공한 버튼 컴포넌트를 사용하는 간단한 앱을 만들어보는 예제를 통해 Webpack 설정과 동작을 이해해본다.
각 앱 별로 개별 화면 구현하기
App1에서는 버튼 컴포넌트(Button.js) 를 구현하고 Container에서는 Webpack 설정이 되었다고 가정 되어있는 상태에서 App1에서 제공한 타이틀과 Button 하나를 보여주는 화면(App.js)을 구현 한다. 참고로 App1 자체에서도 독립적으로 마이크로 앱의 실행 가능하다. 그러기 위해서는 App1의 bootstrap.js, App.js 파일을 보면 독립적으로 실행해서 하면을 보여주기 위해 루트 DOM 노드를 하나 만드는 것을 볼 수 있으며 http://localhost:3002에 접속하면 App1만 실행된 화면을 볼 수 있다.
Webpack Config 설정하기
동적으로 빌드된 모듈을 제공받고 사용하기 위한 Module Federation 환경을 만들기 위해서 Webpack에서는 환경설정 파일인 webpack.config.js의 plugins부분에 ModuleFederationPlugin을 추가하고 Remote/Host 앱은 각각 세부 내용을 기술해야 한다.
- name: 앱의 이름으로 유니크해야 함
- filename: 매니페스트 파일의 이름 지정 (디폴트 파일명: remoteEntry.js)
- expose: 노출될 모듈 정의
- shared: 런타임에 federated된 앱 간에 공유하거나 공유 받을 의존성 패키지 정의
- singleton: 해당 패키지를 공유할 때 항상 이미 생성된 단일 인스턴스를 공유
- requiredVersion: 해당 패키지가 필요로 하는 버전을 명시
- remotes: 앱이 사용할 Remote 정의
- [host에서 사용할alias]: `[remote name]@//[remote host]:[remote port]/[remoteEntry name]`
빌드 파일 살펴보기
Expose할 모듈이 있는 App1 빌드하면 remoteEntry.js 파일과 Button 컴포넌트 번들 파일이 생성된다. remoteEntry.js는 App1 webpack.config.js에 정의된 ModuleFederationPlugin의 filename에 명시된 이름으로 만들어지며 코드는 포함되어있지 않는다. Expose된 모듈들의 레퍼런스와 shared 의존성 패키지에 대한 정보를 담고 있는 매니페스트 데이터들만 가지고 있는 파일로 모듈을 공유할 수 있도록 하는데 중요한 역할을 한다.
다음으로 Expose 된 대상을 사용하려는 쪽인 Container를 빌드하면 앱에 대한 메인 번들 파일이 생성된다. 메인 번들 파일(main.js)의 일부를 보면 Container webpack.config.js에 기술한 내용을 바탕으로 App1에서 가지고 있는 remoteEntry.js를 받아올 수 있도록 하기 위한 코드와 Shared 의존성 패키지 정보에 대한 정보가 있는 것을 확인할 수 있다.
동작 이해하기
Container와 App1의 번들 파일을 각각 배포한 상태에서 브라우저에서 컨테이너 앱으로 접속(http://localhost:3001)하면 메인 번들 파일인 main.js 로드 된다. main.js는 App1에서 가지고 있는 remoteEntry.js 정보가 전역변수에 있는지 확인하고 정보가 없다면 App1에게 remoteEntry.js 를 요청한다. App1의 remoteEntry.js 내용을 로드 후 Container는 전역변수에 메타 정보를 저장한다. 또한 Shared 정보도 설정에 따라 필요한 의존성 패키지를 App 중에 한번만 로드되어지도록 요청한다. Module Federation을 사용하기 위한 초기 정보 셋팅이 완료되면 Container에서 App1의 Button 컴포넌트가 “<리모트앱 name>/모듈명” 사용되는 시점(lazy loading)이 되면 초기 저장했던 App1의 메타 정보를 이용하여 button.js 번들 파일을 App1에 요청을 한다. App1을 통해 button.js 번들 파일이 로드가 되고 화면에 출력된다.
정리하면 사용하려는 쪽 화면 첫 진입 시 노출된 모듈이 있는 마이크로 앱의 매니페스트 파일들을 통해 메타 정보를 전역에 저장하고 노출된 모듈이 사용되는 시점에 저장된 전역 정보를 이용해 관련 빌드 파일을 받아와 화면을 구성하는 식으로 동작된다.
아래 이미지는 크롬에서 Container 앱을 접속했을 때 화면과 개발자도구의 네트워크 탭을 이용하여 다운로드 된 javascript 파일 목록이다. 내용을 확인해보면 Container와 Host 관련 파일들이 다운로드 되어 브라우저에서 실행된 것을 확인할 수 있다.
마치며
Module Federation은 독립적인 개발 및 빌드, 배포가 가능한 마이크로 프론트엔드를 수월하게 개발하기 위한 기술이 될 것으로 예상하며 마이크로 프론트엔드를 개발하면서 고려해야 했던 부분들을 쉽게 해결해준다.
첫 번째로 마이크로 앱들을 통합하는 일을 프로젝트 상황에 맞게 개별적으로 구현해야 했다면 이제는 Webpack을 사용하고 추가 설정만 하면 쉽게 통합 구조를 만들 수 있기 때문에 통합방법에 대한 고려와 개발시간이 단축된다.
두 번째로 같은 프론트엔드 프레임워크(React, Vue 등등)를 사용하더라도 마이크로 앱들 간 store, router, i18n, axios의 공통 에러 처리 등을 데이터를 통합 처리하는 것이 쉽지 않다. 하지만 Module Federation의 Shared 설정을 통한 패키지 객체의 공유는 이런 이슈를 쉽게 해결해준다. 마이크로 앱들 간에 같은 프론트엔드 프레임워크를 사용하는 곳에서 Module Federation을 사용하면 개발이 용이할 것으로 생각된다.
하지만 반대로 불편했던 점도 존재한다. 에러 났을 경우 에러의 원인이 여러 마이크로 앱으로 분산되어 있기 때문에 원인을 찾는 디버깅에 대한 어려움이 존재하고 그 외 여러 가지 고려해야 될 이슈들에 대한 아직까지 많은 레퍼런스들을 찾기 쉽지 않다.
그렇기 때문에 해당 기술을 사용하고 싶다면 간단한 프론트엔드 앱에서부터 시작해서 문제를 해결하며 기술을 습득하길 바란다.
다른 관련 없는 모듈들 간의 종속성 등에서 분리하여 독립적으로 앱들을 유지보수하고 싶고 마이크로 프론트엔드를 쉽게 만들어보고 싶은 개발자들에게 Module Federation 적용은 좋은 시도이며 발전된 경험을 축적해 볼 수 있는 좋은 선택지가 될 수 있을 것이다.
# References
- https://martinfowler.com/articles/micro-frontends.html#TheMicroFrontends
- https://www.kimcoder.io/blog/micro-frontend-module-federation
- https://feed.hoondev.com/articles/webpack/webpack-5-module-federation-for-micro-frontends
- https://mobicon.tistory.com/572
- Module Federation의 컨셉과 작동 원리 이해하기 (maxkim-j.github.io)
- https://webpack.js.org/plugins/module-federation-plugin/
김수이 프로
소프트웨어사업부 플랫폼사업팀
에스코어 개발플랫폼그룹에서 프론트엔드 개발을 담당하고 있습니다.
Register for Download Contents
- 이메일 주소를 제출해 주시면 콘텐츠를 다운로드 받을 수 있으며, 자동으로 뉴스레터 신청 서비스에 가입됩니다.
- 뉴스레터 서비스 가입 거부 시 콘텐츠 다운로드 서비스가 제한될 수 있습니다.
- 파일 다운로드가 되지 않을 경우 s-core_mktg@samsung.com으로 문의해주십시오.