webpack 소스를 수정할 때마다 build를 해서 확인해야 하는 불편함이 있다. development 모드에서 수정할 때마다 전체 새로고침 없이 모든 종류의 모듈들을 런타임 시점에 업데이트 되게 해주는 HMR (Hot Module Replacement)을 사용한다.
npm install --save-dev express webpack-hot-middleware webpack-dev-middleware
webpack-hot-middleware는 webpack dev server의 hot reloading과 Express 서버를 결합시키는 매우 유용한 툴이다.
웹팩으로 빌드한 정적파일을 처리하는 익스프레스 스타일 미들웨어이다. 웹팩 패키지가 제공하는 함수를 실행하면 Compiler 타입의 인스턴스를 반환해준다. 웹팩 설정 객체를 함수 인자로 전달하는데 보통은 설정 파일 webpack.config.js에 있는 코드를 가져다 사용한다.
webpack.config.js plugins 수정const webpack = require('webpack');
plugins: [
//...
new webpack.HotModuleReplacementPlugin()
]
package.json webpack command line 수정{
"scripts": {
"build": "cross-env NODE_ENV=production webpack",
"start": "cross-env NODE_ENV=development node webpack/dev"
}
}
webpack/dev.js 파일 생성const webpack = require('webpack');
const webpackConfig = require('../webpack.config');
const compiler = webpack(webpackConfig);
const webpackDevMiddleware = require('webpack-dev-middleware');
const webpackHotMiddleware = require('webpack-hot-middleware');
const express = require('express');
const app = express();
app.use(webpackDevMiddleware(compiler, {
hot: true,
noInfo: true,
publicPath: webpackConfig.output.publicPath,
stats: 'minimal',
historyApiFallback: true
}));
app.use(webpackHotMiddleware(compiler));
app.listen(3000, () => console.log('http://localhost:3000'));
webpack/entry.js 파일 생성require('../src/app.js');
if(module.hot) {
module.hot.accept();
}
webpack.config.js 파일 수정const isDev = process.env.NODE_ENV === 'development' // 모드 구분
module.exports = {
mode: isDev ? 'development' : 'production',
entry: isDev ? ['webpack-hot-middleware/client', './webpack/entry.js'] : './webpack/entry.js',
// ...
plugins: [
// ...
new webpack.HotModuleReplacementPlugin()
]
}
npm run start
webpack 실행 시 아래와 같은 오류가 발생했다. webpackDevMiddleware 미들웨어에 알 수 없는 옵션이 사용되었다는 내용이다. 전에 설정할 때는 이런 오류가 없었는데 아마 버전 업데이트 되면서 전에 쓰던 옵션들이 없어진거 같다.
Invalid options object. Dev Middleware has been initialized using an options object that does not match the API schema. options has an unknown property 'historyApiFallback'. These properties are valid. object { mimeTypes?, writeToDisk?, methods?, headers?, publicPath?, serverSideRender?, outputFileSystem?, index? }
webpack/dev.js 파일에서 publicPath 옵션만 설정하고 나머지는 삭제한다.
app.use(webpackDevMiddleware(compiler, {
publicPath: webpackConfig.output.publicPath
}));
다시 실행하여 http://localhost:3000로 접속하여 css, scss, js 등 파일을 수정하면 새로고침하지 않아도 바로 적용되는걸 확인 할 수 있다.
링크 참고 mini-css-extract-plugin HMR
개발모드에서 css, scss import 시 아래와 같은 에러 발생 시
Module build failed (from ./node_modules/mini-css-extract-plugin/dist/loader.js): ReferenceError: document is not defined
webpack.config.js파일 수정
{
test: /\.(sa|sc|c)ss$/,
use: [
isDev ? 'style-loader' : {
loader: MiniCssExtractPlugin.loader,
options: {
publicPath: '/'
}
},
'css-loader',
'sass-loader'
]
},
웹팩이 js, css, 이미지 파일을 번들링한 뒤 서버에서 제공하는 html 파일에서 이를 로딩한다. 만약 html 파일까지도 웹팩 빌드 프로세스에 추가하려면 html-webpack-plugin을 사용할 것이다.
이렇게되면 webpack-dev-middleware를 사용해 서버를 구성할 때 html 파일을 서비스해야하는데 웹팩이 만든 html파일의 위치를 찾아야 한다.
webpack() 함수가 반환한 compiler 객체는 outputFileSystem이란 객체를 가지고 있다. 빌드한 결과물을 위한 별도의 파일 시스템 인터페이스인 셈이다. 이 객체 메소드 중에 readFile()로 빌드한 결과물의 내용을 읽을 수 있다.
/webpack/dev.js const RESULT_FILE = path.join(compiler.outputPath, 'index.html');
app.use(async(req, res, next) => {
if(!req.method === 'GET') return next();
if (process.env.NODE_ENV === "development") {
compiler.outputFileSystem.readFile(RESULT_FILE, (err, result) => {
if(err) return next(err);
res.set('content-type','text/html');
res.send(result);
res.end();
});
}
if (process.env.NODE_ENV === "production") {
res.sendFile(path.join(__dirname, "../public/index.html"));
}
});
require('../src/index.js');
if(module.hot) {
let prevTimeoutIndex = -1;
let prevIntervalIndex = -1;
let prevRAFIndex = -1;
module.hot.accept((err) => {
console.log('err', err);
});
module.hot.dispose(data => {
console.log(module.hot.status());
const tIdx = setTimeout(() => {});
for (let i = prevTimeoutIndex; i < tIdx; i++) clearTimeout(i);
prevTimeoutIndex = tIdx;
const iIdx = setInterval(() => {});
for (let i = prevIntervalIndex; i < iIdx; i++) clearInterval(i);
prevIntervalIndex = iIdx;
const rIdx = requestAnimationFrame(() => {});
for (let i = prevRAFIndex; i < rIdx; i++) cancelAnimationFrame(i);
prevRAFIndex = rIdx;
});
}
setTimeout(() => {}) 를 콘솔로 찍어봤을 때, 숫자가 나오는데 그 숫자의 의미는 해당 브라우저에서 setTimeout 함수 사용 횟수이다. setTimeout 를 해제하기 위해 clearTimeout 함수를 이용하는데 사용법은 다음과 같다
const timeout = setTimeout(() => console.log('hi'), 1000);
clearTimeouf(timeout);
이렇게 clearTimeout를 사용하는데 함수 인자값은 setTimeout를 담고 있는 변수 즉 숫자인건데, 그 숫자는 해당 setTimeout 함수의 key 값 인것이다. ...setInterval, requestAnimationFrame 도 만찬가지
위 코드가 하는 기능은 HMR 모듈이 다시 생성될 때 수정 전에 사용되었던 setTimeout, setInterval, requestAnimationFrame 함수를 모두 멈추고 다시 실행되로록 해준다. (함수들이 겹치지 않기위해)