테마
Webpack UMD 빌드 설정과 런타임 인젝션
CRA 기반 레거시 프로젝트에서 eject 없이 Webpack 설정을 커스터마이징하여 UMD 라이브러리로 빌드하고, 다른 어플리케이션에서 런타임으로 마이크로앱을 인젝션하는 방법을 학습한다.
학습 목표
- UMD(Universal Module Definition) 형식의 개념과 동작 원리를 설명할 수 있다.
- react-app-rewired를 사용하여 CRA의 Webpack 설정을 eject 없이 커스터마이징할 수 있다.
- config-overrides.js에서 entry/output을 변경하여 UMD 라이브러리 빌드를 구성할 수 있다.
- 개발 서버(style-loader)와 프로덕션(extracted CSS)의 차이를 인지하고 대응할 수 있다.
- 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-dev2.2 package.json 스크립트 변경
react-scripts를 react-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 오류가 없는지 확인한다.
핵심 정리
| 항목 | 내용 |
|---|---|
| UMD | AMD, CommonJS, 전역 변수를 모두 지원하는 범용 모듈 형식 |
| react-app-rewired | CRA를 eject하지 않고 Webpack 설정을 오버라이드하는 도구 |
| config-overrides.js | output.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 내부로 스타일을 완전히 이전하는 방법과 개발/프로덕션 환경을 통합하는 전략을 다룬다.