MFA 마이크로 프론트엔드 찍먹기

Gee·2024년 8월 10일
1
post-thumbnail
post-custom-banner

테크 강연에서 주로 나오던 마이크로 프론트엔드 개념을 정리할겸 작성한 글입니다. ( 아래 레포에 예시도 있으니 참고해주세요. )

마이크로프론트엔드 개념과 특징

마이크로 프론트엔드(Micro Frontend)는 대규모 프론트엔드 애플리케이션을 여러 개의 독립적인 모듈로 나누어 각각의 팀이 독립적으로 개발, 배포, 유지보수할 수 있도록 하는 아키텍처입니다.

위의 그림과 같이 서로 독립된 형태의 프론트엔드 애플리케이션을 통합해 하나의 큰 프론트엔드 애플리케이션으로 구현하는 것입니다.

서로 독립된 형태의 프론트엔드 애플리케이션의 배포 단위를 쪼갤 수 있는데요. 배포 단위를 컴포넌트 단위로 해도되고, 페이지 단위 또는 UI 컴포넌트 별로 쪼갤 수 있습니다.

이로 인해 마이크로 프론트엔드는 유연성, 확장성, 독립적 개발에 도움이 될 수 있습니다. 핵심 특징은 다음과 같습니다.

  1. 독립된 팀에 의해 개발되고, 자체적인 빌드 및 배포 파이프라인을 가질 수 있고 사용하는 기술, 언어, 라이브러리 또한 자유롭게 선택할 수 있습니다.
  2. 여러 마이크로 프론트엔드가 하나의 애플리케이션으로 무리 없이 통합되어, 사용자에게 일관된 경험을 제공합니다. 이를 위해 공통된 디자인 시스템과 API를 사용할 수 있습니다.

하지만, 마이크로프론트엔드 방식은 프로젝트 상황을 고려해 도입 시점을 정하는 것이 좋습니다. 서비스를 개발하는데 집중해야할 때 즉, 프로젝트 규모가 작을 때는 가급적이면 도입하지 않는 것을 권장합니다. 단순한 웹사이트에는 오버스택일 수 있는 부분이 있고, 개발 프로세스를 복잡하게 만들 수 있습니다.

어느 정도 윤곽이 잡히고, 프로젝트가 커졌을 때 설계와 유지 관리 리소스가 충분할 때 마이크로 프론트엔드를 도입하는 것이 더 효과적일 것 같습니다.

마이크로 프론트엔드 방식은 어떻게 구현할 수 있을까요 ?

