Skip to content

Webpack UMD 빌드 설정과 런타임 인젝션

CRA 기반 레거시 프로젝트에서 eject 없이 Webpack 설정을 커스터마이징하여 UMD 라이브러리로 빌드하고, 다른 어플리케이션에서 런타임으로 마이크로앱을 인젝션하는 방법을 학습한다.

학습 목표

  1. UMD(Universal Module Definition) 형식의 개념과 동작 원리를 설명할 수 있다.
  2. react-app-rewired를 사용하여 CRA의 Webpack 설정을 eject 없이 커스터마이징할 수 있다.
  3. config-overrides.js에서 entry/output을 변경하여 UMD 라이브러리 빌드를 구성할 수 있다.
  4. 개발 서버(style-loader)와 프로덕션(extracted CSS)의 차이를 인지하고 대응할 수 있다.
  5. Docker와 Nginx를 활용하여 프로덕션 환경에서 마이크로앱 동작을 검증할 수 있다.

1. UMD(Universal Module Definition) 형식 소개

UMD는 JavaScript 모듈을 다양한 환경에서 사용할 수 있도록 하는 범용 모듈 정의 형식이다. AMD, CommonJS, 전역 변수 방식을 모두 지원하여, 소비하는 쪽의 모듈 시스템에 관계없이 동작한다.

1.1 UMD가 MFA에서 적합한 이유

특성MFA에서의 이점
환경 무관소비하는 앱의 모듈 시스템에 의존하지 않음
window 객체 등록<script> 태그만으로 로드 후 즉시 사용 가능
설정 최소화소비하는 쪽에서 별도 번들러 설정 불필요
레거시 호환Module Federation을 지원하지 않는 환경에서도 동작

UMD로 빌드된 마이크로앱은 다음과 같이 동작한다:

html
<!-- 소비하는 어플리케이션 -->
<script src="http://micro-app-server:7001/static/js/main.js"></script>
<script>
  // window.docs 객체로 접근 가능
  const { App } = window.docs;
  App.render(document.getElementById('micro-app-container'));
</script>

1.2 UMD vs Module Federation 비교

항목Runtime Injection (UMD)Module Federation
의존성 공유불가 (각 앱이 독립 번들)가능 (shared 설정)
번들 크기큼 (React 등 중복 포함)작음 (공유 의존성 제외)
설정 복잡도낮음 (output.library만 설정)높음 (ModuleFederationPlugin)
레거시 호환성뛰어남Webpack 5 이상 필요
컴포넌트 간 통신제한적 (이벤트, props 전달)풍부 (shared scope)
적합한 상황다양한 기술 스택의 레거시 환경기술 스택이 통일된 환경

2. react-app-rewired로 CRA 설정 커스터마이징

CRA(Create React App)는 Webpack 설정을 내부에 숨기고 있다. 이 설정을 변경하려면 eject를 실행해야 하지만, 한번 eject하면 되돌릴 수 없고 CRA의 업데이트를 받을 수 없다.

react-app-rewired는 eject 없이 CRA의 Webpack 설정을 오버라이드할 수 있게 해주는 도구다.

2.1 설치

bash
cd apps/docs
npm install react-app-rewired --save-dev

2.2 package.json 스크립트 변경

react-scriptsreact-app-rewired로 교체한다:

json
{
  "scripts": {
    "dev": "react-app-rewired start",
    "build": "react-app-rewired build",
    "test": "react-app-rewired test"
  }
}

2.3 config-overrides.js 생성

프로젝트 루트에 config-overrides.js 파일을 생성한다:

javascript
module.exports = function override(config, env) {
  // Webpack 설정을 수정하고 반환
  return config;
};

이 함수는 CRA의 기본 Webpack config 객체를 인자로 받아 수정한 후 반환한다.

3. UMD 라이브러리 빌드를 위한 config-overrides.js 설정

3.1 기본 UMD 출력 설정

javascript
module.exports = function override(config, env) {
  // UMD 라이브러리로 빌드
  config.output.library = {
    name: 'docs',       // window.docs로 접근 가능
    type: 'umd',        // UMD 형식
  };

  return config;
};

이 설정만으로 index.tsx에서 export한 모든 것이 window.docs 객체 하위에 등록된다.

tsx
// apps/docs/src/index.tsx
export function hi() {
  return 'Hello from docs micro-app!';
}

// 브라우저 콘솔에서 확인
// window.docs.hi()  => "Hello from docs micro-app!"

3.2 컴포넌트 렌더 함수 export

마이크로앱의 핵심은 외부에서 특정 DOM 요소에 컴포넌트를 렌더링할 수 있도록 함수를 제공하는 것이다:

tsx
// apps/docs/src/index.tsx
const App = lazy(() => import('./App'));

export function render(container: HTMLElement) {
  const root = createRoot(container);
  root.render(
    <ShadowDOM>
      <Suspense fallback={<div>Loading...</div>}><App /></Suspense>
    </ShadowDOM>
  );
}

// SPA로 독립 실행될 때만 자동 렌더
const rootEl = document.getElementById('root');
if (rootEl && rootEl.childElementCount === 0) render(rootEl);

핵심 포인트: SPA로서 독립 실행될 때와 마이크로 컴포넌트를 export하는 컨테이너 앱으로서 작동할 때를 분리해야 한다.

3.3 publicPath 설정

Lazy loading이나 코드 스플리팅을 사용하는 경우, 청크 파일의 로드 경로를 올바르게 설정해야 한다:

