Vite + Module Federation 으로 Micro Frontends 경험해보기

기운찬곰·2023년 6월 5일
12

프론트개발이모저모

목록 보기
12/20
post-thumbnail
post-custom-banner

Overview

최근에 우연찮게 Micro Frontends에 대해 알아볼 기회가 생겼습니다다. 사실 그 전까지는 알아볼 필요성을 못느꼈다고 봐야 될 거 같습니다. 이전 회사에서 제가 맡은 서비스가 대규모인것도 아니었고, 개인적인 공부나 실습에서도 사용할 필요가 없었기 때문입니다.

Webpack 5 Module Federation 에 대해서도 얼핏 듣긴 했었지만 제대로 알아보게 된 것도 이번이 처음입니다. 그래서 이번 글은 틀리거나 주관적인 내용이 있을 수 있으니 참고해서 봐주셨으면 좋겠습니다.


대규모 웹 애플리케이션

어떤 서비스가 대규모 웹 애플리케이션일까

대규모 웹 애플리케이션이란 어떤 것을 말하는 걸까요? Micro Frontends에 대해 공부하다보니 여러 회사의 사례를 찾을 수 있었습니다.

  • NHN Dooray 서비스
  • BZNAV 서비스
  • Flex (플렉스팀)

저는 여기서 한가지 공통점을 찾을 수 있었습니다. 서비스가 정말 클 수 있다는 것입니다. 특히, 독립적이지 않은 연관된 서비스들이 하나의 웹 애플리케이션이 되어야 할 때 이를 대규모 웹 애플리케이션이라고 볼 수 있을 거 같습니다.

가령, NHN Dooray 서비스를 참고하면 애플리케이션은 하나인데, 그 안에 홈, 업무, 메일, 캘린더, 드라이브, 위키, 주소록 같은 큼지막한 서비스가 있습니다. 이들 각각은 독립적이지 않습니다. 메일을 사용한다고 했을 때 드라이브를 불러와야 하는 경우도 있을 것입니다.

대규모 웹 애플리케이션에서 나타나는 문제점

이렇게 프로젝트 규모가 커지면 여러가지 문제가 발생한다고 합니다. (물론 저는 경험해보지 못했습니다...😂)

그 중 공통적으로 발생하는 문제는 빌드 타임입니다. 조그만 수정사항에도 전체를 빌드해야하므로 비효율적이면서 시간도 오래걸린다고 합니다. 듣기로는 30~40분도 걸린다고 합니다. 빌드 타임 뿐만 아니라 소스코드 관리도 힘들어집니다. 각 서비스마다 개발하고 통합하고 QA 하는 과정도 문제가 될 수 있습니다.

새로운 아키텍처로의 전환

그래서 이상적으로 생각하기로는 서비스를 분리해서 개발한 다음에, 운영에서 보여줄 때는 하나로 합쳐서 보여주면 되지 않을까요? 이것이 바로 Micro-Frontends 입니다.

Micro Frontends Integration Approaches

Micro Frontends에서 서비스를 어떻게 통합할 것인가에 대해서는 크게 빌드 타임 통합과 런타임 통합이 있습니다.

  • 빌드 타임 통합 : 쉽게 생각하면 프로젝트를 따로 만들고 빌드해서 npm에 배포를 한 다음, 사용하는 곳에서는 가져다가 사용하는 식이라고 보면 됩니다. 결국에는 전체를 다시 빌드해야 하는 건 똑같기 때문에 아쉬운 측면이 있습니다. 그래서 자주 바뀌지 않은 Shared Library (공통 UI, 함수 로직)은 빌드 타임 통합을 사용한다고 합니다. 예를 들어, toss slash 가 그런 형태인거 같습니다.
  • 런타임 통합 : javascript, iframes, web-components 통합이 있습니다. 그 중에서 javscript를 이용한 런타임 통합을 많이 추천하는 추세입니다. 런타임 통합은 빌드 타임 통합과는 다르게 A라는 서비스가 변경이 되었다면, 그 부분만 바뀌면 끝입니다. iframe을 생각해볼까요? iframe 내에 바뀐 내용은 분명 그 부분만 바뀔 것입니다. 따로 더 이상 뭔가를 할 필요가 없습니다.

빌드 타입 통합이냐 런타임 통합이냐는 특성이 다른거 같고, Micro Frontends에 대해서 각 서비스별로 구현해서 통합하기 위해서는 런타임 통합이 여러모로 맞는거 같습니다. 그리고 webpack 5 module federation 을 사용하면 webpack 설정으로 자바스크립트 런타임 통합을 구현해볼 수 있습니다.


Vite + Module Federation 으로 Micro Frontends 경험해보기

