Skip to content

MFA 소비 컴포넌트 - 클래스 컴포넌트 래퍼

클래스 컴포넌트 기반의 MicroComponent 래퍼를 구현하여 다른 프로젝트의 마이크로 컴포넌트를 소비하는 공통 패턴을 학습한다

학습 목표

  1. 클래스 컴포넌트로 마이크로 컴포넌트 래퍼를 구현하는 이유를 이해한다
  2. loadScript를 활용한 동적 스크립트 로딩과 렌더링 흐름을 익힌다
  3. 마운트/언마운트/업데이트 생명주기에서 마이크로앱을 올바르게 관리할 수 있다
  4. Props 전달과 baseName 지원을 포함한 완전한 소비 인터페이스를 구현할 수 있다

본문

1. MicroComponent가 필요한 이유

지금까지 마이크로앱을 소비할 때마다 useRef, useEffect, loadScript를 반복 작성했다. 이를 하나의 재사용 가능한 공통 컴포넌트로 추출한다.

2. 클래스 컴포넌트를 선택한 이유

MicroComponent를 함수 컴포넌트가 아닌 클래스 컴포넌트로 구현하는 이유가 있다.

이유설명
React 버전 호환다양한 React 버전의 프로젝트에서 공통으로 사용 가능
명시적 생명주기componentDidMount, componentDidUpdate, componentWillUnmount로 마이크로앱 관리가 명확
Ref 관리createRef로 DOM 참조가 직관적
인스턴스 변수this.unmountFn으로 언마운트 함수를 저장

3. MicroComponent 전체 구현

전체 코드

typescript
import React from "react";
import { loadScript, MicroAppProps } from "../utils";

interface MicroComponentProps extends MicroAppProps {
  props?: Record<string, any>;
}

export default class MicroComponent extends React.Component<MicroComponentProps> {
  private containerRef = React.createRef<HTMLDivElement>();
  private unmountFn: (() => void) | undefined;

  async componentDidMount() {
    await this.renderMicroApp();
  }

  async componentDidUpdate(prevProps: MicroComponentProps) {
    // URL, appName, componentName이 변경되면 재렌더링
    if (
      prevProps.url !== this.props.url ||
      prevProps.appName !== this.props.appName ||
      prevProps.componentName !== this.props.componentName
    ) {
      this.cleanup();
      await this.renderMicroApp();
    }
  }

  componentWillUnmount() {
    this.cleanup();
  }

  private cleanup() {
    if (this.unmountFn) {
      this.unmountFn();
      this.unmountFn = undefined;
    }
  }

  private async renderMicroApp() {
    const { url, appName, componentName = "default", props } = this.props;
    const container = this.containerRef.current;
    if (!container) return;

    try {
      const microApp = await loadScript({ url, appName });
      const component = microApp[componentName] || microApp.default;

      if (!component?.render) return;

      // 렌더링 후 언마운트 함수 저장
      this.unmountFn = component.render(container, props);
    } catch (error) {
      console.error(`마이크로앱 로딩 실패: ${appName}`, error);
    }
  }

  render() {
    return <div ref={this.containerRef} />;
  }
}

4. 사용 예시

tsx
// Docs 프로젝트에서 Web의 ShoppingList 소비
<MicroComponent
  url="http://localhost:7002"
  appName="web"
  componentName="ShoppingList"
/>

// Web 전체 앱을 소비하면서 baseName 전달
<MicroComponent
  url="http://localhost:7002"
  appName="web"
  componentName="default"
  props=&#123;&#123; baseName: "/web" &#125;&#125;
/>

// Web 프로젝트에서 Docs의 MailList 소비
<MicroComponent
  url="http://localhost:7001"
  appName="docs"
  componentName="MailList"
/>

5. 마이크로앱 Export 측 인터페이스

소비 컴포넌트가 올바르게 동작하려면 Export하는 측에서 일관된 인터페이스를 제공해야 한다.

Export 측 구현

typescript
// docs/index.tsx
const renderApp = (container: HTMLElement, props?: any) => {
  const root = ReactDOM.createRoot(container);
  root.render(
    <GlobalProvider shadowRoot={props?.shadowRoot}>
      <App baseName={props?.baseName} />
    </GlobalProvider>
  );
  return () => root.unmount();
};

const renderMailList = (container: HTMLElement, props?: any) => {
  const root = ReactDOM.createRoot(container);
  root.render(
    <GlobalProvider shadowRoot={props?.shadowRoot}>
      <MailList />
    </GlobalProvider>
  );
  return () => root.unmount();
};

// UMD Export
window.docs = {
  default: { render: renderApp },
  MailList: { render: renderMailList },
};

6. 라우팅과 MicroComponent 통합

tsx
// Docs 프로젝트 App.tsx
function App({ baseName }: { baseName?: string }) {
  return (
    <BrowserRouter basename={baseName}>
      <Header />
      <Routes>
        <Route path="/" element={<MailList />} />
        <Route
          path="/shopping-list"
          element={
            <MicroComponent
              url="http://localhost:7002"
              appName="web"
              componentName="ShoppingList"
            />
          }
        />
        <Route
          path="/web/*"
          element={
            <MicroComponent
              url="http://localhost:7002"
              appName="web"
              props=&#123;&#123; baseName: "/web" &#125;&#125;
            />
          }
        />
      </Routes>
    </BrowserRouter>
  );
}

7. 오류 처리와 로딩 상태

typescript
interface MicroComponentState {
  loading: boolean;
  error: string | null;
}

export default class MicroComponent extends React.Component<
  MicroComponentProps,
  MicroComponentState
> {
  state: MicroComponentState = { loading: true, error: null };

  private async renderMicroApp() {
    this.setState({ loading: true, error: null });
    try {
      // ... 로딩 로직
      this.setState({ loading: false });
    } catch (error) {
      this.setState({
        loading: false,
        error: `${this.props.appName} 로드 실패`,
      });
    }
  }

  render() {
    const { loading, error } = this.state;
    return (
      <>
        {loading && <div>로딩...</div>}
        {error && <div style=&#123;&#123; color: "red" &#125;&#125;>{error}</div>}
        <div ref={this.containerRef} />
      </>
    );
  }
}

핵심 정리

  1. 공통 소비 컴포넌트: MicroComponent 클래스 컴포넌트로 마이크로앱 소비 로직을 추상화하여 반복 코드를 제거한다
  2. 생명주기 관리: componentDidMount에서 로딩, componentDidUpdate에서 재렌더링, componentWillUnmount에서 정리를 수행한다
  3. 일관된 인터페이스: Export 측은 { render, unmount } 패턴을, 소비 측은 url, appName, componentName, props로 통일한다
  4. Props 전달: baseName, shadowRoot 등 런타임 설정값을 props로 전달하여 유연한 통합을 지원한다

다음 단계

레거시 프로젝트 구성 ->