회사 프로젝트에서 타팀의 앱 영역에 우리 팀의 코드가 들어가는 경우가 종종 있다. 현재 회사의 제품은 크게 세가지의 영역으로 나뉠 수 있는데 첫번째, 커머스 상점을 만들 수 있는 에디터이고 두번째, 에디터로부터 만들어지는 커머스 웹사이트의 뼈대, 그리고 마지막 그 상점에 대한 주문/상품/고객 정보를 관리 할 수 있는 어드민이다. 여기서 첫번째와 두번째의 제품을 우리팀에서 만들고 세번째의 제품을 다른팀에서 만든다.

여기서 제품 구조상 우리팀의 관리포인트인 웹사이트 영역의 특정 데이터, 예를들면 웹사이트에 들어갈 기본정보나, 법적필수정보 설정 같은 것들도 상점에 대한 어드민 영역에 들어가게 된다.

이 때, 만약 타팀의 코드에 직접 우리의 코드를 심는다면 관심사의 분리에 실패하게 된다. 타팀의 코드에 결국 우리 팀의 서버 정보, 환경 변수 등등이 함께 들어가게 되기 때문이다. 또한 우리 팀의 코드만 변경사항이 발생해도, 우리 팀의 코드만 배포할 수 없다. 타팀의 코드도 함께 배포해야한다.

원래도 모듈페더레이션을 사용하고있었고, 이번에 맡은 기능이 새로운 앱 구축부터 시작해서, 모듈 페더레이션 연결까지 하게 되었다. 그러면서 자연스럽게 모듈페더레이션은 어떤식으로 동작하는지 궁금해졌다.


Module Federation 이란

먼저 Module Federation은 웹팩에서 지원해주는 기능으로, 여러개의 어플리케이션을 각각 따로 빌드한 후에, 하나의 어플리케이션으로 동작하게 만들어준다. 그래서 말그대로 Module Federation, 모듈 연합을 의미한다. 이전에 모듈은 대체 정확히 뭘까?

Module 이란, 웹팩으로 빌드 할 때 사용하는 코드를 포함 한 모든 리소스를 말한다. 자바스크립트, CSS, HTML, 그 외에도 다양한 asset 파일들이 여기에 포함된다.

그럼 일단 웹팩이 해주는 일을 단순하게 생각해보자. 웹팩은 자바스크립트 코드의 모듈 의존 관계를 파악하여, 실행 할 때 필요한 모듈을 로딩한다. 그런데 일반적으로 생각하면 하나의 웹팩 빌드에 포함된 모듈만 로딩 할 수 있(었)다. 즉, 각각 다른 웹팩 빌드의 결과물로 각각 다른 서버에 배포 되어있는 모듈을 한 곳에서 로딩 할 수는 없(었)다.

이를 개선하는 것이 Module Federation 이다. Module Federation은 단일 웹팩 빌드에 포함된 모듈(이하 로컬모듈)뿐만 아니라, 여러 서버에 배포되어 있는 모듈 (이하 원격 모듈)을 하나의 애플리케이션에서 로딩 할 수 있도록 도와준다.

따라서 A 어플리케이션에 B 어플리케이션의 특정 코드를 삽입하여 A 어플리케이션을 완성시키고 싶을 때, A빌드 컨테이너에서 B빌드 컨테이너를 로딩하여 보여줄 수 있다. (역방향도 물론 가능하며 순환참조도 가능하다.)


어떻게 동작할까? 🤔

일단 원격 모듈을 로딩하는 호스트앱이 있고, 원격 모듈을 내보내주는 리모트앱이 있다고 가정한다.

  1. 원격 모듈을 내보내주는 리모트앱에서는 모듈을 내보내는 설정을 하게 되면, 빌드시에 remoteEntry.js 파일이 생성된다.
  2. 호스트 앱은 리모트 앱의 remoteEntry.js 파일을 로딩한다.

호스트앱의 Webpack 에서 Module Federation 설정을 아래와 같이 해준다.


new ModuleFederationPlugin({
  name: "호스트앱",
  remotes: {
    someApp: `someApp@${리모트앱의 URL}/remoteEntry.js`,
  },
}),

위의 설정은 “호스트앱” 에서 “someApp” 이라는 경로를 리모트앱 URL/remoteEntry.js 로 할게. 즉 “someApp”이라는 전역변수에 저 경로를 지정할게! 라는 뜻이다.

또한 리모트앱에서도 어떤 모듈을 expose 할 지 아래와 같이 설정을 해주어야 한다.

new ModuleFederationPlugin({
  exposes: {
    './Page': './src/Page',
  },
});

그러면 리모트 앱에 생성되는 remoteEntry.js 는 무엇일까?

  • 원격 모듈을 내보내주는 리모트앱에서는 모듈을 내보내는 설정(expose)를 하게 되면, 빌드시에 remoteEntry.js 파일이 생성된다.
  • 말그대로 remoteEntry === remote 진입 파일 이라고 생각하면 된다.
  • 실제 remoteEntry 파일 내부에서 설정한 컨테이너를 expose 하여 moduleMap 객체에 선언하는 구문을 찾을 수 있다.
var moduleMap = {
  './Page': function () {
    return __webpack_require__.e(979).then(function () {
      return function () {
        return __webpack_require__(42979);
      };
    });
  },
};

그리고 실제로 호스트앱에서 설정한 리모트앱의 원격 모듈을 로딩하는 코드는 아래와 같을 것이다.

const RemotePage = React.lazy(() => import('someApp/Page'));
  • 이 때 호스트앱은 someApp 이란 컨테이너를 찾기 위해 전역변수 someApp 을 찾는다.
  • 전역변수 someApp 은 위에서 MF 설정을 통해서 someApp@${리모트앱의 URL}/remoteEntry.js 로 되어있다.
  • 따라서 리모트 앱의 remoteEntry.js 파일을 참조하여 해당 Page 를 가져오게 된다.

그래서 언제 사용 할 수 있을까?

  • 하나의 어플리케이션이 너무 많은 역할을 할 때 관심사 분리의 목적으로 사용 할 수 있다.
  • 즉, 마이크로 프론트엔드 구축을 위해서 모듈 페더레이션 기법을 사용 할 수 있다.

하지만 이 방법이 능사는 아니다. 관심사의 분리가 이루어져도 하나의 앱에 종속된 기능이라면 해당 앱의 특정 컨텍스트를 공유해야 할 수도 있다. (전역 상태/전역 변수 등등) 그럴 때 모듈페더레이션을 사용하여 마이크로프론트엔드를 구현한다면 꽤나 구현이 복잡해질 것이고 결국 호스트앱과 리모트앱의 의존성이 다시 묶여버려서 마이크로프론트엔드의 큰 이점을 달성하지 못하게 된다.

따라서, 모듈페더레이션을 도입하기 전에는 정말로 분리되어야 할 관심사가 맞는지 고민이 필요하다. 분할을 위한 분할은 오히려 여러 이유들로 비용 > 이익이 될 수 있기 때문이다.

만약 위 질문에 대한 답이 YES 이지만, 호스트앱의 데이터를 리모트앱이 알아야 한다면, 애초에 해당 데이터가 호스트앱에 종속되는 구조가 올바른 것인지, 의심하고 해당 데이터에 대한 의존성 설계를 의심할 때라고 생각한다. 어쩌면 마이크로프론트엔드 구축의 가장 큰 장점은 기존 설계와 구조를 의심 할 수 있는 것 자체가 아닐까라고 생각한다.


참고