[go: up one dir, main page]

Hot Module Replacement

Hot Module Replacement(또는 HMR)는 webpack에서 제공하는 가장 유용한 기능 중 하나입니다. 모든 종류의 모듈을 새로고침 할 필요 없이 런타임에 업데이트 할 수 있습니다. 이 페이지는 구현에 초점을 맞추고 개념 페이지는 작동 원리와 왜 유용한지에 대한 자세한 내용을 제공합니다.

Enabling HMR

이 기능은 생산성을 크게 높여줍니다. webpack-dev-server 설정을 업데이트하고 webpack의 내장 HMR plugin을 사용하면 됩니다. 이제 index.js 모듈이 print.js를 사용하므로 해당 엔트리 포인트도 제거합니다.

webpack-dev-server v4.0.0부터 Hot Module Replacement가 기본적으로 활성화되어 있습니다.

webpack.config.js

  import path from 'node:path';
  import { fileURLToPath } from 'node:url';
  import HtmlWebpackPlugin from 'html-webpack-plugin';

  const __filename = fileURLToPath(import.meta.url);
  const __dirname = path.dirname(__filename);

  export default {
    entry: {
       app: './src/index.js',
-      print: './src/print.js',
    },
    devtool: 'inline-source-map',
    devServer: {
      static: './dist',
+     hot: true,
    },
    plugins: [
      new HtmlWebpackPlugin({
        title: 'Hot Module Replacement',
      }),
    ],
    output: {
      filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist'),
      clean: true,
    },
  };

HMR에 대한 수동 엔트리포인트를 제공할 수도 있습니다.

webpack.config.js

  import path from 'node:path';
  import { fileURLToPath } from 'node:url';
  import HtmlWebpackPlugin from 'html-webpack-plugin';
+ import webpack from 'webpack';

  const __filename = fileURLToPath(import.meta.url);
  const __dirname = path.dirname(__filename);

  export default {
    entry: {
       app: './src/index.js',
-      print: './src/print.js',
+      // hot module replacement를 위한 런타임 코드
+      hot: 'webpack/hot/dev-server.js',
+      // 웹 소켓 전송, hot 및 live 리로드 로직을 위한 개발 서버 클라이언트
+      client: 'webpack-dev-server/client/index.js?hot=true&live-reload=true',
    },
    devtool: 'inline-source-map',
    devServer: {
      static: './dist',
+     // 웹 소켓 전송, hot 및 live 리로드 로직을 위한 개발 서버 클라이언트
+     hot: false,
+     client: false,
    },
    plugins: [
      new HtmlWebpackPlugin({
        title: 'Hot Module Replacement',
      }),
+     // hot module replacement를 위한 플러그인
+     new webpack.HotModuleReplacementPlugin(),
    ],
    output: {
      filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist'),
      clean: true,
    },
  };

이제 index.js 파일을 업데이트하여 print.js 내부의 변경이 감지되면 webpack에서 업데이트된 모듈을 수락하도록 지시합니다.

index.js

  import _ from 'lodash';
  import printMe from './print.js';

  function component() {
    const element = document.createElement('div');
    const btn = document.createElement('button');

    element.innerHTML = _.join(['Hello', 'webpack'], ' ');

    btn.innerHTML = 'Click me and check the console!';
    btn.

    element.appendChild(btn);

    return element;
  }

  document.body.appendChild(component());
+
+ if (module.hot) {
+   module.hot.accept('./print.js', function() {
+     console.log('Accepting the updated printMe module!');
+     printMe();
+   })
+ }

print.js에서 console.log 문을 변경하면 브라우저 콘솔에 다음과 같은 출력이 표시됩니다. (당분간 button.> 출력에 대해 걱정하지 마세요. 나중에 해당 부분을 변경할 것입니다.)

print.js

  export default function printMe() {
-   console.log('I get called from print.js!');
+   console.log('Updating print.js...');
  }

console

[HMR] Waiting for update signal from WDS...
main.js:4395 [WDS] Hot Module Replacement enabled.
+ 2main.js:4395 [WDS] App updated. Recompiling...
+ main.js:4395 [WDS] App hot update...
+ main.js:4330 [HMR] Checking for updates on the server...
+ main.js:10024 Accepting the updated printMe module!
+ 0.4b8ee77….hot-update.js:10 Updating print.js...
+ main.js:4330 [HMR] Updated modules:
+ main.js:4330 [HMR]  - 20

