Module Federation

작년 초에 Module Federation을 적용한 작은 서비스를 개발했습니다.
이 글은 그때의 경험을 바탕으로 Module Federation를 사용하는 이유, 단점, 사용하며 느낀 점 등을 다룹니다.

1. Micro-Frontend and MSA

Micro-Frontend

MSA(Microservice Architecture)는 이제 Backend에서 친숙한 단어입니다.
MSA는 여러 개의 작은 서비스를 모아 하나의 앱을 구현하는 방식입니다.
작은 단위로 유지되는 각각의 서비스는 독립적으로 개발, 운영되기 때문에 유지보수, 짧은 배포 시간 등 여러 면에서 장점이 있습니다.

Micro-Frontend는 이러한 장점을 Frontend에도 도입하고자 하는 시도입니다.
기존의 Frontend 구조는 컴포넌트 단위로 코드를 분리하긴 하지만 결국엔 하나의 커다란 앱이란 한계가 있습니다.
그래서 기능이 추가될수록 코드의 양은 늘어가고, 배포 시간도 증가합니다.
이런 문제들은 Monolith 구조를 가진 Backend 앱의 문제와 동일합니다.
Micro-Frontend는 Backend의 MSA처럼 작은 배포 단위를 유지할 수 있도록 Frontend의 코드를 나누려고 합니다.

2. Module Federation

Module Federation은 개별적으로 build 된 JavaScript 코드(= Federated Module)를 비동기 방식으로 불러와서 사용할 수 있게 합니다.
Module Federation은 Micro-Frontend만을 위해 개발된 것은 아니지만 해당 기능을 활용하면 Micro-Frontend를 간편하게 구현할 수 있습니다.

일반적으로 Frontend에서 중복되는 코드는 별도의 Component로 분리하여 사용합니다.
그리고 이런 Component가 여러 개 생기고, 많아지면 관련 있는 Component끼리 묶어서 라이브러리로 만들기도 합니다.
코드를 분리한다는 측면에서 Component, 라이브러리와 Module Federation은 비슷합니다.
그러나 Module Federation은 배포 단위를 분리하고, 버전 관리의 피로도를 줄이며, 프로젝트의 크기는 계속 작게 유지할 수 있다는 점에서 차이가 있습니다.

예를 들어 Container 프로젝트와 Search, Order라는 프로젝트가 있고, Container 프로젝트가 다른 두 프로젝트를 포함해서 화면을 표현할 경우 package.json은 다음과 같이 구성할 수 있습니다.

Search, Order를 사용하는 Container

이 형태는 코드가 분리되어 있고, 독립적인 프로젝트에서 각각 코드를 빌드를 합니다. 이런 걸 Micro-Frontend라고 하는 걸까요?
사실 이 구조는 몇 년 전 신입 개발자끼리 사내 프로젝트를 진행할 때 채택했던 구조입니다.
개발 당시에는 아무 생각없이 사용했지만 이후 유지보수를 할 때 불편한 점이 드러났습니다.
Search 프로젝트에서 수정 작업을 하여 버전을 올려야 한다면 빌드를 2번해야 했습니다.
코드 작업이 이루어진 Search 1번, 그리고 Search를 사용하는 Container 1번.

Search와 함께 바뀌는 Container

예시에서는 2개의 서브 프로젝트만 있으므로 큰 어려움이 없습니다.
하지만 서브 프로젝트가 많아질수록 여러 문제가 발생합니다.
Module Federation을 적용하면 이런 문제들을 해결할 수 있습니다.


3. Module Federtion 프로젝트 구성 예시

Module Federation의 특징을 체감하기 위해서는 역시 직접 프로젝트를 만들어 보는 게 좋습니다.
Module Federation을 이용해서 간단하게 앱을 만들어 보겠습니다.
개발 환경은 다음과 같습니다.

  • Node: 18.12.1
  • Yarn: 3.2.4(Yarn berry)
  • nextjs: 13.0.7
  • react:18.2.0
  • webpack: 5.75.0
  • @module-federation/nextjs-mf: 5.12.9
  • typescript: 4.8.3

프로젝트는 Host, Remote 2개이며 각 프로젝트에 대한 설명은 다음과 같습니다.

  • Host 프로젝트는 NextJS로 만들었습니다. ( create-next-app 사용 X )

  • Remote 프로젝트는 React로 구성했습니다. ( create-react-app 사용 X )

  • Host 프로젝트는 Remote 프로젝트의 “SimpleButton”, “ComplexButton” 컴포넌트를 불러와서 화면에 보여줍니다.

  • Remote 프로젝트는 “SimpleButton”, “ComplexButton”을 구현한 프로젝트입니다.
    “SimpleButton”은 클릭하면 Hello from Remote App이라는 로그를 남깁니다.
    “SimpleButton”는 prop를 이용해서 버튼의 글을 수정할 수 있습니다.
    “ComplexButton”은 클릭하면 useState를 이용하여 왼쪽의 숫자를 1씩 늘립니다.

프로젝트의 주요 설정은 다음과 같습니다.
Module Federation의 자세한 설정 방법은 공식 문서에 다양한 예시와 함께 설명되어 있습니다.

Host의 next.config.js


Remote의 webpack.config.js


Remote의 SimpleButton, ComplexButton 코드


Host에서 SimpleButton, ComplexButton을 사용하는 코드


완성된 화면


간단하게 화면을 구성했습니다.
“SimpleButton”과 “ComplexButton”가 정상적으로 동작합니다.

4. Module Federation 동작 방식

