본문으로 건너뛰기

무작정 MFA 체험하기 - Module Federation

· 약 11분
Austin Lee
DevOps Engineer @ Allganize

최근 회사에서 MFA를 활용한 프로젝트를 진행해 보자는 이야기가 있어 공부를 하고 있습니다.
해당 개념을 알게 된 지는 얼마 되지 않았는데, 우선 MFA를 구현하는 방법 중 하나인 Module Federation을 간단히 체험해 보았습니다.

MFA란?

MFA (Micro Frontend Architecture)는 프론트엔드 서비스를 작은 단위로 쪼개어 개별적으로 개발◦배포를 진행하고, 이를 조합하여 통합 서비스를 만드는 방법론입니다. 개별 코드와 리소스를 기능별로 분리하기 때문에 유지보수와 재사용이 용이하고, 서로 독립적인 작업이 가능하기 때문에 조직 운영에도 도움이 되는 등 대규모 서비스 또는 조직에서 강점을 보이는 방식이라고 할 수 있습니다.

MFA를 구현하는 방법에는 여러 가지가 있습니다. 패키지로 만들어 빌드 과정에서 통합을 하는 방법도 있고, <iframe> 태그를 사용하는 방법도 있습니다. 하지만 저희 측에서는 JavaScript 번들을 런타임에서 로드해서 합치는 방식인 Module Federation을 고려했고 이쪽으로 테스트를 진행하기로 했습니다.

Module Federation 적용해 보기

외부로 내보낼 컴포넌트 호스트 화면

예시를 위해 CRA로 간단하게 2개의 앱을 구성했습니다. 목표는 첫 화면(이하 외부)의 카드를 호스트로 불러오는 것입니다.
외부 카드는 아래와 같은 데이터를 받아 teamName, teamImgUrl 정보를 사용하고 있습니다.
호스트 카드는 편의상 하드코딩되어 있습니다.

{
"teamName": "T1",
"teamImgUrl": "<t1-url>",
"playerName": "Faker",
"playerImgUrl": "<faker-url>"
}

Module Federation을 사용하기 위해서는 webpack을 설정해야 합니다.
초기화에는 npx webpack init 명령어를 사용했는데, 직접 설치를 해도 됩니다.

여러 설정 중에서도 가장 중요한 부분은 webpack.config.js 파일 설정이었습니다.

webpack.config.js (export)
const { ModuleFederationPlugin } = require("webpack").container;
const { FederatedTypesPlugin } = require("@module-federation/typescript");
// ...
const federationConfig = {
name: "exportapp",
filename: "remoteEntry.js",
exposes: {
"./Team": "./src/component/Team.tsx",
"./types": "./src/@types/index.ts",
},
shared: {
...deps,
react: { singleton: true, eager: true, requiredVersion: deps.react },
"react-dom": {
singleton: true,
eager: true,
requiredVersion: deps["react-dom"],
},
// ...
},
};
// ...
const config = {
entry: "./src/index.ts",
// ...
plugins: [
// ...
new ModuleFederationPlugin(federationConfig),
new FederatedTypesPlugin({ federationConfig }),
],
// ...
};
// ...

위는 외부 모듈의 설정 파일입니다. federationConfig 변수에 Module Federation 설정이 들어 있는데, 주목할 부분은 다음과 같습니다.

  • name은 전체 모듈의 이름으로, 다른 앱과 중복되지 않아야 합니다.
  • filename은 내보낼 파일의 이름입니다.
  • exposes는 어떤 소스를 어떤 이름으로 내보낼지 결정합니다.
    name과 함께 모듈 호출에 사용되기 때문에 정확히 설정해 주어야 합니다.

Module Federation에 필수적인 ModuleFederationPlugin 외에, TypeScript에서 타입 공유를 위해 추가로 FederatedTypesPlugin을 사용하였습니다. 우아한형제들에서는 양방향 타입 공유를 위해 @module-federation/native-federation-typescript 플러그인을 사용했는데, 저의 경우 아직 양방향 타입 공유를 고려할 단계는 아니라고 판단했습니다.

webpack.config.js (host)
// ...
const federationConfig = {
name: "hostapp",
remotes: {
sample1: "exportapp@http://localhost:3001/remoteEntry.js",
},
shared: {
// ...
},
};
// ...