5가지 방법이 있습니다. (https://martinfowler.com/articles/micro-frontends.html)

  1. Server-side template Composition
  2. Build-time Integration
  3. Run-time Integration via iframes ( 보안문제 )
  4. Run-time Integration via web-components
  5. Run-time Integration via javascript

1,2 는 페이지 별 배포 단위로 볼 수 있고(빌드 타임에 통합), 3, 4, 5번부터는 페이지 단위 이하로 배포 단위를 분리할 수 있는 방법입니다.

3번은 iframes 관련 보안 문제로 이슈가 있을 수 있을 것이고,

4번, 5번이 비교적 최신 방식이며 동적인 앱이면 런타임 통합 방식에 적합 할 거 같습니다.

Run-time Integration via Web Components

런타임 통합을 통해 Web Components를 사용하는 방식은 각 마이크로 프론트엔드를 독립적인 Web Component로 구현하고, 이를 통합 애플리케이션에서 동적으로 로드하여 사용합니다.

상태 공유는 여전히 쉽지 않을 수 있습니다. web-components는 커스텀한 HTML 태그의 형태를 가지고 있어서 속성 값을 통해서만 데이터를 공유 받을 수 있기 때문입니다.

물론 대안은 있지만, iframe 과 웹 컴포넌트의 경우 이미 그것으로 완결되어 런타임에서는 다른 컴포넌트와 상호작용할 필요가 없는 소수의 UI 컴포넌트들을 런타임에 통합시키는데 사용하기 좋은 방식일 거 같습니다. (공통 레이어 header나 navigation 정도..?)

Run-time Integration via javascript (✅)

JavaScript를 통한 런타임 통합 방식은 각 마이크로프론트엔드 모듈을 독립적으로 개발하고, 배포하며, 런타임에 JavaScript 코드를 통해 동적으로 통합하는 방법입니다. 이는 다양한 프레임워크나 라이브러리를 사용하여 구현될 수 있으며, 각 마이크로프론트엔드의 독립성과 유연성을 보장합니다.

이를 구현하기 위한 여러가지 방안이 있습니다.

  • SystemJS를 사용해 구현하기
  • Import Maps를 사용해 구현하기
  • Webpack > Module Federation 사용하기 ✅

Javascript로 된 Framework를 사용하고 있는 입장에서는 자바스크립트 런타임 통합방식이 적합해보이는데요. 이를 구현하는 방식 중 주로 웹팩으로 빌드를 하고 있기때문에 Webpack > Module Federation 에 대해 알아보도록 하겠습니다.

Module Federation

하나의 앱을 독립적인 배포가 가능한 모듈 단위(webpack 에서의 청크)로 나누어 브라우저 런타임에 합체시키는 개념입니다. ( micro frontend 를 구현하는 방법 중 하나 )

  • webpack5 의 새로운 기능입니다.
  • 양방향(Bidirectional)으로 Module Federation이 가능하다. 예를 들어 A 빌드에서 B 빌드에 있는 코드를 실행시킬 수 있고, B빌드에서도 A빌드에 있는 코드를 실행시킬 수 있다는 것이다.
  • Code Splitting 과 비슷하지만, Module Federation은 별도의 Webpack 애플리케이션의 기능을 독립된 애플리케이션으로 분리할 수 있게 해줍니다. 덕분에 개별적으로 개발하고 배포할 수 있게 되어 유연성과 확장성을 높일 수 있습니다.

주요 개념

Container

다른 마이크로 앱에서 로드 가능한 단위입니다. A앱을 Host에서 불러와 쓸 수 있다면, ‘A앱은 Container를 포함하고 있다’ 라고 말할 수 있습니다.

  • 1개 이상의 Exposed Module 을 가지고 있는 Module Federation Plugin의 단위 입니다.
  • Remote Container or Remote App 이라고 말할 수 있습니다.
    • 현재 빌드의 일부가 아닌, 원격 컨테이너에서 런타임에 로드되는 모듈입니다.
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';

Container References

특정 앱에서 다른 마이크로 앱을 Import 할 때 만들어지는 참조 관계 입니다. . A 앱에서 B 앱을 import해서 쓰고 있다면 “A앱은 B앱에 대한 Container Refrences가 존재한다.”고 말할 수 있습니다.

위의 그림에서는 “Host” 영역으로 보시면 됩니다.

  • 1개 이상의 Container를 Import 하고 있는 Module Federation Plugin의 단위 입니다.
  • Host App이라고도 불립니다.
new ModuleFederationPlugin({
    name: 'microAppA',
    // remotes 설정을 가지고 있다면 conatiner reference를 포함한다.
    remotes: {
       microAppB: 'microAppB@http://localhost:3002/remoteEntry.js',
    },
  }),

Share Scope

공유되는 의존성이 유효한 하나의 scope입니다. 마이크로 앱간 의존성 공유를 할 때 공유 의존성에 대한 설정, 버전을 체크하고 이를 설정한 바에 맞게 각 Micro App에서 로드해서 쓰는 방식을 결정하게 하는 역할을 합니다.

new ModuleFederationPlugin({
    shared: {
      // react를 default shared scope 범위에서 공유 모듈로 추가합니다.
      react: {
        requiredVersion: deps.react,
        singleton: true,
        shareScope: 'default'
      },
    },
  }),
  1. 런타임에 로드 되는 Micro App의 빌드 타임에 공유 의존성의 모듈 (webpack module)을 별도 청크로 분리해 번들링합니다.
  2. 런타임에서 Micro App 이 로드 될 때, 해당 공유 의존성을 사용하면서 먼저 로드되는 앱의 런타임에 미리 분리된 청크를 로드해서 의존성을 사용하게 합니다.
  3. 이어 로드되는, 해당 공유 의존성을 사용하는 다른 Micro App에서 먼저 불러온 공유 의존성은 따로 로드하지 않고 이미 로드된 청크에서 의존성을 사용할 수 있게 합니다.
  4. 만약에 먼저 불러온 공유 의존성이 로드된 Micro App에서 사용한다고 정의한 의존성과 버전, share scope가 일치하지 않는 경우 해당 Micro App은 자신의 빌드 번들에서 공유 의존성에 대한 번들을 꺼내서 씁니다.

최적화할 수 있는 방안에 대한 옵션들이 많기 때문에 이는 다음에 알아보도록 하자..

내보낼 모듈

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",
          },
        },
      }),
	
	
	]
}

맞이했던 문제

  1. module output publicPath 미지정
    1. remote App 의 호출 주소가 현재 화면을 기준으로 module 을 호출하고 있었다.
    2. remote, host 모두 publicPath 를 지정해주었다.
  2. CRA로 셋팅한 프로젝트의 webpack을 건들기위해 다른 라이브러리를 사용했는데, 꽤나 귀찮았다..
    1. 기본으로 react + webpack + babel 설정하는 법을 다시 익혀야할 거 같습니다..
  3. 런타임에 import 한 Module 을 불러오지 못하는 이슈 ( 의존성 비동기 문제 )
    1. 의존성 모듈을 먼저 불러오고 화면 렌더링을 해야했는데 의존성 모듈을 먼저 불러오지 못한 이슈로 인해 중간 레이어 파일을 두고 랜더링하는 방식으로 변경
    2. 하지만, 이렇게 구성하는 것은 module federation 으로 인한 화면 파일이 하나 더 생기는 것이라고 생각해서 다른 방안을 고안하게 됨
    3. webpack.config.js 내부의 옵션을 통해 설정
    4. Dependency 를 동기적으로 제공, 청크가 처음 불러와지는 순간에 의존성 모듈을 밀어넣어서 같이 불러와지도록 한다. ( 비동기적으로 다운로드 하지 않음 )
      • 이렇게 할 경우, 청크 파일이 커지기 때문에 Shell (Host)에서만 제공하는 것을 추천합니다.
module.exports = {
	..., 
	plugins: [
		...,
		 new ModuleFederationPlugin({
				 ...,
        shared: {
          react: {
            eager: true, // 추가! 
            singleton: true,
            requiredVersion: "^18.2.0",
          },
        },
      })
	]
}

알려진 문제

  1. Typescript를 지원하지 않음
  2. 패키지 구조가 복잡해짐

참고 자료

profile
작은 실패, 빠른 피드백, 다시 시도
post-custom-banner

0개의 댓글