Module Federation 기능을 사용해서 레거시 Vue2 프로젝트에 React 를 띄운 후 점진적 마이그레이션을 하려던 시도는, 레거시 프로젝트의 webpack이 4버전인 관계로 무산되었다.
webpack 버전 업그레이드로 인한 사이드이펙트를 고려하는 대신, Module Federation 을 사용하는 대신 script 를 mount/unmount 하는 방식으로 진행하기로 했는데, 다헹히 큰 틀에서의 사용성 차이는 없어 비교적 수월하게 테스트가 완료됐다.
이와 함께 MF를 사용하지 않는다면, 굳이 webpack을 사용할 필요가 없어져 Vite를 사용하는 방안을 고려했다.
현재 프로젝트는 SSR 사용 할 필요 없음, SEO 고려 할 필요 없음 > Next.js 등 프레임워크 사용 할 필요 없음 (무거움)
Webpack 또한 자유도는 높으나 설정비용과 유지보수 부담이 크고, 가장 큰 이유로 번들링이 너무 오래걸리기 때문에 가벼운 Vite로 설정하는 것을 고려한다.
혹시 모를 미래에 MF를 사용할 것을 고려하여 webpack 으로 서비스 돌리기 vs Vite로 초기 세팅 후 필요 시 webpack 으로 전환 이라는 선택지에서 후자를 선택했는데, 이후 webpack 으로 전환 시 비용을 줄이기 위해 번들러와 서비스코드를 분리하는 구조로 설계를 진행한다.
src/
├── app/
├── features/
├── shared/
├── mount/
- window mount 방식 유지
- entry 분리
- shared 모듈 분리
이럴 경우 Webpack으로 옮겨도 구조가 유지되기 때문에 전환 비용이 줄어든다고 하니 (gpt) 일단 믿어보기로 했다.
우선 기존의 webpack 으로 올린 프로젝트를 사용할 순 없으므로 (vite로 설정을 바꿀 순 있지만 작업이 복잡해져서 리스크가 있다고 함) vite버전 프로젝트를 새로 생성하고, config 설정을 잡아준다. (도르마무... 도르마무...)
npm create vite@latest react-remote
cd react-remote
npm install
npm install react react-dom
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
lib: {
entry: 'src/main.ts',
name: 'ReactRemote',
fileName: 'react-app',
formats: ['iife'], // script 방식으로 사용하기 위해서
},
rollupOptions: {
output: {
entryFileNames: 'react-app.js',
},
},
},
server: {
port: 3001,
cors: true, // 외부 호스트 접근 허용(현재는 vue 프로젝트)
},
});
다행히도 설정 외의 코드는 현재로서는 수정 할 필요가 없으니 이전에 작성해둔 프로젝트의 src 폴더를 통째로 새로 작성한 프로젝트에 옮겨심으면 된다.
다만, 주의 할 점은 webpack과 다르게 vite는 별도의 build 없이 실행 할 경우 dist 파일이 생성되지 않으므로 build 후 serve를 돌리거나, npx 로 실행을 할 필요가 있다.
npm run build
npx serve dist
# or
serve -l {port} dist
나는 아래의 명령어를 package.json에 등록하고 사용했다. serve 명령어가 없다는 에러가 발생할 수 있는데, 그럴 땐 침착하고 install 해주자
npm install -D serve
이후 vite > webpack 으로 변환이 필요할 때에도 동일한 흐름으로 진행하면 된다.
** 실제 전환 시 체크리스트 **
[ 필수 ]
> require → import 변경
> env 변수 변경
> alias 설정
> mount 유지
[ 선택 ]
> CSS 구조 정리
> shared 분리
세팅을 다 하고 실행했는데, CORS 에러가 나를 반겼다
[Vite] Cross-origin requests for classic scripts must be made with CORS mode enabled. Make sure to set the "crossorigin" attribute on your <script> tag.
리액트 프로젝트의 cors 설정은 config에서 등록해줬으니, 사용할 vue 프로젝트의 코드에 crossOrigin 설정값을 'anonymous' 로 추가해주자.
// ReactWrapper.vue
methods: {
loadScript() {
return new Promise((res, rej) => {
if(window.ReactRemote) {
return res()
}
const script = document.createElement('script');
script.src = 'http://localhost:3001/react-app.js';
script.crossOrigin = 'anonymous'; // << 이 부분
script.onload = res;
script.onerror = rej;
document.head.appendChild(script);
})
}
},
vue 구조 잡기는 이전 글에 작성되어있으니 참고하기...
이때, 프로젝트 1은 localhost, 프로젝트 2는 127.0.0.1 형식으로 실행하면 서버측 cors가 또 나올 수 있으니, 호스트를 통일하도록 하자.
cors를 해결했더니
react-app.js:242 Uncaught ReferenceError: process is not defined
에러가 뜬다. 왜 그런고 하니 vite에서는 process.env ... 같은 형식을 사용하지 않는데 설치한 내부 라이브러리에서 사용하는 곳이 있는 듯 하다. 그렇다면 왜 프로젝트 자체를 실행할 때 런타임에서는 에러가 나지 않느냐? 하면 그때는 build 없이 모듈을 그대로 브라우저로 가져가서 실행하는 형태인데, 지금 내가 사용하려는 script 방식은 빌드가 필수이기 때문에 이 과정에서 내부 라이브러리를 번들링하면서 생기는 것. node polufill 플러그인을 다운받아 해결하자
npm install vite-plugin-node-polyfills
// vite.config.ts
import { nodePolyfills } from 'vite-plugin-node-polyfills';
plugins: [
react(),
babel({ presets: [reactCompilerPreset()] }),
nodePolyfills() // << 이거 추가
],
** 토막상식, polyfill 이란? **
폴리필은 기본적으로 지원하지 않는 이전 브라우저에서 최신 기능을 제공하는 데 필요한 코드 (일반적으로 웹의 JavaScript)입니다.
예를 들어, 폴리필은 독점 IE 필터를 사용하여 IE7에서 text-shadow의 기능을 모방하거나, JavaScript를 사용해 스타일을 적절하게 동적으로 조정함으로써 rem 단위 또는 미디어쿼리를 모방하거나, 또는 추가적으로 필요한 부분에 사용될 수 있습니다.
출처:
https://developer.mozilla.org/ko/docs/Glossary/Polyfill
이제 script 형식으로도 기존 프로젝트에 새로 띄운 프로젝트가 정상적으로 이식되었다.
MF 등 script형의 장점이라면 모듈 프로젝트 서버를 계속 띄워놓지 않고, 빌드 후 js 파일만 변경하면 된다는 것을 갑자기 깨닫고(!) 이를 활용해 기존 vue 프로젝트 내부에 빌드한 리액트 js 파일을 넣어놓고 하나의 host로 관리하면 되겠다 싶었다. 굿~
이때, 모듈 프로젝트가 수정되어 새로 빌드를 하더라도 캐싱 문제로 인해 변경사항이 반영되지 않을 수 있는데, 그때마다 호스트가 되는 프로젝트를 새로 돌릴 순 없는 일이다. (취지에도 맞지 않음)
운영 환경에서는 빌드 시 해시를 추가해서 js 파일을 생성하고, 호스트 프로젝트 내에서는 manifest 를 활용해 번들파일을 관리하도록 하자.
// vite.config.ts
rollupOptions: {
output: {
entryFileNames: 'react-app.[hash].js'
}
}
// vue 에서 로드해서 사용
fetch('/react-manifest.json')
.then(res => res.json())
.then(data => {
const script = document.createElement('script');
script.src = `/${data.file}`;
script.crossOrigin = 'anonymous';
document.head.appendChild(script);
});
manifest.json도 물론 캐싱이 되기 때문에, js와 다른 캐싱 정책을 가져가야 한다. no-cache 와 no-store 정책 중 하나를 선택하면 되는데, 이건 인프라쪽에 부탁하도록 하자.
이때, "manifest.json을 안 쓰고 js 번들파일 자체를 no-cache 로 하면 안되냐?" 라고 할 수 있는데, 맞는 말이다. 다만 번들파일은 해시형태로 캐싱처리를 하게되면, 이전 버전으로의 롤백이 간단하기 때문에 예기치 못한 상황에서 서비스가 터졌을 때 복구가 쉽겠다! 라는 생각에 캐싱정책을 저렇게 가져가면 좋겠다 싶었다.
이제 기본 세팅을 마쳤으니 내부적으로 개발을 진행하면 된다.
레거시 프로젝트에 새로운 프로젝트 이식해서 마이그레이션 토대 쌓기 끗!
'Client > React' 카테고리의 다른 글
| wrapper로 constant 관리하기 (0) | 2026.04.27 |
|---|---|
| Modul Federation 으로 Vue2 > React 마이그레이션 하기 (0) | 2026.04.10 |
| [React] forwardRef (0) | 2023.01.03 |