호스트의 설정 파일도 거의 동일하지만, 모듈을 불러오기 위해 remotes 옵션을 설정했습니다. 외부 모듈에서 설정한 이름과 파일명, 그리고 접근 주소를 사용해 값을 입력합니다.
이렇게 설정을 마치고 서비스들을 재실행하면 호스트에 타입 파일이 생성됩니다.

호스트에 생성된 타입 파일

생성된 타입 파일을 이용하여 호스트에서 코드를 작성할 수 있습니다. VS Code 기준 코드 작성 시에도 타입을 올바르게 불러오는 것을 확인할 수 있었습니다.

외부 모�듈을 위한 코드 작성

카드에 들어가는 데이터를 변경해도 문제없이 모듈이 표시되었습니다.

정상적으로 표시되는 모듈

마주쳤던 문제들

  • 여러 환경이 있기 때문에 스타일 충돌 문제는 반드시 고려해야 했습니다.
    처음에는 Shadow DOM을 생각했습니다. 숨겨진 DOM을 만들어 스타일 태그를 추가하는 식으로 개별적인 스타일 적용을 구현할 수 있는 방법입니다. 하지만 이 경우 Shadow DOM을 설정할 명확한 경계를 정해야 하는데, 현재 정책이 모호해서 구현이 애매하다는 생각이 들었습니다. 우선 예제만 찾아 두고 필요할 경우 적용하기로 했습니다.
    가장 간단한 방법은 styled-components 등의 CSS-in-JS 라이브러리를 사용하는 것입니다. 이 방법을 통해서는 정상적으로 스타일 분리가 이루어지는 것을 확인하였습니다.
    위에 기술한 방법도 클래스명이 중복되면 충돌 문제가 있어, 별도의 내부 규칙을 정하는 것이 가장 확실하다는 의견을 받았습니다. (세문님 감사드립니다 😊)
  • Module Federation은 별도의 설정이 없을 경우 호스트에 연결된 UI 중 하나라도 로드에 실패한다면 호스트 전체가 멈추는 치명적인 문제가 있었습니다. 이슈에서도 해당 문제에 대해 논의한 흔적이 있고 예외 처리나 로우레벨 API 등의 방법을 사용하라는 이야기가 있었지만, 여러 가지 시도를 해 본 결과 제 기준 가장 깔끔한 방법은 Error boundary를 사용하는 것이었습니다. 모듈 다운로드 에러를 잡는 별도 컴포넌트를 만들어서 공유 모듈을 감싸고, 에러 발생 시 따로 처리하도록 하였습니다.

외부 모듈 에러 처리

체험해 보며 느낀 점

  • 예상은 했지만 생각보다 더 설정이 복잡했습니다. 위에서는 webpack.config.js 파일의 일부 설정만 다루었지만 tsconfig.json 설정, 패키지 최적화 등 여러 문제를 해결해야 했습니다. 테스트 과정이었기 때문에 일부 문제는 증상만 적어 두고 넘어가기도 했습니다.
  • 복잡도가 높아 큰 서비스에서는 이를 감수하고 사용할 수 있지만, 소규모 서비스에는 적합하지 않다고 느꼈습니다.
  • 아직 생태계가 미성숙하다고 생각되었습니다. 자료도 찾기 쉽지 않았고, 문서나 예제도 설명이 부실하거나 참고하기 힘든 경우가 많았습니다.
    +) 2024.4월 기준 계속 공식 페이지와 플러그인 등이 변하고 있습니다.
  • 개발 환경에서 여러 가지 자잘한 버그가 있었습니다. 특히 webpack 개발 서버와 호환이 잘 되지 않는 것으로 보입니다.

마치며

오랜만에 깊게 프론트엔드 분야를 파고들어서 그런지 쉽지 않았던 것 같습니다. 사실 이것이 주된 작업은 아니고, K8s 환경에서 MFA 템플릿을 구축하고 배포까지 자동화할 수 있는지 검증하는 것이 최종 목적입니다. 이후 진행사항이 생기면 추가로 적어 보도록 하겠습니다.

저의 작업 내역은 아래 Repository에서 확인하실 수 있습니다.

Repository 바로가기

참고 자료