[MFE]한 지붕 두 React 버전 공존

JACKJACK·2026년 2월 15일
post-thumbnail

1. 배경과 목적

  • MFE 환경에서 Host와 Remote의 React 버전이 서로 다를 때 발생하는 충돌 문제를 확인
  • Shared 설정 없이 Remote를 불러올 때 React 버전 충돌을 제거할 수 있는 방법을 검증

2. Remote랑 Host간 버전 충돌 확인

2-1. React 버전 충돌 확인

Host와 Remote의 React 인스턴스가 다를 때(Remote에서 shared 설정이 없는 경우)

Host (React 19)
├─ React DOM 트리
└─ App 컴포넌트
└─ Remote 컴포넌트 (React 18 or 19 인스턴스 사용) ❌

  • Host는 React 19 인스턴스 사용
  • Remote는 React 18,19 각 인스턴스를 사용하는 케이스
  • React DOM tree에 다른 인스턴스 element를 넣으려고 해서 에러 발생
    (multiple copies of the 'react' package is used)

Host에서 shared 선언을 하지 않는 경우 테스트

Host (React 19)
├─ React DOM 트리
└─ App 컴포넌트
└─ Remote 컴포넌트 (React 19 인스턴스 사용) ❌

  • Host와 Remote가 각자 React 19 포함
  • 결국 두 개의 React 인스턴스 존재 → 마찬가지로 React DOM tree 에러 발생
    (multiple copies of the 'react' package is used)

Host와 Remote 모두 같은 React로 shared 설정

Shared React 19 (singleton)
├─ Host
└─ Remote

  • Host와 Remote가 같은 React 인스턴스 사용
  • React DOM tree에 문제 없이 렌더링 가능 ✅

핵심 결과

  • React는 싱글턴 라이브러리
  • Host와 Remote에서 같은 버전 + singleton shared 설정 필수
  • shared를 빼거나 버전이 다르면 → 두 개 React 인스턴스 → 렌더링 에러가 발생한다.

“위 테스트를 통해 React는 싱글턴으로 관리되어야 한다는 것을 확인했으며, 다음으로 Shared 없이도 충돌을 제거할 수 있는 방법을 실험”

2-2. Shared 없이 React 버전 충돌 제거

MFE 환경에서 Host React 19와 Remote React 16/18/19가 섞여 있을 때
일반적인 React 컴포넌트를 Host DOM Tree 안에 직접 렌더링하면 multiple React copies 에러 발생

충돌을 없애기 위해 Shared 설정 없이 mount/unmount 방식으로 Remote를 DOM 컨테이너 안에 렌더링하여 충돌을 제거하는 방식을 채택해 보았다.

Remote 앱 정의 (mount/unmount)

Remote 앱을 단순 함수로 mount/unmount 제공

import { createRoot, type Root } from 'react-dom/client'
import App from './App.tsx'
import './App.css'
import './index.css'

let reactRoot: Root | null = null

export function mount(element: HTMLElement, props: Record<string, unknown> = {}) {
  console.log('🔥 Mounting React 19 app with props:', props)

  if (reactRoot) {
    console.warn('⚠️ React 19 app already mounted, unmounting first...')
    unmount(element)
  }

  // Create React 19 root and render
  reactRoot = createRoot(element)
  reactRoot.render(<App {...props} />)

  console.log('✅ React 19 app mounted successfully')
}

export function unmount(element: HTMLElement) {
  console.log('🧹 Unmounting React 19 app...')

  if (reactRoot) {
    reactRoot.unmount()
    reactRoot = null
    console.log('✅ React 19 app unmounted successfully')
  }

  // Clear the container
  element.innerHTML = ''
}

// For backwards compatibility, also export as default
export default {
  mount,
  unmount
}

Host에서 사용

Host는 Remote를 독립 컨테이너(div) 안에서 mount
Remote React 인스턴스는 Host DOM Tree와 직접 섞이지 않음

import { useFederation } from '@/shared/context/federationContext.tsx'
import { useEffect, useRef } from 'react'

export const RemoteReact19 = () => {
  const federation = useFederation()
  const ref = useRef<HTMLDivElement>(null)

  useEffect(() => {
    let isMounted = false
    let mount: ((el: HTMLElement, props?: any) => void) | undefined
    let unmount: ((el: HTMLElement) => void) | undefined

    const loadRemoteModule = async () => {
      try {
        console.log('🔄 Loading React 19 remote module...')
        const remoteModule = await federation.loadRemote('mf2/mount') as {
          mount: (el: HTMLElement, props?: any) => void
          unmount: (el: HTMLElement) => void
        }

        mount = remoteModule.mount
        unmount = remoteModule.unmount

        if (ref.current && mount) {
          console.log('🔥 Mounting React 19 app...')
          mount(ref.current, {
            message: 'Hello from Host!',
            timestamp: new Date().toLocaleString()
          })
          isMounted = true
          console.log('✅ React 19 app mounted successfully')
        }
      } catch (error) {
        console.error('❌ Failed to load React 19 remote module:', error)
      }
    }

    loadRemoteModule()

    return () => {
      if (unmount && ref.current && isMounted) {
        console.log('🧹 Cleaning up React 19 app...')
        unmount(ref.current)
      }
    }
  }, [federation])

  return (
    <div style={{
      padding: '20px',
      color: 'var(--text-primary)',
      backgroundColor: 'var(--bg-primary)'
    }}>
      <h1 style={{
        color: 'var(--text-primary)',
        marginBottom: '20px',
        fontSize: '24px',
        fontWeight: 'bold'
      }}>React 19 페이지 영역</h1>
      <div ref={ref}></div>
    </div>
  )
}

mount/unmount 방식 사용 결과

Host DOM 안에 Remote DOM을 마운트하지만, React Fiber Tree 단위에서 Host와 Remote가 독립적이므로 충돌이 없음.
(Fiber Tree: React 내부에서 컴포넌트 렌더링과 업데이트를 관리하는 자료구조)

Host React Fiber Tree (React 19)
┌───────────────────────────────────────────────────────────┐
│ <RemoteLoader /> (Host Component)                    │
│                                                      │
│  ┌─────────────────────────────────────────────────────┐  │
│  │ <div ref={domRef}>  (Host DOM Container)        │  │
│  │                                                 │  │
│  │  [ Mounting Point ]                             │  │
│  │          │                                     │  │
│  │          ▼                                     │  │
│  │  Remote React Fiber Tree (React 18 or 19)      │  │
│  │  ┌───────────────────────────────────────────────┐  │  │
│  │  │ <RemoteApp />                             │  │  │
│  │  │                                           │  │  │
│  │  │  • Independent State / Effect             │  │  │
│  │  │  • Own React Runtime                      │  │  │
│  │  └───────────────────────────────────────────────┘  │  │
│  └─────────────────────────────────────────────────────┘  │
└───────────────────────────────────────────────────────────┘

mount/unmount 컨테이너를 독립적으로 관리 → 여러 Remote React 인스턴스 공존 가능

3. 결과

  • Shared 없이 라이브러리를 사용할 때 버전 충돌 제거 가능(필요한 조건은 아래와 같음)
    → Host DOM Tree와 Remote React DOM Tree를 겹치지 않도록 격리
    → mount/unmount 방식으로 Remote를 독립적으로 렌더링
  • 추가로 Props 전달 가능하나 타입 검증, 유지보수 리소스 발생할 가능성이 있기에 Minimal Props만 전달하거나 브라우저 전역 스토리지를 사용하는 방법을 사용하는것이 좋아보임
profile
러닝커브를 빠르게 극복하자🎢

0개의 댓글