Module Federation을 소개하며 그 특징으로 “개별적으로 build 된 JavaScript 코드(= Federated Module)를 비동기 방식으로 불러와서 사용”한다는 점을 말했습니다.
이에 대해 조금 더 자세히 알아보겠습니다.
Module Federation을 구현하는 ModuleFederationPlugin은 이미 코드가 공개되어 있습니다.
내부 코드를 살펴보면 정확한 동작 방식을 알 수 있겠지만 시간이 오래 걸릴 것 같으니 간접적으로 알아보겠습니다.

1) Remote 호출하기

런타임에 Host가 Remote의 컴포넌트를 사용하기 위해서는 Remote의 빌드 결과물(output)에 접근해야 합니다.
Host는 어떻게 Remote의 위치를 알 수 있을까요?
공식 문서는 Remote로의 접근 경로를 plugin의 옵션에 직접 작성하지만 Host 프로젝트의 경우 script 코드를 넣어서 직접 Remote의 위치를 알려주었습니다.
Host의 next.config.js에 Remote의 URL이 없던 이유이기도 합니다.
“src” 경로는 Remote 프로젝트에 있는 webpack의 publicPath 옵션과 ModuleFederationPlugin의 filename 옵션의 값입니다.

2) Remote 컴포넌트 내용

Host를 띄운 브라우저의 개발자 도구 Network탭


다음은 Host가 Remote의 코드를 어떻게 전달받는지 입니다.
위의 사진은 개발자 도구 Network 탭을 캡쳐한 사진입니다.
몇몇 요청의 응답을 살펴보겠습니다.
아래의 사진은 RemoteApp.js 요청의 응답입니다.

RemoteApp.js


굉장히 복잡한 코드지만 정리하면 아래의 사진과 같습니다.

RemoteApp.js를 정리한 코드


다음은 src/components_index_ts.js 요청의 응답을 정리한 코드입니다.

src/components_index_ts.js를 정리한 코드


여러 요청의 응답을 살펴보니 전부 Remote 프로젝트에서 생성한 “SimpleButton”, “ComplexButton” 컴포넌트의 코드입니다.
몇몇 부분에서는 ModuleFederationPlugin에서 설정한 옵션(exposes, share)과 관련된 코드도 확인할 수 있습니다.

3) Host 적용

Host 프로젝트는 import를 이용해서 Remote의 컴포넌트를 불러오고 있습니다.
아래의 사진은 그 과정을 간단하게 요약한 사진입니다.

Federated Module Import 과정

그리고 이 코드를 Host의 코드에 대입하면 아래처럼 표현할 수 있습니다.

Host 코드를 대입한 모습

5. 정리

지금까지의 과정을 정리하면 다음과 같습니다.
Host 프로젝트는 미리 지정된 URL로 Remote 프로젝트의 컴포넌트 코드를 요청합니다.
Remote 프로젝트에서 불러온 코드는 plugin의 설정을 이용해서 전역 변수를 생성합니다.
그리고 이 전역 변수 내부에 “exposes” 설정에 명시된 컴포넌트들을 담습니다.
Host는 이렇게 전역 변수에 담긴 Remote의 컴포넌트를 읽어서 사용합니다.

6. Module Federation의 단점, TypeScript

요즘 React 프로젝트는 주로 타입스크립트를 이용해서 개발합니다.
타입스크립트와 React를 이용해서 개발하면 컴포넌트의 prop 목록을 IDE의 자동완성 기능으로 알 수 있습니다.
이 글에서의 Host, Remote 프로젝트 역시 타입스크립트로 구성했습니다.
그런데 Module Federation을 이용해서 읽어온 Remote의 컴포넌트에서는 이런 기능을 사용할 수 없습니다.
읽어온 코드가 자바스크립트라서 그럴까요?
그래서 어쩔 수 없이 아래처럼 Host에서 Remote 컴포넌트의 타입을 선언해야 합니다.

Host 프로젝트의 types.d.ts 파일

7. 글을 마치며

현재 제가 속해 있는 Module Federation 서비스는 규모가 작아서 2명씩 짝지어진 3개의 팀이 관리하고 있습니다.
각 팀은 각각의 화면과 서버까지 전담해서 완전히 독립된 상태이고 덕분에 Micro-Frontend의 장점을 톡톡히 누리고 있습니다.
특정 프로젝트의 경우 빌드 시간이 극단적으로 짧아졌으며 굳이 팀별로 공통된 구조, 기술을 사용할 필요가 없어서 팀별로 익숙한 방식을 따라 개발합니다.
팀이 서로 독립적으로 움직이니 불필요한 회의와 의사결정 과정이 없어진 것도 큰 장점입니다.
Module Federation의 개발자는 Module Federation을 “A game-changer in JavaScript architecture”라고 표현했습니다.
실제로 사용해보니, 전혀 과장된 말이 아닌 듯합니다.

404

참고자료

l Module Federation | webpack

l webpack/ModuleFederationPlugin.js at main · webpack/webpack · GitHub

l module-federation-examples/nextjs-react at master · module-federation/module-federation-examples · GitHub

l Micro Frontends - extending the microservice idea to frontend development (micro-frontends.org)

l @module-federation/nextjs-mf - npm (npmjs.com)

l Module Federation (module-federation.github.io)

l Webpack Module Federation 도입 전에 알아야 할 것들 | 카카오엔터테인먼트 FE 기술블로그 (kakaoent.com)

l Zack Jackson - Module Federation - YouTube

l Five Module Federation/Micro-Frontend Mistakes - YouTube