remote 프로젝트 생성

상위에 vite-mod-fed 이라는 폴더를 생성합니다. 그리고 나서 remote를 vite로 생성하겠습니다.

❯ pnpm create vite remote --template react

여기서 remote란 개념이 익숙하지 않을거 같은데, 쉽게 설명해서 remote에서 버튼을 만든다고 했을 때 이를 expose로 내보내기 설정을 하면 host는 곳에서 이를 받아서 사용할 수 있습니다.

그리고 @originjs/vite-plugin-federation 를 사용해서 vite에서 federation을 사용해보겠습니다.

❯ pnpm install -D @originjs/vite-plugin-federation

A Vite/Rollup plugin which support Module Federation. Inspired by Webpack and compatible with Webpack Module Federation.

remote 앱의 포트는 5001번으로 설정해주었습니다.

"scripts": {
    "dev": "vite --port 5001",
    "build": "vite build",
    "preview": "vite preview --port 5001"
  },

그리고 간단하게 Remote에서 버튼 컴포넌트를 하나 만들어봤습니다.

import { useState } from "react";

function Button() {
  const [state, setState] = useState(0);

  return (
    <div>
      <button className="shared-btn" onClick={() => setState((s) => s + 1)}>
        Click me: {state}
      </button>
    </div>
  );
}

export default Button;

host 프로젝트 생성

마찬가지로 host 프로젝트를 만들어보겠습니다. 그리고 federation 플러그인도 설치해줍니다.

❯ pnpm create vite host --template react
❯ pnpm install -D @originjs/vite-plugin-federation

Configuration Remote

이제 remote에 설정을 해보도록 하겠습니다. 설정방법에 대해서는 깃허브에 잘 나와있습니다.

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import federation from "@originjs/vite-plugin-federation";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    react(),
    federation({
      name: "remote-app",
      filename: "remoteEntry.js",
      // Modules to expose
      exposes: {
        "./Button": "./src/components/Button",
      },
      shared: ["react", "react-dom"],
    }),
  ],
  build: {
    modulePreload: false,
    target: "esnext",
    minify: false,
    cssCodeSplit: false,
  },
});
  • name은 remote application 이름을 의미.
  • filename은 매니페스트 파일을 의미한다고 보면 된다. 기본적으로 remoteEntry.js를 사용한다.
  • exposes가 중요한데, Remote에서 내보낼 요소를 정할 수 있다. 여기서는 버튼을 내보낼 것이므로 버튼 컴포넌트 경로를 작성해주었다.
  • shared는 의존성을 나타낸다. 기본적으로 react, react-dom를 명시해준다.

실제 빌드를 해보면 remoteEntry.js 파일을 볼 수 있습니다. 그 외 Button에 대해서도 볼 수 있습니다.

remoteEntry.js 를 살펴보면 해당 파일은 실제 데이터 파일이 아니라는 것입니다. 경로만 가리키고 있는 것을 알 수 있습니다.

let moduleMap = {
	"./Button":()=>{
      dynamicLoadingCss(["style-e8125edc.css"]);
      return __federation_import('./__federation_expose_Button-fa1bfa2d.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},};
...

const get =(module) => {
        return moduleMap[module]();
    };

const init =(shareScope) => {
      globalThis.__federation_shared__= globalThis.__federation_shared__|| {};
			...
}

export { dynamicLoadingCss, get, init };

Configuration Host Module Federation

다음은 host에 대해서 remote를 명시해주는 단계입니다.

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import federation from "@originjs/vite-plugin-federation";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    react(),
    federation({
      name: "host-app",
      remotes: {
        remoteApp: "http://localhost:5001/assets/remoteEntry.js",
      },
      shared: ["react", "react-dom"],
    }),
  ],
  build: {
    modulePreload: false,
    target: "esnext",
    minify: false,
    cssCodeSplit: false,
  },
});
  • remote app remoteEntry.js 를 명시해주면 된다.
  • shared을 이런식으로 사용하면 동일한 react, react-dom을 중복해서 다운로드하지 않습니다.

Using remote modules on the host side

그리고 Host 앱에서 Remote App에 있는 Button을 가져다가 사용해보겠습니다.

import { useState } from "react";
import "./App.css";
import Button from "remoteApp/Button";

function App() {
  const [count, setCount] = useState(0);

  return (
    <>
      <h1>Host Application</h1>
      <Button />
      <div className="card">
        <button onClick={() => setCount((count) => count + 1)}>
          count is {count}
        </button>
        <p>
          Edit <code>src/App.jsx</code> and save to test HMR
        </p>
      </div>
      <p className="read-the-docs">
        Click on the Vite and React logos to learn more
      </p>
    </>
  );
}