Via the Node.js API

Node.js API와 함께 Webpack Dev Server를 사용하는 경우 webpack 설정 객체에 dev 서버 옵션을 추가하지 마십시오. 대신 생성 시 두 번째 매개 변수로 전달하십시오. 예를 들어 보겠습니다.

new WebpackDevServer(options, compiler)

HMR을 활성화하려면 HMR 엔트리 포인트를 포함하도록 webpack 설정 객체도 수정해야 합니다. 다음은 그 모습에 대한 간단한 예시입니다.

dev-server.js

import path from "node:path";
import { fileURLToPath } from "node:url";
import HtmlWebpackPlugin from "html-webpack-plugin";
import webpack from "webpack";
import WebpackDevServer from "webpack-dev-server";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const config = {
  mode: "development",
  entry: [
    // hot module replacement를 위한 런타임 코드
    "webpack/hot/dev-server.js",
    // 웹 소켓 전송, hot 및 live 리로드 로직을 위한 개발 서버 클라이언트
    "webpack-dev-server/client/index.js?hot=true&live-reload=true",
    // 엔트리
    "./src/index.js",
  ],
  devtool: "inline-source-map",
  plugins: [
    // hot module replacement를 위한 플러그인
    new webpack.HotModuleReplacementPlugin(),
    new HtmlWebpackPlugin({
      title: "Hot Module Replacement",
    }),
  ],
  output: {
    filename: "[name].bundle.js",
    path: path.resolve(__dirname, "dist"),
    clean: true,
  },
};
const compiler = webpack(config);

// `hot` 및 `client` 옵션을 수동으로 추가했기 때문에 비활성화됩니다.
const server = new WebpackDevServer({ hot: false, client: false }, compiler);

try {
  await server.start();
  console.log("dev server is running");
} catch (err) {
  throw new Error(`Failed to start dev server: ${err.message}`, { cause: err });
}

webpack-dev-server Node.js API 전체 문서를 참고하세요.

Gotchas

Hot Module Replacement는 까다로울 수 있습니다. 이를 보여주기 위해 작업 예제로 돌아갑시다. 계속해서 예제 페이지의 버튼을 클릭하면 콘솔이 이전 printMe 함수를 인쇄하고 있음을 알 수 있습니다.

이것은 버튼의 onclick 이벤트 핸들러가 여전히 원래의 printMe 함수에 바인딩 되어 있기 때문에 발생합니다.

HMR에서 이 작업을 수행하려면 module.hot.accept를 사용하여 새 printMe 함수에 대한 바인딩을 업데이트해야 합니다.

index.js

  import _ from 'lodash';
  import printMe from './print.js';

  function component() {
    const element = document.createElement('div');
    const btn = document.createElement('button');

    element.innerHTML = _.join(['Hello', 'webpack'], ' ');

    btn.innerHTML = 'Click me and check the console!';
    btn.  // onclick 이벤트는 원래 printMe 함수에 바인딩 됩니다.

    element.appendChild(btn);

    return element;
  }

- document.body.appendChild(component());
+ let element = component(); // print.js 변경 시 다시 렌더링할 요소 저장
+ document.body.appendChild(element);

  if (module.hot) {
    module.hot.accept('./print.js', function() {
      console.log('Accepting the updated printMe module!');
-     printMe();
+     document.body.removeChild(element);
+     element = component(); // 클릭 핸들러를 업데이트하려면 "component"를 다시 렌더링하십시오.
+     document.body.appendChild(element);
    })
  }

이것은 하나의 예시일 뿐이지만 사람들이 실수할 수 있는 상황이 많이 있습니다. 운 좋게도 Hot Module Replacement를 훨씬 쉽게 만들어주는 많은 로더가 있습니다. 그중 일부는 아래에 언급되었습니다.

What an update actually replaces

대부분의 HMR 혼란은 하나의 잘못된 가정에서 비롯됩니다. 업데이트가 accept 호출을 작성한 모듈을 다시 실행한다는 것입니다. 그렇지 않습니다.