javascript
module.exports = function override(config, env) {
  config.output.library = {
    name: 'docs',
    type: 'umd',
  };

  // 청크 파일의 기본 경로를 마이크로앱 서버로 지정
  config.output.publicPath = 'http://localhost:7001/';

  return config;
};

publicPath를 설정하지 않으면 소비하는 앱(예: localhost:7002)에서 청크를 로드할 때 자신의 도메인에서 찾으려 하여 404 오류가 발생한다.

3.4 HtmlWebpackPlugin 설정 변경

CRA의 기본 설정에서 JS는 defer 속성으로 로드된다. 이를 blocking으로 변경하면 스크립트 실행 순서를 보장할 수 있다. config-overrides.js에 다음을 추가한다:

javascript
const htmlPlugin = config.plugins.find(
  (p) => p.constructor.name === 'HtmlWebpackPlugin'
);
if (htmlPlugin) {
  htmlPlugin.options.scriptLoading = 'blocking'; // defer -> blocking
  htmlPlugin.options.inject = 'head';             // body -> head
}
  • scriptLoading: 'blocking'defer를 제거하여 스크립트가 순차 실행되도록 한다.
  • inject: 'head'는 스크립트를 <head>에 배치한다.
  • 이렇게 하면 <body> 하단의 인라인 스크립트에서 window.docs를 즉시 사용할 수 있다.

4. 소비하는 앱에서 마이크로앱 인젝션

4.1 스크립트 태그로 로드

html
<!-- apps/web/public/index.html -->
<head>
  <script src="http://localhost:7001/static/js/main.js"></script>
</head>
<body>
  <div id="root"></div>
  <div id="micro-app-container"></div>
  <script>
    if (window.docs && window.docs.render) {
      window.docs.render(document.getElementById('micro-app-container'));
    }
  </script>
</body>

4.2 React 컴포넌트에서 동적 로드

tsx
export function MicroAppLoader({ scriptUrl, namespace }: Props) {
  const containerRef = useRef<HTMLDivElement>(null);
  useEffect(() => {
    const script = document.createElement('script');
    script.src = scriptUrl;
    script.onload = () => {
      (window as any)[namespace]?.render(containerRef.current);
    };
    document.head.appendChild(script);
    return () => { document.head.removeChild(script); };
  }, [scriptUrl, namespace]);
  return <div ref={containerRef} />;
}

5. 개발 서버와 프로덕션의 CSS 차이 대응

5.1 문제 상황

개발 환경에서는 style-loader가 CSS를 <style> 태그로 document.head에 삽입한다. 프로덕션에서는 MiniCssExtractPlugin이 CSS를 별도 파일로 추출하여 <link> 태그로 참조한다.

마이크로앱의 CSS가 어떤 방식으로 로드되든 호스트 앱의 document.head에 추가되므로, 두 환경 모두에서 CSS 오염이 발생한다.

5.2 해결 전략

Shadow DOM 내부에서 스타일을 관리하면 두 환경의 차이를 상쇄할 수 있다:

  • 개발 환경: style-loader<style> 태그를 head에 추가하는 대신, Shadow Root 내부에 추가하도록 커스텀 로더를 설정하거나, Shadow DOM 컴포넌트에서 스타일을 직접 주입한다.
  • 프로덕션 환경: 추출된 CSS 파일을 <link> 태그 대신 fetch로 가져와 Shadow Root 내부에 <style> 태그로 삽입한다.
tsx
// Shadow DOM 내부에서 외부 CSS 파일 로드
function ShadowStyleLoader({ href }: { href: string }) {
  const shadowRoot = useShadowRoot();

  useEffect(() => {
    if (!shadowRoot || !href) return;

    fetch(href)
      .then(res => res.text())
      .then(css => {
        const style = document.createElement('style');
        style.textContent = css;
        shadowRoot.appendChild(style);
      });
  }, [shadowRoot, href]);

  return null;
}

6. Docker와 Nginx로 프로덕션 환경 확인

Nginx 설정에서 Access-Control-Allow-Origin *로 CORS를 허용하고, Dockerfile에서 빌드 결과물을 Nginx 정적 파일 디렉터리에 복사한다.

bash
cd apps/docs && npm run build
docker build -t docs-app . && docker run -d -p 7001:7001 docs-app

프로덕션 환경의 브라우저 개발자 도구에서 CSS가 <link> 태그로 로드되는지, window.docs 객체가 등록되었는지, CORS 오류가 없는지 확인한다.


핵심 정리

항목내용
UMDAMD, CommonJS, 전역 변수를 모두 지원하는 범용 모듈 형식
react-app-rewiredCRA를 eject하지 않고 Webpack 설정을 오버라이드하는 도구
config-overrides.jsoutput.library로 UMD 설정, output.publicPath로 청크 경로 설정
SPA/컨테이너 분리index.tsx에서 독립 실행과 export 모드를 조건부로 분기
CSS 환경 차이개발(style-loader, <style>)과 프로덕션(MiniCssExtractPlugin, <link>)
publicPath코드 스플리팅 시 청크 파일의 기본 로드 경로를 마이크로앱 서버로 지정
CORS 설정Nginx에서 Access-Control-Allow-Origin 헤더 필수

다음 단계

UMD 빌드 설정이 완료되었으니, 이제 실제 마이크로앱을 호스트 어플리케이션에 통합하면서 발생하는 CSS 격리 심화 기법을 학습한다. Shadow DOM 내부로 스타일을 완전히 이전하는 방법과 개발/프로덕션 환경을 통합하는 전략을 다룬다.

다음: 01-CSS-격리-기법