LHJ

I'm a FE developer.

핫로딩

04 Oct 2020 » node_webpack2

핫로딩 (핫 모듈 리플레이스먼트)

이번에는 한 모듈 리플레이스먼트라는 웹팩 기능에 대해 살펴보도록 하겠다.
지난 시간에 웹팩 서버를 세팅하는 방법을 알아봤다.
그래서 파일이 변화함에 따라서 브라우저가 자동으로 갱신되는 그런 기능이 추가되었다.
이런식으로 개발환경이 자동화되면 개발 속도가 빨라진다.

배경

웹팩 개발 서버는 코드의 변화를 감지해서 전체 화면을 갱신하기 때문에 개발 속도를 높일 수 있다.
하지만 어떤 상황에서는 전체 화면을 갱신하는 것이 좀 불편한 경우도 있다.
싱글페이지 어플리케이션(SPA)은 브라우저에서 데이터를 들고 있기 때문에 리프레시 후에 모든 데이터가 초기화 되어버리기 때문이다.
다른 부분을 수정했는데 입력한 폼 데이터가 날라가 버리는 경우도 있다.

전체 화면 갱신하지 않고 변경한 모듈만 바꿔치기한다면 어떨까?
핫 모듈 리플레이스먼트는 이러한 목적으로 제공되는 웹팩 개발서버의 한 기능이다.

일단 다음 코드를 봐보자.
다음은 진입점 app.js 파일의 코드이다.

// app.js
import form from "./form";
import result from "./result";

document.addEventListener('DOMContentLoaded', async () => {
    const formEl = document.createElement("div");
    formEl.innerHTML = form.render();
    document.body.appendChild(formEl);
    
    const resultEl = document.createElement("div");
    resultEl.innerHTML = await result.render();
    document.body.appendChild(resultEl);
})

form.jsresult.js 파일을 import해와 각각 render 메서드로 HTML 렌더링을 하고있다.
form.jsresult.js 파일 내용을 봐보자.

// form.js
const form = {
    render() {
        return `
            <form>
                <input />
                <button type="submit">검색</button>
                <button type="reset">취소</button>
            </form>
        `;
    }
}

export default form;
// result.js
import axios from "axios";

const result = {
    async render() {
        const res = await axios.get("/api/users");
    
        return (res.data || [])
            .map(user => {
                return `<div>${user.id}: ${user.name}</div>`
            })
            .join("");
    }
}

export default result;

그리고 npm start를 통해 웹팩 데브 서버를 실행시킨다.

그럼 위와 같은 화면이 생성되는 걸 볼 수 있다.
위에 인풋과 버튼 두 개 생성된 것이 form 모듈이고, 그 아래 api 결과를 나타내는 것이 result 모듈이다.

이 두 모듈을 활용해 화면을 그렸는데, 만약에 result 모듈을 수정한다고 가정하자.

그럼 위와 같이 바로 반영된다.

하지만 문제는 위와 같은 경우이다.
form모듈의 인풋 안에 텍스트를 작성하고 result 모듈을 수정하면 인풋 안에 적어놓은 텍스트도 같이 리프래시된다.
전체화면이 리프레시 되는 것이다.

핫 모듈 리플레이스먼트라는 것은 변경한 모듈만 갈아치우는 것을 의미한다.
즉 전체화면을 리프레시 하지않고 변경된 모듈만 리프레시하기 때문에 다른 모듈은 이전 데이터를 유지할 수 있다.
그래서 화면 개발할 때 좀 더 빠르게 개발할 수 있다.

설정

// webpack.config.js
module.exports = {
    devServer: {
        overlay: true,
        stats: "errors-only",
        before: (app) => {
            app.use(apiMocker("/api", "mocks/api"));
        },
        hot: true,
    },
}

아주 간단하다.
devServer 안에 hot 플래그를 true라고만 설정해주면 된다.
이렇게 하고 다시한번 npm start를 실행해보자.

이전과 다른 메시지가 콘솔창에 찍힌다.
핫 모듈 기능이 활성화됨을 알 수 있다.

이렇게 웹팩 데브 서버는 간단히 핫 모듈 리플레이스먼트 기능을 활성화 시킬 수 있다.
그런데 이 핫 모듈 리플레이스먼트를 제대로 사용하려면, 제공하는 모듈이 핫모듈 리플레이스먼트(HMR) 인터페이스를 맞춰줘야된다.
app.js 파일에 다음과 같은 코드를 추가해보자.

// app.js
import form from "./form";
import result from "./result";

document.addEventListener('DOMContentLoaded', async () => {
    const formEl = document.createElement("div");
    formEl.innerHTML = form.render();
    document.body.appendChild(formEl);
    
    const resultEl = document.createElement("div");
    resultEl.innerHTML = await result.render();
    document.body.appendChild(resultEl);
})

if (module.hot) {
    console.log('핫 모듈 켜짐');
}

hot 플래그를 true로 설정하면 module.hot에 값이 들어온다.

콘솔창에 메시지가 찍히는 것을 확인할 수 있다.
콘솔창에 해당 메시지가 찍혔다는 것은 핫 모듈 리플레이스먼트(HMR)이 켜져있다는 얘기다.
아래와 같이 수정해보자.

// app.js
import form from "./form";
import result from "./result";

document.addEventListener('DOMContentLoaded', async () => {
    const formEl = document.createElement("div");
    formEl.innerHTML = form.render();
    document.body.appendChild(formEl);
    
    const resultEl = document.createElement("div");
    resultEl.innerHTML = await result.render();
    document.body.appendChild(resultEl);
})

if (module.hot) {
    console.log('핫 모듈 켜짐');

    module.hot.accept('./result', () => {
        console.log('result 모듈 변경됨');
    });
}