모듈이 변경되면 webpack은 해당 모듈을 가져온 모듈들을 따라 위로 올라가 변경을 수락한 모듈을 찾을 때까지 이동합니다. 변경된 모듈만 다시 실행됩니다. 이를 수락한 모듈은 계속 실행됩니다 — 동일한 인스턴스, 동일한 변수, 동일한 클로저 — 그리고 반응할 수 있도록 콜백이 호출됩니다. 따라서 accept('./dep.js', cb)는 "내가 ./dep.js의 변경을 처리하겠습니다"를 의미하며, "변경될 때 나를 다시 실행하세요"가 아닙니다.

이로 인해 두 가지 결과가 발생하며, 이것이 실제로 HMR의 전부입니다.

  • 수락 모듈의 dispose 핸들러는 실행되지 않습니다. 그 핸들러는 해당 모듈 자체가 교체될 때만 실행되며, 이는 변경되거나 자기 자신을 수락할 때 발생합니다.
  • 콜백이 실행될 때 이미 가져온 바인딩이 업데이트되어 있습니다. webpack이 import를 재연결하므로 콜백 내에서 dep()를 호출하면 새 버전이 호출됩니다 — 다시 가져올 필요가 없습니다.
import { greet } from "./service.js";

// this runs once; it is NOT re-run when service.js changes
console.log(greet());

if (import.meta.webpackHot) {
  import.meta.webpackHot.accept("./service.js", () => {
    // `greet` already points at the new version here
    console.log(greet());
  });
}

Self-accepting a module

외부에 보이는 바인딩이 없는 모듈 — 실행될 때 사이드 이펙트만 발생시키는 모듈 — 은 자체 변경을 수락할 수 있습니다. webpack은 해당 모듈을 제자리에서 다시 실행하며, 업데이트는 엔트리 포인트까지 버블링되지 않고 거기서 멈춥니다.

import.meta.webpackHot.accept();

이것이 스타일시트가 하는 방식이며, 등록된 라우트 테이블, 차트 정의 세트, <canvas> 렌더 루프 같은 것에 적합한 선택입니다. 다른 모듈이 이 모듈이 내보낸 것을 참조하고 있는 경우에는 잘못된 선택입니다. 해당 모듈들은 import 시점에 이전 값을 캡처했으며, 모듈을 다시 실행해도 그 복사본이 업데이트되지 않습니다. 대신 참조를 소유한 모듈에서 수락하세요.

Cleaning up side effects

모듈을 다시 실행하면 사이드 이펙트가 두 번째로 실행됩니다. 인터벌이 두 번 스케줄되고, 리스너가 두 번 등록되고, 소켓이 다시 열립니다 — 열 번의 수정 후에는 페이지에 모든 것이 열 개씩 있게 되어 webpack과 무관한 이유로 "느리거나" "화면이 깜빡이는" 것처럼 보입니다.

dispose는 이를 해제하는 밸브입니다. 새 버전이 실행되기 전에 실행되므로, 현재 버전이 점유한 것을 되돌리는 곳입니다.

const socket = new WebSocket(url);
const timer = setInterval(poll, 1000);
const onResize = () => layout();

window.addEventListener("resize", onResize);

if (import.meta.webpackHot) {
  import.meta.webpackHot.accept();
  import.meta.webpackHot.dispose(() => {
    socket.close();
    clearInterval(timer);
    window.removeEventListener("resize", onResize);
  });
}

경험 법칙: 모듈 자체 평가보다 더 오래 지속되는 모든 것 — 타이머, 리스너, 소켓, 옵저버, document에 추가된 DOM 노드, 전역 레지스트리에 추가된 항목 — 은 dispose에 대응하는 줄이 필요합니다.

Preserving state across an update

dispose에 전달된 data 객체는 다음 버전의 모듈에 import.meta.webpackHot.data로 전달되며, 처음 실행 시에는 undefined입니다. 이 왕복 과정이 편집 시 유지해야 할 상태 — 스크롤 위치, 폼 초안, 게임 점수, 애니메이션의 프레임 카운터 — 를 보존하는 방법입니다.

