개발자
류준열
module federation에서 코드 수정 후 저장하면 새로고침 되는 문제
회사에서 webpack module federation 기반의 마이크로 프론트엔드 어플리케이션(pnpm create mf-app)을 개발하는데, 개발환경에서 코드를 수정하면 새로고침되어 state가 초기화 되는 불편함이 있었다. 이를 개선하는 과정에서 예상보다 큰 어려움을 겪었다.
webpack.config.js에 HMR 세팅이 있었는데도 코드 변경시 새로고침되고 있었다.
// webpack.config.js plugins: [ ... new HtmlWebPackPlugin({ template: "./src/index.html", favicon: "./public/favicon.ico", }), ],
원인
이건 버그가 아니라, module federation과 HMR의 설계 차이에서 발생한 문제다.
HMR은 단일 어플리케이션 내에서 모든 의존성 그래프를 파악하고 있는 상태에서 동작한다. 그렇기 때문에 변경된 부분만 수정 할 수 있는 것이다.
하지만 module federation은 독립된 webpack을 가진 마이크로 프론트앱들이 커스텀 이벤트로 통신하기 때문에, HMR의 전제(모든 의존성 그래프 파악)과 맞지 않는다. 그래서 개발환경에서는 코드 수정시 안전장치로 새로고침을 하는 것이다.
우리 프로젝트는 host app만 단일로 사용하고 있었기 때문에 mf-app의 의미가 없었다. (이렇게 된 이유는 내부사정..)
그래서 가장 간단한 수정 방법으로 react-refresh를 추가하였다.
React Refresh
구글링을 하다가 @pmmmwh/react-refresh-webpack-plugin와 react-refresh 를 알게 되어 해당 패키지들을 이용하였다.
React Refresh는 개발 환경에서 React 내부의 코드를 변경 시 페이지 전체를 새로고침하지 않고, 해당 컴포넌트만 업데이트 하여 현재 state를 유지할 수 있도록 하는 도구이다.
단 React 내에서만 작동하고 React 외부의 코드를 수정하면 새로고침된다.
Webpack과 Babel에서의 설정
처음에는 개발환경, 운영환경 신경쓰지 않고 react-refresh가 적용되도록 webpack.config.js와 .babelrc를 설정했다. (webpack, babel 둘 중 하나라도 설정 빼먹으면 에러 남)
// webpack.config.js
const RefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin"); // 추가
module.exports = (_, argv) => {
return {
// ... output, resolve 등 생략
module: {
rules: [
// 다른 로더 설정 생략
{
test: /\.(ts|tsx|js|jsx)$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
plugins: [require.resolve("react-refresh/babel")], // 추가
},
},
},
],
},
plugins: [
new RefreshWebpackPlugin(), // 추가
// Module Federation, HtmlWebPackPlugin, Dotenv 등 기타 플러그인들
],
};
};
// .babelrc { "presets": [ "@babel/preset-typescript", [ "@babel/preset-react", { "runtime": "automatic" } ], "@babel/preset-env" ], "plugins": [ ["@babel/transform-runtime"], "react-refresh/babel" // 추가 ], }
이렇게 추가하니 개발 중 코드변경시에 더이상 새로고침이 되지 않았다.
development 빌드와 production 빌드 차이

development 빌드(webpack --mode develoment)에서는 React Refresh가 정상 작동하고, webpack dev server를 통해 HMR 환경을 제공하지만, production 빌드(webpack --mode production)에서는 불필요한 개발용 코드가 포함되어 $RefreshReg$ is not defined 오류가 발생했다. (아래 package.json 참고)
"scripts": { "build": "webpack --mode production", "build:dev": "webpack --mode development", "build:start": "cd dist && PORT=3000 npx serve -s", ... },
이를 해결하기 위해 Babel과 Webpack 모두에서 개발 모드에 한해서만 React Refresh 관련 코드가 포함되도록 조건을 달았다.
// webpack.config.js
const RefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin"); // 추가
module.exports = (_, argv) => {
const isDev = argv.mode === "development"; // 개발환경인지 아닌지
return {
// ... output, resolve 등 생략
module: {
rules: [
// 다른 로더 설정 생략
{
test: /\.(ts|tsx|js|jsx)$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
plugins: isDev ? [require.resolve("react-refresh/babel")] : [], // isDev 조건 추가
},
},
},
],
},
plugins: [
isDev && new RefreshWebpackPlugin(), // isDev 조건 추가
// Module Federation, HtmlWebPackPlugin, Dotenv 등 기타 플러그인들
].filter(Boolean),
// devServer 설정 등 생략
};
};
// .babelrc { "presets": [ "@babel/preset-typescript", [ "@babel/preset-react", { "runtime": "automatic" } ], "@babel/preset-env" ], "plugins": [["@babel/transform-runtime"]], "env": { "development": { "plugins": ["react-refresh/babel"] // 개발환경에서만 react-refresh 이용 } } }
환경변수 확인과 cross-env 사용
"scripts": { "build": "webpack --mode production", }
개발 모드(isDev)에만 React Refresh 가 포함되도록 했고, 빌드명령어가 production으로 지정되어 있는데도 여전히 해결되지 않았다. babel이 의심되어 .babelrc를 babel.config.js로 바꿔서 console.log를 찍어보니 환경변수가 development로 잡히고 있었다.
cross-env로 환경변수를 강제하여, Babel과 Webpack 모두 production 환경으로 인식하도록 하였다.
"scripts": { "build": "cross-env NODE_ENV=production webpack --mode production" }
이렇게 하니 $RefreshReg$ is not defined 에러가 사라졌다.
결론
pnpm create mf-app으로 세팅된 코드는 개발환경에서 코드 변경시 새로고침된다.- React Refresh 설정을 수동으로 넣어주어야 한다.
- 개발 환경과 production 환경의 명확한 분리
- Babel과 Webpack 설정에서 개발 전용 플러그인이 production 번들에 포함되지 않도록 주의해야 한다.
- Webpack의 --mode와 Node 환경변수(
NODE_ENV)는 서로 다르게 설정 될 수 있기 때문에cross-env등을 활용하여 강제하는 것이 좋다.