개발자
류준열

Webpack Module Federation과 React Refresh

회사에서 webpack module federation 기반의 마이크로 프론트엔드 어플리케이션을 개발하는데, 개발환경에서 코드를 수정하면 새로고침되어 state가 초기화 되는 불편함이 있었다. 이를 개선하는 과정에서 예상보다 큰 어려움을 겪었다.

React Refresh

webpack.config.js에 분명이 HMR 세팅이 있었는데도 코드 변경시 새로고침되고 있었다.

// webpack.config.js
plugins: [
  ...
  new HtmlWebPackPlugin({
    template: "./src/index.html",
    favicon: "./public/favicon.ico",
  }),
],

구글링을 하다가 @pmmmwh/react-refresh-webpack-pluginreact-refresh 를 알게 되어 해당 패키지들을 이용하였다.

React Refresh는 개발 환경에서 코드 변경 시 페이지 전체를 새로고침하지 않고, 해당 컴포넌트만 업데이트 하여 현재 state를 유지할 수 있도록 하는 도구이다.

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등을 활용하여 강제하는 것이 좋다.