const previous = import.meta.webpackHot && import.meta.webpackHot.data;
let ticks = previous ? previous.ticks : 0;

const timer = setInterval(() => {
  ticks += 1;
  render(ticks);
}, 1000);

if (import.meta.webpackHot) {
  import.meta.webpackHot.accept();
  import.meta.webpackHot.dispose((data) => {
    clearInterval(timer);
    data.ticks = ticks;
  });
}

이 모듈을 편집하면 카운터가 0에서 다시 시작하는 대신 멈춘 곳에서 계속됩니다 — dispose가 이전 인터벌을 멈추고 값을 저장한 다음, 새 버전이 data에서 값을 다시 읽어옵니다.

Hot-swapping a singleton

스토어, 라우터, DI 컨테이너: 전체 애플리케이션이 참조를 보유하는 한 번만 생성되는 무언가입니다. 객체 자체를 교체하면 모든 보유자가 이전 객체에 묶이게 되므로, 대신 _내용물_을 교체하고 동일성을 유지합니다. Redux 스토어가 친숙한 예시이며 — replaceReducer는 정확히 이를 위해 존재합니다.

import { createStore } from "redux";
import rootReducer from "./reducers/index.js";

const store = createStore(rootReducer);

if (import.meta.webpackHot) {
  import.meta.webpackHot.accept("./reducers/index.js", () => {
    store.replaceReducer(rootReducer);
  });
}

export default store;

스토어는 상태와 구독자를 유지하고, 리듀서 함수만 변경됩니다. "구현을 교체하는" 메서드가 있는 모든 싱글톤 — 라우터의 라우트 테이블, 레지스트리의 항목, 이벤트 버스의 핸들러 — 은 동일한 패턴을 따릅니다.

When an update can't be applied

체인의 어떤 모듈도 변경을 수락하지 않으면 업데이트가 중단됩니다. webpack-dev-server는 페이지를 리로드하는 방식으로 응답하므로, 개발 중 전체 리로드가 발생한다면 보통 "이 파일을 수락한 모듈이 없다"는 의미이며, 설정이 잘못된 것이 아닙니다.

의도적으로 이를 제어하는 두 가지 방법이 있습니다.

  • decline은 모듈을 핫 업데이트 불가능으로 표시하여, 실패한 시도 후가 아니라 즉시 리로드를 강제합니다. 사이드 이펙트를 진정으로 되돌릴 수 없는 모듈에 유용합니다.
  • invalidate는 조건부 케이스에 사용됩니다. 디펜던시를 수락했지만 이 특정 변경은 콜백이 처리할 수 없으므로 부모에게 업데이트를 넘깁니다.
import.meta.webpackHot.accept("./config.js", () => {
  if (config.port !== runningPort) {
    // a port change needs a restart; let it bubble
    import.meta.webpackHot.invalidate();
    return;
  }
  applyConfig(config);
});

HMR on the server

HMR은 브라우저 전용이 아닙니다 — target: 'node'에서도 동작하며, 리스닝 소켓이나 인메모리 상태를 끊지 않고 요청 핸들러를 교체하는 장기 실행 서버에서 사용할 수 있습니다. 여기에는 dev-server 클라이언트가 없으므로 번들이 직접 업데이트를 요청하며, 업데이트 파일을 작성하는 와처 컴파일러에 의해 구동됩니다.

import express from "express";
import handler from "./handler.js";

const app = express();
let current = handler;

// the indirection is what makes the swap possible:
// express keeps this closure, and the closure reads `current`
app.use((req, res, next) => current(req, res, next));

const server = app.listen(3000);

if (import.meta.webpackHot) {
  import.meta.webpackHot.accept("./handler.js", () => {
    current = handler;
  });
  import.meta.webpackHot.dispose(() => server.close());

  setInterval(() => {
    if (import.meta.webpackHot.status() === "idle") {
      import.meta.webpackHot.check(true).catch((err) => {
        console.error("HMR update failed, restart the server:", err.message);
      });
    }
  }, 1000);
}

