본문 바로가기

Client/React

Vue 프로젝트에 React 도킹하기 (script 방식)

반응형

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