webpack-dev-server와 HMR

webpack-dev-server

webpack-dev-server는 webpack으로 번들된 파일을 인-메모리에서 서브합니다.

  1. 서버 실행시 소스 파일들을 번들링하여 메모리에 올립니다.
  2. 소스 파일을 watch하고 있다가 변경이 감지되면 변경된 모듈만 새로 번들링합니다.
  3. 변경된 모듈 정보를 브라우저에 전송합니다.
  4. 변경을 인지하고 새로고침되어 변경사항이 반영된 페이지가 로드됩니다.

webpack-dev-server는 webpack과 분리된 NPM 패키지로, 아래의 명령으로 설치합니다.

$ npm i --save-dev webpack-dev-server

설정 파일(시작하기 참고)이 있는 상태에서 실행합니다.

$ webpack-dev-server

이후 소스 파일을 변경 저장하면 페이지가 새로고침되어 변경된 내용이 반영된걸 확인할 수 있습니다.

HMR(Hot Module Replacement)

HMR은 내용이 변경된 모듈을 페이지 새로고침 없이 런타임에서 업데이트합니다. 업데이트에 실패할 경우 새로고침을 수행합니다.

module 업데이트

변경된 모듈을 hot module이라고 합니다.

모듈이 업데이트 되면 업데이트는 해당 모듈 부터 accept 될때까지 부모 모듈(해당 모듈을 import or require하는)로 bubbling-up 됩니다. 의존성 트리의 최상단(entry)까지 bubbling-up 되서도 accept 되지 않으면 업데이트는 실패하게 됩니다.

  1. 모듈 E가 업데이트 됐을 때, 업데이트를 자체적으로 accept(self accept)할 수 있는지 체크합니다.
  2. 만약 아니라면 모듈 E를 의존하는 모듈(B)에서 업데이트를 accept할 수 있는지 체크 합니다.
  3. 만약 아니라면 모듈 B를 의존하는 모듈(A)에서 업데이트를 accept할 수 있는지 체크 합니다. 모듈 A는 accept합니다.
  4. 모듈이 업데이트 됩니다.
  5. 모듈 A의 하위 모든 모듈은 다시 실행 됩니다.
module.hot.accept 함수로 업데이트를 accept
if (module.hot) {
  module.hot.accept();
}
module.hot.dispose함수로 부작용 처리

업데이트 accept 시 모듈을 다시 실행하기 때문에 부작용이 발생 할 수 있습니다.

예를들어 entry가 다음과 같은 모듈을 import할 때

// entry.js
import createDiv from './createDiv';

var diceNumber = -~(Math.random() * 6);
var diceDiv = createDiv(diceNumber);
document.body.appendChild(div);
if (module.hot) {
  module.hot.accept();
}
// createDiv.js
export default function(content) {
  var div = document.createElement('div');
  div.textContent = content;
  return div;
};

createDiv 모듈이 업데이트 된다면 entry가 accept 하면서 document.body.appendChild(div)가 다시 실행될 것 입니다. 그러면 업데이트 된 이후 그린 div 뿐만아니라 업데이트 되기전에 그렸던 div도 남아있게 됩니다.

이러한 부작용을 피하기 위해 dispose함수를 사용합니다. dispose에 등록한 핸들러는 현재의 모듈 코드가 교체되면 실행됩니다. dispose 핸들러는 생성, 추가 했던 요소를 제거하는 목적으로 사용합니다.

보전하려는 현재 상태가 있다면 핸들러로 넘어오는 파라미터인 data 오브젝트에 추가합니다. 교체된 모듈에서는 module.hot.data로 사용할 수 있습니다.

// entry.js
import createDiv from './createDiv';

var diceNumber = (module.hot && module.hot.data && module.hot.data.diceNumber)
  || -~(Math.random() * 6); // 업데이트전 diceNumber를 사용.
  // 너무 ugly한데 이런 방법으로 사용하는 게 맞는지..
var diceDiv = createDiv(diceNumber);
document.body.appendChild(div);
if (module.hot) {
  module.hot.accept();
  module.hot.dispose(function(data) {
    div.parentNode.removeChild(div);
    // diceNumber 보전
    data.diceNumber = diceNumber;
  });
}

모듈 케이스별 부작용 처리

  • 부작용 없는 모듈

    모듈내에서 처리할 것이 없음. 어떤 부모 모듈도 accept 할 수 있음.

  • 부작용 있는 모듈

    모듈엔 dispose핸들러가 있어야함. 그러면 어떤 부모 모듈도 accept 할 수 있음.

  • export하지 않고 부작용 있는 모듈

    모듈엔 dispose핸들러가 있어야하고 모듈 자체적으로 accept 할수 있음. 부모 모듈에서 처리할 것이 없음.

    만약 내 코드가 아니라면 부모 모듈에서 커스텀 dispose 로직과 함께 모듈을 accept 할 수 있음.

  • entry 모듈

    entry 모듈은 export하지 않기때문에 자체적으로 accept 할 수 있음. 모듈 교체시 dispose핸들러로 교체전 상태를 전달 할 수 있음.

  • 처리불가한 부작용을 가진 외부 모듈

    가장 가까운 부모 모듈에서 module.hot.decline로 해당 모듈을 decline 함. 이렇게하면 업데이트가 accept 되지않아 페이지가 새로고침 되지만 외부 모듈의 코드 변경은 매우 드뭄.

HMR 사용하기

$ webpack-dev-server --hot --inline

옵션 --hot은 entry에 'webpack/hot/dev-server'를추가하고 plugin에 HotModuleReplacementPlugin를 추가할 것 입니다.

옵션들을 아래와 같이 설정 파일에 반영할 수 있습니다. (entry에 'webpack/hot/dev-server'가 들어가면 안됩니다. devServer.hot이 'webpack/hot/dev-server'를 자동으로 추가하기 때문에 entry 아이템 중복 에러가 발생합니다.)

module.exports = {
  entry: [
    path.join(__dirname, 'src', 'entry.js')
  ],
  ... 
  중략
  ...
  plugins: [
    new webpack.HotModuleReplacementPlugin()
  ],
  devServer: {
    hot: true,
    inline: true
  }
};
$ webpack-dev-server

참고

webpack-dev-server - https://webpack.github.io/docs/webpack-dev-server.html

hot module replacement - https://webpack.github.io/docs/hot-module-replacement.html, https://webpack.github.io/docs/hot-module-replacement-with-webpack.html

results matching ""

    No results matching ""