1. HMR
Hot Module Replacement(HMR) 기능은 초기에 webpack에서 제공한 기능으로, 런타임 시 JavaScript 모듈을 핫 업데이트할 수 있습니다(전체 새로 고침 없이 모듈을 교체, 추가, 삭제 가능):
Hot Module Replacement (HMR) exchanges, adds, or removes modules while an application is running, without a full reload.
(Hot Module Replacement Concepts에서 발췌)
전체 새로 고침과 비교할 때 모듈 단위의 핫 업데이트가 갖는 가장 큰 의미는 애플리케이션의 현재 런타임 상태를 유지할 수 있다는 것입니다. 이는 더욱 효율적인 Hot Reloading 개발 방식을 가능하게 했습니다.
P.S. 나중에는 다른 빌드 도구들도 유사한 메커니즘을 구현했습니다. 예를 들어 Browserify나 React Native Packager 등이 있습니다.
그렇다면, 컴파일 시점에 소스 코드 편집으로 인해 발생하는 파일 변경과 런타임 시점에 이루어지는 모듈 교체는 어떻게 서로 연결되는 것일까요?
2. 기본 원리

파일 변경이 감지되면 빌드 도구(HMR plugin)에 알림을 보내고, 변경된 파일(모듈)을 애플리케이션 내부에서 실행 중인 런타임 프레임워크(HMR Runtime)로 전송합니다. 런타임 프레임워크는 이 모듈들을 모듈 시스템에 밀어 넣습니다(기존 모듈을 추가/삭제하거나 교체함).
여기서 HMR Runtime은 빌드 도구가 컴파일 시점에 주입하는 것으로, 고유한 모듈 ID를 통해 컴파일 시점의 파일과 런타임 시점의 모듈을 매핑하고, 애플리케이션 계층의 프레임워크(예: React, Vue 등)와 연동할 수 있는 일련의 API를 노출합니다.
3. HMR API
가장 자주 사용되는 것은 accept입니다:
module.hot.accept(dependencies, callback): 지정된 의존성 모듈의 업데이트를 감지합니다.
예를 들어:
import printMe from './print.js';
if (module.hot) {
module.hot.accept('./print.js', function() {
console.log('Accepting the updated printMe module!');
printMe();
})
}
accept(콜백)가 트리거되면 새 모듈이 이미 모듈 시스템에 주입되었음을 의미하며, 이 시점 이후로 접근하는 것은 모두 새 모듈 인스턴스가 됩니다.
P.S. 전체 예제는 Hot Module Replacement Guides를 참조하세요.
하지만 실제 환경에서는 모듈 간에 다단계 의존성이 존재하는 경우가 많아, 한 모듈을 교체하면 그 모듈에 의존하는 모든 모듈(직접적 또는 간접적)에 영향을 미칩니다:

그렇다면 모든 모듈에 이와 유사한 업데이트 처리 로직을 추가해야 할까요?
일반적으로는 그럴 필요가 없습니다. 모듈 업데이트 이벤트에는 버블링 메커니즘이 있어서, accept로 처리되지 않은 업데이트 이벤트는 의존성 체인을 따라 역방향으로 전달되므로, 중요한 노드(예: Router 컴포넌트)에서 집중적으로 처리하기만 하면 됩니다.
accept 외에도 다음과 같은 API가 제공됩니다:
-
module.hot.decline(dependencies): 의존성을 업데이트할 수 없는 것으로 표시합니다(전체 새로 고침을 원할 때 사용). -
module.hot.dispose/addDisposeHandler(data => {}): 현재 모듈이 교체될 때 트리거되며, 리소스를 정리하거나 (data매개변수를 통해) 새 모듈로 상태를 전달할 때 사용합니다. -
module.hot.invalidate(): 현재 모듈을 무효화하여 강제로 업데이트하도록 합니다. -
module.hot.removeDisposeHandler(callback): 모듈 교체 이벤트에 대한 리스너를 제거합니다.
P.S. webpack HMR API에 대한 자세한 정보는 Hot Module Replacement API를 참조하세요.
4. HMR Runtime
애플리케이션 관점에서 모듈 교체 과정은 다음과 같습니다:
-
애플리케이션이 HMR Runtime에 업데이트 확인을 요청합니다.
-
HMR Runtime이 비동기적으로 업데이트를 다운로드하고 애플리케이션에 알립니다.
-
애플리케이션이 HMR Runtime에 이 업데이트를 적용하도록 요청합니다.
-
HMR Runtime이 업데이트를 동기적으로 적용합니다.
(빌드 도구로부터) 모듈 업데이트 알림을 받으면, HMR Runtime은 Webpack Dev Server에 업데이트 목록(manifest)을 쿼리한 다음, 각 업데이트 모듈을 다운로드합니다. 모든 새 모듈의 다운로드가 완료되고 준비가 끝나면 적용 단계에 들어갑니다.
업데이트 목록에 있는 모든 모듈을 무효(invalid)로 표시하고, 무효로 표시된 각 모듈에 대해 현재 모듈에서 accept 이벤트 처리가 발견되지 않으면 부모 모듈로 버블링하여 부모 모듈도 무효로 표시합니다. 이 과정은 애플리케이션 진입 모듈까지 계속됩니다.
이후 모든 무효 모듈은 해제(dispose)되어 모듈 시스템에서 언마운트되고, 마지막으로 모듈의 해시(hash)를 업데이트한 뒤 관련된 모든 accept 이벤트 핸들러 함수를 호출합니다.
5. 구현 세부 사항
구현 측면에서 보면, 애플리케이션은 초기화 시 Webpack Dev Server와 WebSocket 연결을 설정합니다:

Webpack Dev Server는 애플리케이션에 일련의 메시지를 보냅니다:
o
a["{"type":"log-level","data":"info"}"]
a["{\"type\":\"hot\"}"]
a["{"type":"liveReload"}"]
a["{"type":"hash","data":"411ae3e5f4bab84432bf"}"]
a["{"type":"ok"}"]
파일 내용이 변경되면 Webpack Dev Server가 애플리케이션에 알립니다:
a["{"type":"invalid"}"]
a["{"type":"invalid"}"]
a["{"type":"hash","data":"a0b08ce32f8682379721"}"]
a["{"type":"ok"}"]
이어서 HMR Runtime은 HTTP 요청을 보내 모듈 업데이트 목록을 가져옵니다:
XHR GET http://localhost:8080/411ae3e5f4bab84432bf.hot-update.json
{"h":"a0b08ce32f8682379721","c":{"main":true}}
그리고 script 태그를 통해 모든 모듈 업데이트를 "다운로드"합니다:
SCRIPT SRC http://localhost:8080/main.411ae3e5f4bab84432bf.hot-update.js
webpackHotUpdate("main", {
"./src/App.js": (function(module, __webpack_exports__, __webpack_require__) {
// (새로운) 파일 내용
})
})
이와 같은 방식으로, 런타임의 HMR Runtime은 컴파일 시점의 파일 변�� 사항을 성공적으로 가져오며, 그다음 새 모듈을 모듈 시스템(modules 거대 객체)에 밀어 넣습니다:
// insert new code
for (moduleId in appliedUpdate) {
if (Object.prototype.hasOwnProperty.call(appliedUpdate, moduleId)) {
modules[moduleId] = appliedUpdate[moduleId];
}
}
마지막으로 accept 이벤트를 통해 애플리케이션 계층에 새 모듈을 사용하여 "부분 새로 고침"을 수행하도록 알립니다:
// call accept handlers
for (moduleId in outdatedDependencies) {
module = installedModules[moduleId];
if (module) {
moduleOutdatedDependencies = outdatedDependencies[moduleId];
var callbacks = [];
for (i = 0; i < moduleOutdatedDependencies.length; i++) {
dependency = moduleOutdatedDependencies[i];
cb = module.hot._acceptedDependencies[dependency];
if (cb) {
if (callbacks.indexOf(cb) !== -1) continue;
callbacks.push(cb);
}
}
for (i = 0; i < callbacks.length; i++) {
// accept 모듈 업데이트 이벤트 트리거
cb(moduleOutdatedDependencies);
}
}
}
이로써 모든 과정이 명확해졌습니다.
아직 댓글이 없습니다