Skip to content

리모트 컴포넌트 로딩

다른 서버의 컴포넌트를 런타임에 가져올 때는 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 동작 방식:

단계동작사용자에게 보이는 화면
1Host 앱 렌더링 시작Host 자체 컨텐츠 표시
2React.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()초기 번들 크기 증가, 불필요한 코드 로드
로딩 UISuspense + fallback빈 화면으로 사용자 혼란
에러 격리ErrorBoundary리모트 장애 시 전체 앱 크래시
에러 UIErrorBoundary + fallback에러 상태에서 사용자 안내 불가

이 4가지 요소는 선택이 아닌 필수이다. 마이크로프론트엔드는 서버가 분리되어 있으므로 네트워크 장애, 배포 불일치 등 다양한 실패 시나리오가 발생할 수 있다.


핵심 정리

항목내용
React.lazy리모트 모듈을 동적 import로 지연 로딩
Suspense로딩 중 fallback UI 표시, 네트워크 대기 시간 동안 시각적 피드백
ErrorBoundary로딩 실패 시 에러를 격리하여 전체 앱 크래시 방지
Inject 패턴리모트 앱을 독립 React 루트로 마운트하고 unmount 함수로 정리
중첩 규칙ErrorBoundary > Suspense > RemoteComponent 순서로 감싸기
필수 4요소지연 로딩 + 로딩 UI + 에러 격리 + 에러 UI

다음 단계

  • 공유 라이브러리 설정에서 shared 옵션의 세부 설정(singleton, requiredVersion, eager)과 버전 호환성 관리 전략을 학습한다