키즈노트는 원장님, 선생님, 학부모님의 소통을 돕기 위해 키즈노트 서비스를 제공하고 있어요. 그런데 원장님, 선생님의 행정 업무를 돕는 전자문서+ 서비스도 제공하고 있다는 것, 알고 계셨나요?
대부분의 서비스가 그렇듯이, 키즈노트는 작은 서비스에서 시작되었어요. 사용자들이 늘어가면서 키즈노트도 함께 커졌고, 곧이어 전자문서+도 탄생하게 되었습니다.
키즈노트와 전자문서+, 그리고 이 서비스들이 공통으로 사용하는 공통 레이아웃 컴포넌트는 모두 모놀리스 아키텍처를 따르고 있어요.
모놀리스 아키텍처는 초기 구현이 간단하기 때문에 개발을 빠르게 진행할 수 있다는 장점이 있지만, 키즈노트와 전자문서+의 규모가 커지면서 모놀리스 아키텍처의 단점이 드러나기 시작했어요.
키즈노트 / 전자문서+ / 공통 레이아웃을 분리해서 각각 개발하고 관리하면 위 단점들을 해결할 수 있을 것 같은데요, 어떻게 구현할 수 있을까요?
이 문제에 대해 고민하던 중, FEConf 2023에서 대형 웹 애플리케이션 Micro Frontends 전환기 세션을 듣게 되었어요. 플렉스 FE Labs 팀은 Module Federation을 이용해서 마이크로 프론트엔드를 구현할 수 있었는데요, 이를 키즈노트에도 적용해 볼 수 있을 것 같습니다.
마이크로 프론트엔드는 프론트엔드 어플리케이션을 작은 독립적인 모듈로 분리하여 개발하고 관리하는 아키텍처를 의미해요. 모놀리식 아키텍처와 다르게, 여러 개의 독립적인 어플리케이션을 조합하여 전체 어플리케이션을 구성할 수 있어요.
각각의 모듈은 독립적으로 기술 스택을 선택하고 업그레이드할 수 있습니다. 또한 독립적으로 개발 및 배포가 가능하다는 특징이 있어요.
키즈노트와 전자문서+처럼 큰 규모의 어플리케이션에 마이크로 프론트엔드 아키텍처를 사용하면 좀 더 유연하고 확장성 좋은 어플리케이션을 만들 수 있을 것 같습니다.
Module Federation은 웹팩에서 제공하는 기능 중 하나로, 마이크로 프론트엔드 아키텍처를 구현하는 데 사용해요.
Module Federation은 아래와 같은 핵심 개념을 가지고 있어요.
Module Federation을 사용하면 독립적으로 개발된 어플리케이션들을 통합하여 하나의 어플리케이션으로 구성할 수 있어요. 이는 코드의 재사용성과 유지보수성을 증가시켜 줍니다.
먼저 어플리케이션들을 어떤 구조로 분리하고 싶은지 간단하게 정리해 보았어요.
요구사항 4번의 라우팅 처리에 대해 조금 더 설명드리면, 키즈노트와 전자문서+는 모놀리스 아키텍처를 따르고 있기 때문에 하나의 파일 안에서 두 서비스의 모든 라우팅 처리가 이루어지고 있어요.
성격이 조금 다른 서비스임에도 모든 path(140여 개...)를 하나의 파일에서 관리한다는 건 꽤 복잡한 일인데요, 이를 명확하게 분리할 필요가 있다고 생각했어요.
키즈노트는 URL path에 /service
라는 prefix를 사용하고, 전자문서+는 /e-docs
라는 prefix를 사용해서 라우팅 구조를 분리해 줄 거예요.
요구사항 4번은 다른 것들에 비해 레퍼런스가 거의 없었기 때문에 구현이 가능할지 고민이 많았어요.(Dynamic Import된 라우터를 react-router-dom
에서 처리하지 못하거나 등)
하지만 반드시 개선해야 하는 부분이기 때문에 일단 시도해 보았어요.
🚨 여기부터는 마이크로 프론트엔드 구현의 성공과 실패가 모두 담겨있습니다. 성공만 빠르게 확인하고 싶은 분은 여기에서 확인해 주세요.
CRA를 이용해서 4개의 어플리케이션을 생성해 줄게요.
e-docs
: 전자문서+ 어플리케이션 디렉토리kidsnote
: 키즈노트 어플리케이션 디렉토리layout
: 공통 레이아웃 어플리케이션 디렉토리main
: 원격 모듈을 사용할 호스트 어플리케이션 디렉토리layout
어플리케이션의 ./src/components
디렉토리에서 Header
와 Footer
컴포넌트를 생성해 줄게요. 저는 요즘 잘 사용하고 있는 Chakra UI를 사용했어요.
layout
어플리케이션은 3000번 포트에서 실행해 줍니다.
이제 Header
와 Footer
를 호스트 어플리케이션에서 사용할 수 있도록 expose 해주어야 해요. webpack.config.js
의 초기 설정을 위해 터미널에서 아래 명령어를 실행해 줍니다.
webpack init
명령어를 실행하면 아래와 같은 질문들이 나오게 되는데요, 프로젝트 설정에 따라 Y/N를 선택하면 됩니다.
모든 대답을 완료한 후 webpack.config.js
파일이 생성된 것을 확인할 수 있습니다. 여기서 불필요한 코드는 제거하고, 프로젝트에 맞춰서 수정해 줄게요.
// layout > webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index',
mode: 'development',
output: {
path: path.resolve(__dirname, 'build'),
},
devServer: {
open: true,
host: 'localhost',
port: 3000,
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
module: {
rules: [
{
test: /\.(ts|tsx)$/i,
loader: 'ts-loader',
exclude: ['/node_modules/'],
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.jsx', '.js', '...'],
},
};
(이 포스트에서는 웹팩 설정을 하나하나 설명하지 못하지만, 공식 문서에서 내용을 자세히 확인할 수 있어요.)
Module Federation 플러그인을 plugins
배열에 추가해야 해당 기능을 사용할 수 있어요. 아래와 같이 플러그인을 추가해 줄게요.
// layout > webpack.config.js
plugins: [
...
new ModuleFederationPlugin({
name: 'layout',
filename: 'remoteEntry.js',
exposes: {
'./Header': './src/components/Header',
'./Footer': './src/components/Footer',
},
}),
],
remoteEntry.js
를 사용하며, 번들링 후 파일이 생성돼요.)아래와 같이 exposes
를 설정하면 ./src/components
경로를 모두 붙이지 않고 짧은 경로를 사용해서 모듈을 불러올 수 있어요.
// layout > webpack.config.js
new ModuleFederationPlugin({
name: 'layout',
filename: 'remoteEntry.js',
exposes: {
'./Header': './src/components/Header',
},
})
// main > GlobalLayout.tsx
const Header = React.lazy(() => import('layout/Header'));
원격 모듈을 만들었으니 3001번 포트에서 실행할 호스트 어플리케이션도 만들어줄게요. main
어플리케이션에서 GlobalLayout
컴포넌트를 만들고, GlobalLayout
컴포넌트 안에서 Header
와 Footer
원격 모듈을 사용해 줄 거예요.
위 스텝과 동일한 방법으로 webpack.config.js
파일을 추가하고, plugins
배열에 Module Federation 플러그인을 추가해 줍니다.
// main > webpack.config.js
plugins: [
...
new ModuleFederationPlugin({
name: 'main',
remotes: {
layout: 'layout@http://localhost:3000/remoteEntry.js',
},
}),
],
remotes
는 사용할 원격 모듈 목록을 의미해요. 사용할 원격 모듈의 이름과 remoteEntry.js
파일 경로를 작성해 줍니다.
이제 아래와 같이 원격 모듈을 동적으로 불러옵니다.
// main > GlobalLayout.tsx
import React from 'react';
import { Outlet } from 'react-router-dom';
import { Box } from '@chakra-ui/react';
const Header = React.lazy(() => import('layout/Header'));
const Footer = React.lazy(() => import('layout/Footer'));
export default function GlobalLayout() {
return (
<>
<Header />
<Box>
<Outlet />
</Box>
<Footer />
</>
);
}
그런데, Header
와 Footer
컴포넌트에서 타입 에러가 발생하네요!
Header
와 Footer
컴포넌트는 런타임에서 통합되는 원격 모듈이기 때문에, 호스트는 해당 모듈이 어떤 타입을 갖는지 알 수 없는 상태입니다.
./src
디렉토리 하위에 declarations.d.ts
파일을 추가하고 Header
와 Footer
컴포넌트의 타입을 정의해 줄게요.
// main > declarations.d.ts
declare module 'layout/Header';
declare module 'layout/Footer';
더 이상 타입 에러가 발생하지 않는 것을 확인할 수 있어요.
호스트에서 원격 모듈을 정상적으로 가져오는지 테스트하기 위해 layout
과 main
을 빌드 / 실행해 줄게요.
그런데, main
에서 빌드가 되지 않습니다.😨
layout
과 main
에 Module Federation을 잘 적용한 것 같은데, 왜 빌드에 실패하는 걸까요?
아무래도 원격 어플리케이션이 제대로 빌드가 되었는지, 그래서 Header
와 Footer
컴포넌트를 정상적으로 제공하고 있는지 확인해 볼 필요가 있을 것 같습니다.
layout
의 ./build
디렉토리에서 파일 목록을 확인해 볼게요.
빌드 파일 목록을 살펴보니 호스트에서 접근할 remoteEntry.js
파일이 존재하지 않는군요!
웹팩에 대해 검색해 보니, CRA로 만들어진 어플리케이션에서 웹팩 설정을 변경하려면 별도의 라이브러리를 사용해야 한다고 해요.(너무 당연하게 오버라이드 될 줄 알았었던...)
react-app-rewired 라이브러리를 이용해서 웹팩 설정을 오버라이드 해줄게요. layout
과 main
의 루트에 config-overrides.js
파일을 추가하고, 문서에 따라 아래와 같이 작성합니다. 기존에 작성했던 설정을 거의 그대로 가져왔어요.
// layout > config-overrides.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
webpack: (config) => {
return {
...config,
entry: './src/index',
mode: 'development',
output: {
path: path.resolve(__dirname, 'build'),
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
}),
new ModuleFederationPlugin({
name: 'layout',
filename: 'remoteEntry.js',
exposes: {
'./Header': './src/components/Header',
'./Footer': './src/components/Footer',
},
}),
],
module: {
rules: [
{
test: /\.(ts|tsx)$/i,
loader: 'ts-loader',
exclude: ['/node_modules/'],
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.jsx', '.js', '...'],
},
};
},
devServer: (configFunction) => {
return (proxy, allowedHost) => ({
...configFunction(proxy, allowedHost),
port: 3000,
host: 'localhost',
static: {
directory: path.resolve(__dirname, 'build'),
},
});
},
};
// main > config-overrides.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
webpack: (config) => {
return {
...config,
entry: './src/index',
mode: 'development',
output: {
path: path.resolve(__dirname, 'build'),
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
}),
new ModuleFederationPlugin({
name: 'main',
remotes: {
layout: 'layout@http://localhost:3000/remoteEntry.js',
},
}),
],
module: {
rules: [
{
test: /\.(ts|tsx)$/i,
loader: 'ts-loader',
exclude: ['/node_modules/'],
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.jsx', '.js', '...'],
},
};
},
devServer: (configFunction) => {
return (proxy, allowedHost) => ({
...configFunction(proxy, allowedHost),
port: 3001,
host: 'localhost',
static: {
directory: path.resolve(__dirname, 'build'),
},
});
},
};
package.json
에서 명령어도 아래와 같이 바꿔줄게요.
// layout, main > package.json
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
},
layout
을 빌드 한 후 ./build
디렉토리에서 remoteEntry.js
파일을 확인할 수 있어요.
layout
과 main
을 다시 빌드 / 실행해볼게요.
터미널에서는 에러 메세지 없이 잘 실행되는듯싶었지만, localhost:3001로 접속해 보면 새로운 에러 메세지를 확인할 수 있습니다.
"네...?"
이 에러는 웹팩 공식 문서의 트러블 슈팅에 나와있어요.
모듈을 비동기 청크에 넣지 않고 동기식으로 제공하는 Module Federation 고급 API 내에서 의존성을 eager로 설정할 수 있습니다. 이를 통해 초기 청크에서 이러한 공유 모듈을 사용할 수 있습니다. 그러나 제공된 모든 모듈과 예비 모듈은 항상 다운로드 되므로 주의하세요. 쉘 같이 애플리케이션의 한 지점에서만 제공하는 것이 좋습니다.
비동기 경계를 사용하는 것을 추천합니다. 추가 왕복을 방지하고 일반적인 성능 향상을 위해 더 큰 청크의 초기화 코드를 분할합니다.
무슨 소리인지 하나도 모르겠는 번역이지만, 읽어보니 비동기 청크를 만들기 위해 bootstrap.js
를 추가해 주어야 하는 것 같아요.
사실 bootstrap.js
의 역할을 제대로 이해하기 힘들었는데, 다행히도 우아콘 2023 프론트엔드 개발의 미래, Module Federation의 적용 세션에서 명확하게 설명해 주셔서 이해할 수 있었어요.
bootstrap.js
를 추가한 상태에서 main
어플리케이션을 빌드 / 실행해 볼까요?
넵... 여전히 실행되지 않습니다. 콘솔 창에서 리액트 에러를 확인할 수 있어요.
위 에러는 리액트를 정상적으로 실행할 수 없기 때문에 발생한 에러에요. 호스트 어플리케이션이 초기에 실행될 때, 원격 어플리케이션으로부터 remoteEntry.js
파일을 동기적으로 가져오게 됩니다. 이때 리액트도 초기에 함께 가져와야 추후에 불러오는 원격 모듈이 정상적으로 동작할 수 있어요.
리액트는 초기에 동기적으로 가져오고 원격 모듈은 비동기적으로 가져오기 위해 원격 모듈을 비동기 청크인 bootstrap.js
로 분리해 주어야 합니다.
그리고 Module Federation의 shared
옵션에서 리액트에 eager: true
를 설정해 줄게요.
// layout, main > config-overrides.js
new ModuleFederationPlugin({
...
shared: {
react: {
eager: true,
singleton: true,
requiredVersion: '^18.2.0',
},
},
}),
true
로 설정할 경우, 초기 실행 시 해당 모듈을 동적으로 가져올 수 있어요.true
로 설정할 경우, 여러 모듈에서 해당 모듈을 사용하더라도 하나의 인스턴스만 생성해요.이 외에 다양한 설정들은 여기서 확인할 수 있습니다.
이제 다시 main
을 빌드 / 실행하고 localhost:3001로 진입하면...
layout
의 Header
와 Footer
컴포넌트를 정상적으로 가져오고 있습니다! 🥳 🎊 🎉
네트워크 탭에서도 여러 개의 청크를 불러오는 것을 확인할 수 있어요.
이제 kidsnote
디렉토리에서 키즈노트를 만들어볼게요. 키즈노트는 3002번 포트에서 실행해 줄 예정입니다.
./src/components/common
디렉토리에는 키즈노트에서 사용하는 LNB와 레이아웃 컴포넌트를 만들고, ./src/components
디렉토리에는 페이지 컴포넌트를 만들었어요.
실제 키즈노트는 수많은 페이지를 가지고 있지만, 이번에는 알림장 / 공지사항 / 앨범 페이지만 만들어 줄게요.
라우팅 처리를 위해 react-router-dom
라이브러리를 추가하고, 라우팅 구조를 아래와 같이 만들어볼 거예요.
./src
디렉토리에 AppRoutes.tsx
파일을 추가하고, 라우팅 처리를 해줍니다. 나중에 AppRoutes.tsx
파일을 expose 해줄 예정이에요.
// kidsnote > AppRoutes.tsx
import React from 'react';
import { Routes, Route } from 'react-router-dom';
import Layout from './components/common/Layout';
const ReportPage = React.lazy(() => import('./components/ReportPage'));
const NoticePage = React.lazy(() => import('./components/NoticePage'));
const AlbumPage = React.lazy(() => import('./components/AlbumPage'));
export default function AppRoutes() {
return (
<Routes>
<Route
path="/service"
element={<Layout />}
>
<Route
path="report"
element={<ReportPage />}
/>
<Route
path="notice"
element={<NoticePage />}
/>
<Route
path="album"
element={<AlbumPage />}
/>
</Route>
</Routes>
);
}
App.tsx
파일에서 BrowserRouter
와 AppRoutes
를 넣어줄게요.
// kidsnote > App.tsx
import React from 'react';
import { BrowserRouter as Router } from 'react-router-dom';
import AppRoutes from './AppRoutes';
export default function App() {
return (
<Router>
<AppRoutes />
</Router>
);
}
localhost:3002로 접속하면 kidsnote
어플리케이션이 정상적으로 동작하는 것을 확인할 수 있어요.
layout
과 동일하게 config-overrides.js
를 생성하고 아래와 같이 AppRoutes
를 expose 해줍니다. (package.json
수정도 잊지 마세요!)
// kidsnote > config-overrides.js
plugins: [
...
new ModuleFederationPlugin({
name: 'kidsnote',
filename: 'remoteEntry.js',
exposes: {
'./AppRoutes': './src/AppRoutes',
},
shared: {
react: {
eager: true,
singleton: true,
requiredVersion: '^18.2.0',
},
},
}),
],
키즈노트 원격 모듈을 만들었으니 main
에서 Module Federation 설정을 수정해야 해요. 아래와 같이 remotes
에 kidsnote
를 추가해 줄게요.
// main > config-overrides.js
plugins: [
...
new ModuleFederationPlugin({
name: 'main',
remotes: {
layout: 'layout@http://localhost:3000/remoteEntry.js',
kidsnote: 'kidsnote@http://localhost:3002/remoteEntry.js',
},
shared: {
react: {
eager: true,
singleton: true,
requiredVersion: '^18.2.0',
},
},
}),
],
그리고 main
의 App.tsx
에서 /service
라우팅 처리를 해줍니다.
// main > App.tsx
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import GlobalLayout from './GlobalLayout';
const KidsnoteAppRoutes = React.lazy(() => import('kidsnote/AppRoutes'));
export default function App() {
return (
<Router>
<Routes>
<Route
path="/"
element={<GlobalLayout />}
>
// KidsnoteAppRoutes 내부에서 /service path를 가지고 있으므로 path 생략
<Route element={<KidsnoteAppRoutes />}/>
</Route>
</Routes>
</Router>
);
}
main
을 빌드 / 실행한 후 localhost:3001으로 진입하면 react-router-dom
에서 에러가 발생하는 것을 확인할 수 있습니다.
에러 메세지를 보니 <Routes>
를 <BrowserRouter>
로 감싸지 않았기 때문에 에러가 발생한 것 같아요.
그런데... main
의 App.tsx
에서 이미 <BrowserRouter>
로 감싸주었는데, 왜 이런 에러가 발생하는 걸까요?
이 부분에서 생각보다 고민이 많았는데요, 의외로 답은 간단했습니다. 바로 아래와 같이 라우터 구조를 변경하는 것이었어요.
kidsnote
의 AppRoutes.tsx
를 제거하고, App.tsx
안에 <BrowserRouter>
를 포함시켜줄게요.
// kidsnote > App.tsx
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Layout from './components/common/Layout';
const ReportPage = React.lazy(() => import('./components/ReportPage'));
const NoticePage = React.lazy(() => import('./components/NoticePage'));
const AlbumPage = React.lazy(() => import('./components/AlbumPage'));
export default function App() {
return (
<Router basename="/service">
<Routes>
<Route
path="/"
element={<Layout />}
>
<Route
path="report"
element={<ReportPage />}
/>
<Route
path="notice"
element={<NoticePage />}
/>
<Route
path="album"
element={<AlbumPage />}
/>
</Route>
</Routes>
</Router>
);
}
그리고 App.tsx
를 expose 해줍니다.
// kidsnote > config-overrides.js
plugins: [
...
new ModuleFederationPlugin({
name: 'kidsnote',
filename: 'remoteEntry.js',
exposes: {
'./App': './src/App',
},
shared: {
react: {
eager: true,
singleton: true,
requiredVersion: '^18.2.0',
},
},
}),
],
main
의 App.tsx
도 함께 수정해 줄게요.
// main > App.tsx
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import GlobalLayout from './GlobalLayout';
const KidsnoteApp = React.lazy(() => import('kidsnote/App'));
export default function App() {
return (
<Router>
<Routes>
<Route
path="/"
element={<GlobalLayout />}
>
<Route
path="/service/*"
element={<KidsnoteApp />}
/>
</Route>
</Routes>
</Router>
);
}
마지막으로 e-docs
디렉토리에서 전자문서+를 만들어야 합니다. 전자문서+는 3003번 포트에서 실행해 줄 예정이에요.
사실, 전자문서+는 바로 전 단계에서 만든 키즈노트와 완전히 동일한 구조를 사용했어요. 다른 점이라면 폴더명, 파일명 정도가 되겠네요!
키즈노트와 동일하게 ./src/components/common
디렉토리에는 전자문서+에서 사용하는 LNB와 레이아웃 컴포넌트를 만들고, ./src/components
디렉토리에는 페이지 컴포넌트를 만들었어요.
실제 전자문서+ 또한 수많은 페이지를 가지고 있지만, 이번에는 운영 일지 / 교직원 관리 / 원아 관리 페이지만 만들어 줄게요.
키즈노트와 동일한 구조로 라우터를 설정하고 localhost:3003으로 접속하면 e-docs
어플리케이션이 정상적으로 동작하는 것을 확인할 수 있어요.
config-overrides.js
와 package.json
설정도 잊지 말고 해주어야 합니다.
// e-docs > config-overrides.js
plugins: [
...
new ModuleFederationPlugin({
name: 'e_docs',
filename: 'remoteEntry.js',
exposes: {
'./App': './src/App',
},
shared: {
react: {
eager: true,
singleton: true,
requiredVersion: '^18.2.0',
},
},
}),
main
의 config-overrides.js
와 App.tsx
도 함께 수정해 줄게요.
// main > config-overrides.js
plugins: [
...
new ModuleFederationPlugin({
name: 'main',
remotes: {
layout: 'layout@http://localhost:3000/remoteEntry.js',
kidsnote: 'kidsnote@http://localhost:3002/remoteEntry.js',
e_docs: 'e_docs@http://localhost:3003/remoteEntry.js',
},
shared: {
react: {
eager: true,
singleton: true,
requiredVersion: '^18.2.0',
},
},
}),
],
// main > App.tsx
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import GlobalLayout from './GlobalLayout';
const KidsnoteApp = React.lazy(() => import('kidsnote/App'));
const EDocsApp = React.lazy(() => import('e_docs/App'));
export default function App() {
return (
<Router>
<Routes>
<Route
path="/"
element={<GlobalLayout />}
>
<Route
path="/service/*"
element={<KidsnoteApp />}
/>
<Route
path="/e-docs/*"
element={<EDocsApp />}
/>
</Route>
</Routes>
</Router>
);
}
이제 layout
/ kidsnote
/ e-docs
/ main
어플리케이션을 빌드 / 실행하고, 호스트 어플리케이션이 실행되고 있는 localhost:3001로 진입해 보겠습니다.
모든 컴포넌트를 제대로 불러오고 있고, 라우팅도 정상적으로 동작하고 있습니다! 여기까지 오는 데 정말 오래 걸렸네요. 🥹
이로써 Module Federation으로 마이크로 프론트엔드를 구현해 보았습니다.
비록 작은 어플리케이션들로 실습을 진행했지만, 작업을 하면서 아래와 같은 단점들을 느끼게 되었어요.
실습에서는 적은 수의 원격 모듈을 사용했기 때문에 이 부분이 크게 문제 되지 않았는데요, 하지만 마이크로 프론트엔드 아키텍처는 큰 규모의 어플리케이션에 적합한 아키텍처인 만큼 실무에서는 훨씬 더 많은 원격 모듈을 사용하게 될 것입니다.
저는 원격 모듈을 추가할 때마다 Module Federation의 exposes
를 수정해 주었는데요, 강남언니 프론트엔드 개발팀은 내부 패키지를 이용해서 특정 디렉토리 내 컴포넌트들을 자동으로 expose 하도록 개발했다고 해요.
expose를 자동화 처리한다면 파일명 오타, exposes
목록 내 누락 등 휴먼 에러를 방지할 수 있을 것 같네요!
실습에서는 호스트 어플리케이션이 원격 모듈을 사용할 때마다 declarations.d.ts
파일에 모듈 타입을 정의해 주었어요. 이는 원격 모듈을 추가할 때마다 호스트 어플리케이션을 수정해야 한다는 의미이고, 마이크로 프론트엔드 아키텍처의 "독립적 개발 및 배포"라는 장점을 상쇄시키게 됩니다.
강남언니 프론트엔드 개발팀은 expose-typed
라는 CLI를 이용해서 모듈의 타입 선언 패키지를 만들고 배포하는 방법을 사용했다고 해요. 호스트 어플리케이션은 타입 선언 패키지를 설치하면 해당 모듈의 타입을 알 수 있게 되는 것이죠.
우아한형제들 배민커머스웹프론트개발팀은 module-federation/typescript 패키지를 이용하여 모듈의 타입 선언 파일을 만들고, 모듈과 함께 expose 하는 방법을 사용했다고 해요. 그러나 호스트 어플리케이션과 원격 어플리케이션의 양방향 타입 지원에 대한 한계를 느꼈고, native-federation-typescript라는 패키지를 사용하여 해결했다고 합니다.
원격 모듈의 타입을 호스트 어플리케이션에서 관리하지 않고, 강남언니와 우아한형제들처럼 원격 어플리케이션에서 관리하면 어플리케이션 간의 커플링 이슈를 해결할 수 있을 것 같아요.
실습에서는 리액트를 초기 청크에 불러오기 위해 shared
설정에 추가해 주었습니다. 그런데 라이브러리에 따라 초기 청크에 불러올지, 인스턴스를 몇 개 생성할지, 어떤 버전을 사용할지 등 꼼꼼한 관리가 필요해 보였어요. 모듈과 라이브러리가 늘어날수록 공유 모듈의 관리 난이도도 함께 올라갈 것 같은데요, 팀 내에서 충분한 논의 후 공유 모듈을 관리해야 할 것 같습니다.
실습을 통해 Module Federation은 마이크로 프론트엔드를 구현하는 데 (완벽한 도구는 아니지만) 꽤 좋은 도구인 것을 알게 되었어요. 강력한 장점을 가지고 있는 만큼 빨리 프로덕션에 적용해 보고 싶은데요, 하지만 위 아쉬운 점들 외에도 상태 관리 등 추가로 고려해야 할 부분들이 있고 웹팩 설정에 대한 러닝 커브 등 당장 도입은 쉽지 않은 것 같아요.
하지만 파트 내부에서 Module Federation 도입을 긍정적으로 검토하고 있기 때문에 프로덕션에 적용할 날이 그리 멀지 않은 것 같기도 합니다. 😊