테마
MFA 소비 컴포넌트 - 클래스 컴포넌트 래퍼
클래스 컴포넌트 기반의 MicroComponent 래퍼를 구현하여 다른 프로젝트의 마이크로 컴포넌트를 소비하는 공통 패턴을 학습한다
학습 목표
- 클래스 컴포넌트로 마이크로 컴포넌트 래퍼를 구현하는 이유를 이해한다
- loadScript를 활용한 동적 스크립트 로딩과 렌더링 흐름을 익힌다
- 마운트/언마운트/업데이트 생명주기에서 마이크로앱을 올바르게 관리할 수 있다
- 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={{ baseName: "/web" }}
/>
// 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={{ baseName: "/web" }}
/>
}
/>
</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={{ color: "red" }}>{error}</div>}
<div ref={this.containerRef} />
</>
);
}
}핵심 정리
- 공통 소비 컴포넌트: MicroComponent 클래스 컴포넌트로 마이크로앱 소비 로직을 추상화하여 반복 코드를 제거한다
- 생명주기 관리:
componentDidMount에서 로딩,componentDidUpdate에서 재렌더링,componentWillUnmount에서 정리를 수행한다 - 일관된 인터페이스: Export 측은
{ render, unmount }패턴을, 소비 측은url,appName,componentName,props로 통일한다 - Props 전달:
baseName,shadowRoot등 런타임 설정값을 props로 전달하여 유연한 통합을 지원한다