check(true)는 업데이트를 다운로드하고 적용하며, 교체된 모듈로 리졸브됩니다 — 새로운 것이 없으면 null로 리졸브됩니다. 업데이트를 적용할 수 없는 경우 거부되며, 파일을 지정하는 메시지가 표시됩니다. 예: Aborted because ./handler.js is not accepted. 서버에서 이는 프로세스를 재시작해야 한다는 신호이며, 리로드할 페이지가 없기 때문입니다.

HMR with Stylesheets

CSS Hot Module Replacement는 실제로 style-loader의 도움으로 상당히 간단합니다. 이 로더는 CSS 의존성이 업데이트될 때 <style>태그를 패치하기 위해 백그라운드에서 module.hot.accept를 사용합니다.

먼저 다음 명령으로 두 로더를 모두 설치해 보겠습니다.

npm install --save-dev style-loader css-loader

이제 로더를 사용하도록 설정 파일을 업데이트 하겠습니다.

webpack.config.js

  import path from 'node:path';
  import { fileURLToPath } from 'node:url';
  import HtmlWebpackPlugin from 'html-webpack-plugin';

  const __filename = fileURLToPath(import.meta.url);
  const __dirname = path.dirname(__filename);

  export default {
    entry: {
      app: './src/index.js',
    },
    devtool: 'inline-source-map',
    devServer: {
      static: './dist',
      hot: true,
    },
+   module: {
+     rules: [
+       {
+         test: /\.css$/,
+         use: ['style-loader', 'css-loader'],
+       },
+     ],
+   },
    plugins: [
      new HtmlWebpackPlugin({
        title: 'Hot Module Replacement',
      }),
    ],
    output: {
      filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist'),
      clean: true,
    },
  };

스타일 시트 핫 로딩은 모듈로 가져오는 것만큼 쉽습니다.

project

  webpack-demo
  ├── package.json
  ├── webpack.config.js
  ├── /dist
  │   └── bundle.js
  └── /src
      ├── index.js
      ├── print.js
+     └── styles.css

styles.css

body {
  background: blue;
}

index.js

  import _ from 'lodash';
  import printMe from './print.js';
+ import './styles.css';

  function component() {
    const element = document.createElement('div');
    const btn = document.createElement('button');

    element.innerHTML = _.join(['Hello', 'webpack'], ' ');

    btn.innerHTML = 'Click me and check the console!';
    btn.  // onclick 이벤트는 원래 printMe 함수에 바인딩 됩니다.

    element.appendChild(btn);

    return element;
  }

  let element = component();
  document.body.appendChild(element);

  if (module.hot) {
    module.hot.accept('./print.js', function() {
      console.log('Accepting the updated printMe module!');
      document.body.removeChild(element);
      element = component(); // 클릭 핸들러를 업데이트하려면 "component"를 다시 렌더링하십시오.
      document.body.appendChild(element);
    })
  }

body의 스타일을 background : red;로 변경하면 새로고침 없이도 페이지의 배경색이 변경되는 것을 즉시 확인할 수 있습니다.

styles.css

  body {
-   background: blue;
+   background: red;
  }

Other Code and Frameworks

HMR이 다양한 프레임워크 및 라이브러리와 원활하게 상호 작용할 수 있도록 커뮤니티에는 다른 많은 로더와 예제가 있습니다.

  • React Fast Refresh: 실시간으로 React 컴포넌트를 조정하고 상태를 보존합니다. 더 이상 유지 관리되지 않는 React Hot Loader를 대체합니다.
  • Vue Loader: Vue 컴포넌트에 대한 HMR을 즉시 지원하는 로더입니다.
  • Elm Hot webpack Loader: Elm 프로그래밍 언어에 대한 HMR을 지원합니다.
  • Angular HMR: 로더가 필요 없습니다! HMR 지원은 Angular CLI에 내장되어 있으며, ng serve 명령에 --hmr 플래그를 추가하면 됩니다.
  • Svelte Loader: Svelte 컴포넌트에 대한 HMR을 즉시 지원하는 로더입니다.
Edit this page·
« Previous
Installation

20 Contributors

jmreidyjhnnssararubinrohannairjoshsantosdrpicoxskipjacksbaidongdi2290bdwaincarylixgirmaEugeneHlushkoAnayaDesignaviyacohendhruvduttwizardofhogwartsaholznersnitin315Brennvo

Translators