export default App;

와 정말 됩니다. Remote에서 만든 버튼을 Host에서 가져와서 사용할 수 있다니... 뭔가 색다른 경험인거 같습니다. 이런식으로 서비스도 분리해서 개발할 수 있겠군요.

그리고 사실상 host는 redeploy 없이 runtime에 바뀔 수 있다는 점이 가장 큰 장점입니다. 한번 remote에 button style을 바꿔보겠습니다.

.shared-btn {
    background-color: skyblue;
    border: 1px solid white;
    color: white;
    padding: 15px 30px;
    text-align: center;
    text-decoration: none;
    font-size: 18px;
}

그리고 remote를 빌드하고 실행해보겠습니다. 짜잔. Host에는 아무것도 하지 않았습니다. 근데 잘 바뀐것을 알 수 있습니다.

❯ yarn run build && yarn run preview

State Sharing For Micro-FEs

상태 처리에 대한 공유도 가능합니다. atomic state manager, Recoil, Jatai를 사용을 추천합니다. 여기서는 Jotai를 사용해보도록 하겠습니다. 이제 해볼건 Click me를 클릭하면 count is 에 대한 카운트도 올라가도록 해볼 것입니다. 그 상태 처리를 Remote에서 해볼 것이고, 이를 Host에서도 동일하게 넘겨줘서 잘 되는지 실험해볼 것입니다.

Jotai은 처음인데 사용법은 간단한거 같습니다. recoil이랑도 비슷한거 같기도 합니다. 그냥 useState랑도 흡사..

// src/store/countStore.js
import { atom, useAtom } from 'jotai'

const countAtom = atom(0) // atom은 state 조각을 표현

const useCount = () => useAtom(countAtom) // useState 사용과 비슷하게 useAtom을 사용

export default useCount;

그리고 useState로 된 부분을 useCount로 교체해주면 됩니다. App.jsx도 마찬가지입니다. 그래서 버튼 두개가 하나의 상태를 공유하도록 하는 것입니다.

import useCount from "../store/countStore";

function Button() {
  const [state, setState] = useCount(0);

	...
}

상태를 공유하도록 하려면 어떻게 해야 할까요? 먼저 Remote 설정에서 store를 추가해줍니다. shared도 jotai를 추가해줍니다.

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import federation from "@originjs/vite-plugin-federation";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    react(),
    federation({
      name: "remote-app",
      filename: "remoteEntry.js",
      // Modules to expose
      exposes: {
        "./Button": "./src/components/Button",
        "./store/countStore": "./src/store/countStore",
      },
      shared: ["react", "react-dom", "jotai"],
    }),
  ],
  build: {
    modulePreload: false,
    target: "esnext",
    minify: false,
    cssCodeSplit: false,
  },
});

그리고 나서 Host에서 사용해보면 역시 잘 됩니다. 이처럼 단순 UI 컴포넌트 뿐만 아니라 상태랑 다른 것들도 공유가 가능하다는 것을 보여줍니다.

import "./App.css";
import Button from "remoteApp/Button";
import useCount from "remoteApp/store/countStore";

function App() {
  const [count, setCount] = useCount(0);

  return (
    <>
      <h1>Host Application</h1>
      <Button />
      <div className="card">
        <button onClick={() => setCount((count) => count + 1)}>
          count is {count}
        </button>
        <p>
          Edit <code>src/App.jsx</code> and save to test HMR
        </p>
      </div>
      <p className="read-the-docs">
        Click on the Vite and React logos to learn more
      </p>
    </>
  );
}

export default App;

전체 코드 참고 : https://github.com/ckstn0777/vite-mod-fed


마치면서

이번 시간에는 미약하게나마 대규모 웹 애플리케이션에서 발생할 수 있는 문제점과 이를 해결하기 위한 Micro Frontends 아키텍처에 대해 알아봤고, 여러가지 통합 방법이 있는 것을 알 수 있었습니다. 그리고 최근에는 런타임 자바스크립트 통합에 대한 기술과 Module Federation 에 대한 Webpack, Vite, Rollup 지원이 이뤄지고 있는거 같습니다.

대규모 아키텍처 설계와 방법론을 공부하고 실습해본건 사실상 처음인데 아직 공부할 게 많구나... 뒤돌아보게 되네요.

단순히 어떤 UI, 어떤 서비스를 만들까를 벗어나서 어떻게 하면 대규모 애플리케이션에 대해 아키텍처와 구조를 잘 설계할 수 있을까에 대한 고민도 해볼 수 있어서 좋았습니다.


참고 자료

profile
배움을 좋아합니다. 새로운 것을 좋아합니다.
post-custom-banner

0개의 댓글