테마
리모트 컴포넌트 로딩
다른 서버의 컴포넌트를 런타임에 가져올 때는 React.lazy와 Suspense로 지연 로딩을 구현하고, ErrorBoundary로 장애를 격리하여 사용자 경험을 보호해야 한다.
학습 목표
- React.lazy와 Suspense를 활용하여 리모트 컴포넌트를 지연 로딩하는 방법을 익힌다
- ErrorBoundary로 리모트 컴포넌트의 로딩 실패를 격리하고 대체 UI를 제공하는 패턴을 구현할 수 있다
- 리모트 앱을 격리된 DOM 엘리먼트에 마운트/언마운트하는 inject 패턴을 이해한다
- 마이크로프론트엔드에서 지연 로딩, 지연 UI, 에러 격리가 필수적인 이유를 설명할 수 있다
1. 리모트 컴포넌트의 지연 로딩
Module Federation을 통해 다른 서버의 컴포넌트를 가져오는 것은 네트워크 요청이 수반된다. 따라서 일반적인 import와 달리 **지연 로딩(lazy loading)**이 필수적이다.
1.1 React.lazy를 사용한 리모트 컴포넌트 로딩
jsx
import React, { Suspense } from "react";
// 리모트 컴포넌트를 lazy로 로드
const RemoteButton = React.lazy(
() => import("componentApp/Button")
);
function App() {
return (
<div>
<h1>Main App</h1>
{/* Suspense로 로딩 중 UI 제공 */}
<Suspense fallback={<div>버튼 로딩 중...</div>}>
<RemoteButton onClick={() => console.log("클릭!")}>
리모트 버튼
</RemoteButton>
</Suspense>
</div>
);
}React.lazy + Suspense 동작 방식:
| 단계 | 동작 | 사용자에게 보이는 화면 |
|---|---|---|
| 1 | Host 앱 렌더링 시작 | Host 자체 컨텐츠 표시 |
| 2 | React.lazy의 import 실행 | Suspense의 fallback UI 표시 |
| 3 | 네트워크로 리모트 모듈 요청 | fallback 유지 (로딩 스피너 등) |
| 4 | 모듈 로드 완료 | 리모트 컴포넌트로 교체 |
네트워크 속도가 느린 환경(3G 등)에서는 fallback UI가 더 오래 표시된다. 이 시간 동안 사용자에게 시각적 피드백을 제공하는 것이 UX에 매우 중요하다.
2. ErrorBoundary를 통한 에러 격리
리모트 서버가 다운되었거나 네트워크 오류가 발생하면 컴포넌트 로딩이 실패한다. 이때 ErrorBoundary가 없으면 전체 앱이 크래시된다.
2.1 react-error-boundary 패키지 활용
jsx
import React, { Suspense } from "react";
import { ErrorBoundary } from "react-error-boundary";
const RemoteButton = React.lazy(
() => import("componentApp/Button")
);
const RemoteHeader = React.lazy(
() => import("componentApp/Header")
);
function App() {
return (
<div>
<h1>Main App</h1>
{/* 각 리모트 컴포넌트를 개별 ErrorBoundary로 격리 */}
<ErrorBoundary
fallback={<div>헤더를 불러올 수 없습니다</div>}
>
<Suspense fallback={<div>헤더 로딩 중...</div>}>
<RemoteHeader />
</Suspense>
</ErrorBoundary>
<ErrorBoundary
fallback={<div>버튼을 불러올 수 없습니다</div>}
>
<Suspense fallback={<div>버튼 로딩 중...</div>}>
<RemoteButton>확인</RemoteButton>
</Suspense>
</ErrorBoundary>
</div>
);
}2.2 ErrorBoundary + Suspense 중첩 규칙
ErrorBoundary <-- 가장 바깥: 에러 격리
└── Suspense <-- 중간: 로딩 상태 처리
└── RemoteComp <-- 가장 안쪽: 실제 리모트 컴포넌트| 계층 | 역할 | 대응하는 상태 |
|---|---|---|
| ErrorBoundary | 에러 발생 시 대체 UI 표시 | 로드 실패, 런타임 에러 |
| Suspense | 비동기 로딩 중 대기 UI 표시 | 네트워크 대기 상태 |
| 리모트 컴포넌트 | 실제 렌더링 대상 | 로드 성공 후 정상 렌더 |
각 리모트 컴포넌트를 개별 ErrorBoundary로 감싸는 것이 핵심이다. 하나의 리모트 컴포넌트 실패가 다른 리모트 컴포넌트나 Host 앱 전체에 영향을 주지 않도록 장애를 격리해야 한다.
3. 격리된 DOM 마운트 패턴 (Inject 패턴)
리모트 앱 전체를 Host의 특정 DOM 엘리먼트에 별도로 마운트하는 방식이다. 단순 컴포넌트 import가 아닌, 독립된 React 루트를 생성하여 리모트 앱의 라이프사이클을 완전히 분리한다.
3.1 Remote 측 - Injector 구현
tsx
// apps/isolated-app/src/injector.tsx
import React from "react";
import { createRoot, Root } from "react-dom/client";
import App, { AppProps } from "./App";
export const inject = (
parentElementId: string,
props: AppProps
) => {
const root: Root = createRoot(
document.getElementById(parentElementId)!
);
root.render(<App {...props} />);
// 언마운트 함수를 반환하여 정리 가능하게 함
return () => {
root.unmount();
};
};3.2 Host 측 - Inject 사용
tsx
// apps/main-app/src/App.tsx
import React, { useEffect } from "react";
const ELEMENT_ID = "isolated-app";
function App() {
useEffect(() => {
let unmount: () => void = () => {};
import("isolatedApp/injector")
.then(({ inject }) => {
unmount = inject(ELEMENT_ID, { name: "main" });
});
// 컴포넌트 해제 시 리모트 앱도 언마운트
return () => {
unmount();
};
}, []);
return (
<div>
<h1>Main App</h1>
{/* 리모트 앱이 마운트될 빈 컨테이너 */}
<div id={ELEMENT_ID} />
</div>
);
}3.3 Inject 패턴의 특징
| 특징 | 설명 |
|---|---|
| 독립 React 루트 | Host와 별도의 createRoot로 독자적 라이프사이클 보유 |
| 언마운트 보장 | inject 반환값으로 unmount 함수 제공, 메모리 누수 방지 |
| 프레임워크 독립 | Host가 React가 아니어도 DOM 엘리먼트만 있으면 inject 가능 |
| 스타일 격리 한계 | Shadow DOM이 아니므로 CSS 클래스 충돌은 별도 처리 필요 |
3.4 webpack.config.js에서 injector 노출
js
// apps/isolated-app/webpack.config.js
new ModuleFederationPlugin({
name: "isolatedApp",
filename: "remoteEntry.js",
exposes: {
// 컴포넌트가 아닌 inject 함수를 노출
"./injector": "./src/injector.tsx",
},
shared: { react: { singleton: true }, "react-dom": { singleton: true } },
});4. 마이크로프론트엔드 로딩의 4가지 필수 요소
마이크로프론트엔드에서 리모트 컴포넌트를 사용할 때 반드시 고려해야 할 4가지 요소가 있다.
| 요소 | 구현 기술 | 미적용 시 문제 |
|---|---|---|
| 지연 로딩 | React.lazy + import() | 초기 번들 크기 증가, 불필요한 코드 로드 |
| 로딩 UI | Suspense + fallback | 빈 화면으로 사용자 혼란 |
| 에러 격리 | ErrorBoundary | 리모트 장애 시 전체 앱 크래시 |
| 에러 UI | ErrorBoundary + fallback | 에러 상태에서 사용자 안내 불가 |
이 4가지 요소는 선택이 아닌 필수이다. 마이크로프론트엔드는 서버가 분리되어 있으므로 네트워크 장애, 배포 불일치 등 다양한 실패 시나리오가 발생할 수 있다.
핵심 정리
| 항목 | 내용 |
|---|---|
| React.lazy | 리모트 모듈을 동적 import로 지연 로딩 |
| Suspense | 로딩 중 fallback UI 표시, 네트워크 대기 시간 동안 시각적 피드백 |
| ErrorBoundary | 로딩 실패 시 에러를 격리하여 전체 앱 크래시 방지 |
| Inject 패턴 | 리모트 앱을 독립 React 루트로 마운트하고 unmount 함수로 정리 |
| 중첩 규칙 | ErrorBoundary > Suspense > RemoteComponent 순서로 감싸기 |
| 필수 4요소 | 지연 로딩 + 로딩 UI + 에러 격리 + 에러 UI |
다음 단계
- 공유 라이브러리 설정에서 shared 옵션의 세부 설정(singleton, requiredVersion, eager)과 버전 호환성 관리 전략을 학습한다