module.hot.accept();에서 변경을 감지할 모듈을 정할 수 있는데, 위와 같이 작성해보도록 하겠다.
그리고 result.js 모듈 내용을 수정해보자.

그럼 콘솔창에 result 모듈 변경됨이라는 메시지가 찍힌다.
form.js 모듈을 수정하면 위 메시지는 안 찍힌다.
즉, module.hot.accept()에다가 감지하고자하는 모듈명을 입력하면, 그 모듈이 변경되었음을 인지하고 콜백함수가 실행된다.
즉, 콜백함수 안에다가 모듈을 바꿔치기하는 코드를 넣으면 되는 것이다.

// app.js
import form from "./form";
import result from "./result";

let resultEl;

document.addEventListener("DOMContentLoaded", async () => {
    const formEl = document.createElement("div");
    formEl.innerHTML = form.render();
    document.body.appendChild(formEl);

    resultEl = document.createElement("div");
    resultEl.innerHTML = await result.render();
    document.body.appendChild(resultEl);
});

if (module.hot) {
    console.log("핫 모듈 켜짐");

    module.hot.accept("./result", async () => {
        console.log("result 모듈 변경됨");
        resultEl.innerHTML = await result.render();
    });
}

위와 같이 수정해주자.

그럼 위와 같이 result.js 모듈이 수정되었을 땐 result.js 모듈 부분만 변하는 것을 볼 수 있다.
이것이 바로 핫 모듈 리플레이스먼트의 동작 원리이다.

그럼 마찬가지로 form.js 모듈도 이렇게 해줄 수 있다.

// app.js
import form from "./form";
import result from "./result";

let resultEl;
let formEl;

document.addEventListener("DOMContentLoaded", async () => {
    formEl = document.createElement("div");
    formEl.innerHTML = form.render();
    document.body.appendChild(formEl);

    resultEl = document.createElement("div");
    resultEl.innerHTML = await result.render();
    document.body.appendChild(resultEl);
});

if (module.hot) {
    console.log("핫 모듈 켜짐");

    module.hot.accept("./result", async () => {
        console.log("result 모듈 변경됨");
        resultEl.innerHTML = await result.render();
    });

    module.hot.accept("./form", async () => {
        formEl.innerHTML = form.render();
    });
}

핫 로딩을 지원하는 로더

이런 핫 모듈을 지원하려면 webpack.config.jsdevServer키에서 hottrue로 설정해주는 것 뿐만아니라, 모듈 자체가 핫모듈 리플레이스먼트를 지원해야된다.
지원한다는 것은

if (module.hot) {
    console.log("핫 모듈 켜짐");

    module.hot.accept("./result", async () => {
        console.log("result 모듈 변경됨");
        resultEl.innerHTML = await result.render();
    });

    module.hot.accept("./form", async () => {
        formEl.innerHTML = form.render();
    });
}

이러한 인터페이스를 맞춘다는 것인데, 바로 HMR 인터페이스를 맞춘다라고 표현을 한다.
그래서 이러한 HMR 인터페이스를 구현해야되는데, 우리가 사용하는 로더에도 이러한 HMR 인터페이스를 구현해놓은 로더들이 있다.

이러한 HMR 인터페이스를 구현한 로더만이 핫 로딩을 지원하는데 대표적인 것이 웹팩 기본편에서 보았던 style-loader가 그렇다.
style-loader의 코드를 보면 hot.accept() 함수를 사용한 것을 알 수 있다.

이 외에도 리액트를 지원하는 react-hot-loader, 파일을 지원하는 file-loader는 핫 모듈 리플레이스먼트를 지원한다.

다음과 같이 app.jsimport "./app.css"를 추가하자.

// app.js
import form from "./form";
import result from "./result";
import "./app.css";

let resultEl;
let formEl;

document.addEventListener("DOMContentLoaded", async () => {
    formEl = document.createElement("div");
    formEl.innerHTML = form.render();
    document.body.appendChild(formEl);

    resultEl = document.createElement("div");
    resultEl.innerHTML = await result.render();
    document.body.appendChild(resultEl);
});

if (module.hot) {
    console.log("핫 모듈 켜짐");

    module.hot.accept("./result", async () => {
        console.log("result 모듈 변경됨");
        resultEl.innerHTML = await result.render();
    });

    module.hot.accept("./form", async () => {
        formEl.innerHTML = form.render();
    });
}

그럼 이렇게 배경이 적용된 것을 볼 수 있다.
그럼 이번엔 css 코드를 변경해보자.
css 코드를 변경하면 전체화면을 갱신하는 것이 아니라 css 스타일만 갱신할 것이다.

인풋 안에있는 ‘전체화면 갱신안됨’ 이란 문구가 계속 유지되는 것을 확인할 수 있다.

위 링크에 들어가면

우리가 아까 작성한 것처럼 module.hot 이라는 플래그가 켜져있으면, module.hot.accept 함수를 써서 모듈을 감시하도록 하고 있다.
뿐만아니라 file-loader도 핫모듈 리플레이스먼트를 지원한다.

위와 같이 file-loader의 핫 모듈 리플레이스먼트 기능에의해 배경이미지만 갱신되는 것을 볼 수 있다.
file-loader에도 있으니 url-loader에도 핫 모듈 리플레이스먼트 기능이 당연히 있다.

이렇게 핫모듈 리플레이스먼트 기능을 지원하는 모듈들이 꽤 많은데, 리액트도 이러한 기능을 지원하는 react-hot-module이 있다.
vue도 마찬가지로 지원한다.
vue-loader 자체적으로 핫로딩을 지원한다.