테마
상태 공유 - Context와 Redux
Module Federation 환경에서 React Context와 Redux Store를 여러 마이크로앱 간에 공유하려면 동일한 React 인스턴스를 보장하고, 상태 라이브러리를 별도 패키지로 분리하여 일관된 상태 관리 계층을 구성해야 한다.
학습 목표
- React Context를 Module Federation으로 연결된 앱 간에 공유하는 방법을 이해한다
- 공유 패키지를 만들어 Context Provider를 양쪽 앱에서 사용하는 구조를 설계할 수 있다
- Redux Store를 여러 마이크로앱에서 사용하는 패턴을 구현할 수 있다
- 상태 공유 시 React 싱글톤 보장, 패키지 버전 정책 등의 주의점을 파악한다
- 상태 공유와 상태 격리의 트레이드오프를 판단할 수 있다
1. Context 공유의 전제 조건
React Context가 앱 간에 정상적으로 공유되려면 동일한 React 인스턴스를 사용해야 한다. React가 서로 다른 인스턴스로 로드되면 createContext로 생성된 컨텍스트 객체가 다른 React 트리에서 인식되지 않는다.
필수 webpack.config.js 설정:
js
// Host와 Remote 양쪽에 동일하게 설정
shared: {
react: { singleton: true, requiredVersion: "^18.0.0" },
"react-dom": { singleton: true, requiredVersion: "^18.0.0" },
}React가 singleton으로 공유되고 있다면, 마이크로앱 한쪽에서
createContext를 호출하고 다른 쪽에서useContext로 읽는 것이 정상 동작한다.
2. 공유 패키지를 통한 Context 공유
실무에서는 Context를 특정 마이크로앱에 직접 작성하지 않고, 별도의 공유 패키지로 분리하여 양쪽 앱에서 동일한 Context 객체를 참조하도록 구성한다.
2.1 공유 패키지 작성
tsx
// packages/shared-library/src/NameContext.tsx
import React from "react";
export const NameContext = React.createContext<string | null>(null);
interface NameProviderProps {
name: string;
children: React.ReactNode;
}
export const NameProvider: React.FC<NameProviderProps> = ({
name,
children,
}) => {
return (
<NameContext.Provider value={name}>
{children}
</NameContext.Provider>
);
};
// 편의를 위한 커스텀 훅
export const useNameContext = () => {
const context = React.useContext(NameContext);
if (context === null) {
throw new Error(
"useNameContext는 NameProvider 안에서 사용해야 합니다"
);
}
return context;
};ts
// packages/shared-library/src/index.ts
export { NameContext, NameProvider, useNameContext } from "./NameContext";2.2 Host 앱에서 Provider 제공
tsx
// apps/main-app/src/App.tsx
import React, { Suspense } from "react";
import { NameProvider } from "shared-library";
const RemoteGreeting = React.lazy(
() => import("componentApp/Greeting")
);
function App() {
return (
<NameProvider name="hello">
<h1>Main App</h1>
<Suspense fallback={<div>로딩 중...</div>}>
<RemoteGreeting />
</Suspense>
</NameProvider>
);
}2.3 Remote 앱에서 Context 소비
tsx
// apps/component-app/src/Greeting.tsx
import React from "react";
import { useNameContext } from "shared-library";
const Greeting: React.FC = () => {
const name = useNameContext();
return <div>안녕하세요, {name}!</div>;
};
export default Greeting;2.4 shared-library의 shared 설정
공유 패키지도 webpack.config.js의 shared에 등록해야 한다.
js
// Host와 Remote 양쪽의 webpack.config.js
shared: {
react: { singleton: true },
"react-dom": { singleton: true },
"shared-library": { singleton: true },
// workspace: 프로토콜 문제를 피하기 위해
// requiredVersion은 지정하지 않음
},3. Redux Store 공유 패턴
Redux를 여러 마이크로앱에서 공유하는 방법은 크게 두 가지 패턴으로 나뉜다.
3.1 패턴 A: 단일 Store 공유 (중앙 집중형)
tsx
// packages/shared-store/src/store.ts
import { configureStore } from "@reduxjs/toolkit";
import userReducer from "./slices/userSlice";
export const createAppStore = () =>
configureStore({
reducer: {
user: userReducer,
// 리모트 앱의 슬라이스는 동적으로 주입 가능
},
});
export type AppStore = ReturnType<typeof createAppStore>;
export type RootState = ReturnType<AppStore["getState"]>;
export type AppDispatch = AppStore["dispatch"];tsx
// apps/main-app/src/App.tsx
import { Provider } from "react-redux";
import { createAppStore } from "shared-store";
const store = createAppStore();
function App() {
return (
<Provider store={store}>
<h1>Main App</h1>
<Suspense fallback={<div>로딩 중...</div>}>
<RemoteOrderPage />
</Suspense>
</Provider>
);
}3.2 패턴 B: 이벤트 기반 느슨한 결합
각 마이크로앱이 자체 Store를 보유하고, Custom Event를 통해 필요한 데이터만 교환한다.
tsx
// Host에서 사용자 정보 변경 시 이벤트 발행
const dispatchUserChange = (user: User) => {
window.dispatchEvent(
new CustomEvent("user:changed", { detail: user })
);
};
// Remote에서 이벤트 수신
useEffect(() => {
const handler = (e: CustomEvent<User>) => {
dispatch(setUser(e.detail));
};
window.addEventListener("user:changed", handler as EventListener);
return () => {
window.removeEventListener("user:changed", handler as EventListener);
};
}, []);3.3 패턴 비교
| 항목 | 패턴 A: 단일 Store | 패턴 B: 이벤트 기반 |
|---|---|---|
| 결합도 | 높음 (같은 Store 참조) | 낮음 (이벤트로만 통신) |
| 일관성 | 보장됨 (단일 진실 소스) | 개발자가 동기화 관리 |
| 독립 배포 | 제한적 (Store 구조 변경 시 영향) | 용이 (인터페이스만 합의) |
| 디버깅 | Redux DevTools로 한곳에서 추적 | 이벤트 추적이 복잡 |
| 적합한 경우 | 모노레포, 긴밀한 협업 | 별도 레포, 독립 팀 |
4. 상태 공유 시 주의사항
4.1 React 싱글톤 보장
js
// 반드시 singleton: true로 설정
shared: {
react: { singleton: true },
"react-dom": { singleton: true },
"react-redux": { singleton: true }, // Redux도 싱글톤 필수
}react-redux도 singleton으로 설정하지 않으면 Provider와 Consumer가 서로 다른 react-redux 인스턴스를 참조하여 Store를 찾지 못하는 에러가 발생한다.
4.2 모노레포 내부 패키지 버전 정책
| 정책 | 설명 | 장점 | 단점 |
|---|---|---|---|
| 동기 배포 | 공유 패키지 변경 시 모든 마이크로앱 동시 배포 | 항상 최신 버전 보장 | 배포 범위가 넓어짐 |
| 독립 버전 | 각 앱이 공유 패키지의 특정 버전을 고정 | 독립 배포 가능 | 버전 불일치 가능성 |
4.3 상태 격리가 더 나은 경우
모든 상태를 공유할 필요는 없다. 다음의 경우 상태 격리가 더 적합하다:
- 리모트 앱이 완전히 독립된 비즈니스 도메인을 가질 때
- 팀 간 결합도를 최소화해야 할 때
- 리모트 앱이 다른 프레임워크를 사용할 가능성이 있을 때
- Inject 패턴으로 격리 마운트를 사용할 때
원칙: 공유할 상태는 최소화하고, 반드시 필요한 사용자 인증 정보나 테마 같은 전역 상태만 공유한다. 나머지는 각 마이크로앱이 자체적으로 관리하는 것이 독립성과 유지보수성을 높인다.
핵심 정리
| 항목 | 내용 |
|---|---|
| Context 공유 전제 | React가 singleton으로 공유되어야 함 |
| 공유 패키지 | Context/Store를 별도 패키지로 분리하여 양쪽 앱에서 참조 |
| Redux 패턴 | 단일 Store 공유(중앙 집중) vs 이벤트 기반(느슨한 결합) |
| react-redux | 반드시 singleton으로 shared 설정해야 Provider 정상 동작 |
| 버전 정책 | 모노레포 배포 전략과 shared 설정을 일치시켜야 함 |
| 설계 원칙 | 공유 상태는 최소화, 도메인 상태는 각 앱이 독립 관리 |
다음 단계
- TypeScript 연동에서 리모트 컴포넌트의 타입 정의 문제와 해결 전략을 학습한다