4 minute read

Section 5. 프론트엔드 개발환경의 이해: Webpack 심화

REF: 김정환님의 블로그

1. 웹팩 개발 서버

1.1 배경

운영환경과 유사하게 개발하기 위해 개발용 서버를 제공해 주는 webpack-dev-server를 사용해보자.
ajax 방식의 api 연동은 cors 정책 때문에 반드시 서버가 필요하다.

1.2 설치 및 사용

npm i -D webpack-dev-server
// package.json
"scripts": {
    "build": "webpack",
    "lint": "eslint src --fix",
    "start": "webpack-dev-server"
  },
npm start

1.3 기본 설정

// webpack.config.js:
module.exports = {
  //...
  devServer: {
    contentBase: path.join(__dirname, "dist"),
    publicPath: "/",
    host: "dev.domain.com",
    overlay: true,
    port: 8081,
    stats: "errors-only",
    historyApiFallback: true,
  },
};
  • contentBase: 정적파일을 제공할 경로. 기본값은 웹팩 아웃풋이다.
  • publicPath: 브라우저를 통해 접근하는 경로. 기본값은 ‘/’이다.
  • host: 개발환경에서 도메인을 맞춰야 할 때 사용한다. 예를들어 쿠키 기반의 인증은 인증 서버와 동일한 도메인으로 개발환경을 맞추어야 한다. 운영체제의 호스트 파일에 해당 도메인과 127.0.0.1을 연결해 추가한 뒤 host 속성에 도메인을 설정해서 사용한다.
  • overlay: 웹팩 빌드시 에러나 경고를 브라우저 화면에 표시한다.
  • port: 개발 서버 포트 번호를 설정한다. 기본값은 8080.
  • stats: 메시지 수준을 정할 수 있다. ‘none’, ‘errors-only’, ‘minimal’, ‘normal’, ‘verbose’로 메세지 수준을 조절한다.
  • historyApiFallBack: 히스토리 API를 사용하는 SPA 개발시 설정한다. 404가 발생하면 index.html로 리다이렉트한다.
  • 이 외에도 개발 서버를 실행할 때 명령어 인자로 –progress를 추가하면 빌드 진행율을 보여준다. 빌드 시간이 길어질 경우 사용하면 좋다.

2. API 연동

mock up 데이터를 이용하여 api를 대체하면서 개발한다.

2.1 목업 API 1: devServer.before

웹팩 개발 설정 중 before 속성은 개발 서버에 기능을 추가할 수 있게 한다. Express.js로 만들어져 있고 미들웨어 형태로 서버 기능 확장이 가능하다.

// webpack.config.js
module.exports = {
  devServer: {
    before: (app, server, compiler) => {
      app.get("/api/keywords", (req, res) => {
        res.json([
          { keyword: "이탈리아" },
          { keyword: "세프의요리" },
          { keyword: "제철" },
          { keyword: "홈파티" },
        ]);
      });
    },
  },
};
// src/app.js
import axios from "axios";

document.addEventListener("DOMContentLoaded", async () => {
  const res = await axios.get("/api/users");
  console.log(res);
  document.body.innerHTML = `
    `;
});

2.2 목업 API 2: connect-api-mocker

목업 api 작업이 많을 때는 connect-api-mocker 패키지의 도움을 받자.

npm i -D connect-api-mocker
// mocks/api/keywords/GET.json
[
  { "keyword": "이탈리아" },
  { "keyword": "세프의요리" },
  { "keyword": "제철" },
  { "keyword": "홈파티" }
]
// webpack.config.js
const apiMocker = require("connect-api-mocker");

before: (app) => {
  app.use(apiMocker("/api", "mocks/api"));
};

2.3 실제 API 연동: devServer.proxy

8081 포트로 서버가 구성되어 있다고 가정하고 설정해보자.

// src/model.js
const model = {
  async get() {
    // const result = await axios.get('/api/keywords');

    // 직접 api 서버로 요청한다.
    const { data } = await axios.get("http://localhost:8081/api/keywords");
    return data;
  },
};

CORS 정책 때문에 오류가 발생한다.
서버에서 Access-Control-Allow-Origin 설정을 하거나 프론트에서 아래와 같이 설정한다.

// webpack.config.js
module.exports = {
  devServer: {
    proxy: {
      "/api": "http://localhost:8081", // 프록시
    },
  },
};

http 요청 중 /api로 시작되는 것은 http://localhost:8081로 요청하는 설정이다.

// src/model.js
const model = {
  async get() {
    // const { data } = await axios.get('http://localhost:8081/api/keywords');

    const { data } = await axios.get("/api/keywords");
    return data;
  },
};

3. 핫 모듈 리플레이스먼트

3.1 배경

전체 화면을 새로고침하지 않고 변경된 부분만 새로고침 된다.

3.2 설정

// webpack.config.js:
module.exports = {
  devServer = {
    hot: true,
  },
}

아래와 같이 동작한다.

// src/app.js

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

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

  module.hot.accept("./view", async () => {
    view.render(await model.get(), controller.el); // 변경된 모듈로 교체
  });
}

3.3 핫로딩을 지원하는 로더

HMR 인터페이스를 구현한 로더만이 핫 로딩을 지원하는데 style-loader가 그렇다.
react-hot-loader, file-loader 또한 핫 모듈 리플레이스먼트를 지원한다.

4. 최적화

메가바이트 단위로 커질 수 있는 번들 결과물을 최적화 해보자.

4.1 production 모드

development mode 일 때 아래 두 개 플러그인을 사용한다.

  • NamedChunksPlugin
  • NamedModulesPlugin

production mode 일때 아래의 플러그인을 사용한다.

  • FlagDependencyUsagePlugin
  • FlagIncludedChunksPlugin
  • ModuleConcatenationPlugin
  • NoEmitOnErrorsPlugin
  • OccurrenceOrderPlugin
  • SideEffectsFlagPlugin
  • TerserWebpackPlugin

DefinePlugin을 사용해 process.env.NODE_ENV 값을 mode 변경에 활용할 수 있다.

// webpack.config.js:
const mode = process.env.NODE_ENV || "development"; // 기본값을 development로 설정

module.exports = {
  mode,
};
// package.json
{
  "scripts": {
    "start": "webpack-dev-server --progress",
    "build": "NODE_ENV=production webpack --progress"
  }
}

4.2 optimization 속성으로 최적화

빌드 과정을 커스터마이징 할 수 있는데 optimization 속성으로 가능하다.
css파일도 빈칸없이 압축하려면 optimize-css-assets-webpack-plugin을 사용한다.

npm i -D optimize-css-assets-webpack-plugin
// webpack.config.js:
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");

module.exports = {
  optimization: {
    minimizer: mode === "production" ? [new OptimizeCSSAssetsPlugin()] : [],
  },
};

TerserWebpackPlugin은 자바스크립트 코드를 난독화하고 debugger 구문을 제거한다.
기본 설정 외에도 console.log를 제거하는 옵션도 있다.

npm i -D terser-webpack-plugin
// webpack.config.js:
const TerserPlugin = require("terser-webpack-plugin");

module.exports = {
  optimization: {
    minimizer:
      mode === "production"
        ? [
            new TerserPlugin({
              terserOptions: {
                compress: {
                  drop_console: true, // 콘솔 로그를 제거한다
                },
              },
            }),
          ]
        : [],
  },
};

4.3 코드 스플리팅

압축하는 것 외에도 결과물을 여러개로 쪼개면 브라우저 속도를 높일 수 있다.

가장 단순한 것은 엔트리를 여러개로 분리하는 것이다.

// webpack.config.js
module.exports = {
  entry: {
    main: "./src/app.js",
    controller: "./src/controller.js",
  },
};

두 파일에 중복코드가 있다.
SplitChunksPlugin은 코드를 분리할 때 중복을 예방하는 플러그인이다.
optimization.splitChunks 속성을 사용한다.

// webpack.config.js:
module.exports = {
  optimization: {
    splitChunks: {
      chunks: "all",
    },
  },
};

엔트리 포인트를 직접 분리하지 않고 자동으로 변경해 주는 “다이나믹 임포트”를 사용할 수 있다.

기존 코드

import controller from "./controller";

document.addEventListener("DOMContentLoaded", () => {
  controller.init(document.querySelector("#app"));
});

동적 임포트

function getController() {
  return import(/* webpackChunkName: "controller" */ "./controller").then(
    (m) => {
      return m.default;
    }
  );
}

document.addEventListener("DOMContentLoaded", () => {
  getController().then((controller) => {
    controller.init(document.querySelector("#app"));
  });
});

webpackChunkName을 주석으로 전달하여 웹팩이 이 파일을 처리할 때 청크로 분리한다.

4.4 externals

axios같은 써드파티 라이브러리는 패키지로 제공될 때 이미 빌드 과정을 거쳤기 때문에 빌드 프로세스에서 제외하는 것이 좋다. externals가 바로 이러한 기능을 제공한다.

// webpack.config.js:
module.exports = {
  externals: {
    axios: "axios",
  },
};

번들에 포함하지 않고 빌드한다. 대신 이를 전역 변수로 접근하도록 하는데 키로 설정한 axios가 그 이름이다.

aixos는 이미 node_modules에 위치해 있기 때문에 이를 웹팩 아웃풋 폴더에 옮기고 index.html에서 로딩해야 한다. 파일을 복사하는 CopyWebpackPlugin을 사용해보자.

npm i -D copy-webpack-plugin
const CopyPlugin = require("copy-webpack-plugin");

module.exports = {
  plugins: [
    new CopyPlugin({
      patterns: [
        {
          from: "./node_modules/axios/dist/axios.min.js",
          to: "./axios.min.js", // 목적지 파일에 들어간다
        },
      ],
    }),
  ],
};
<!-- src/index.html -->
  <script type="text/javascript" src="axios.min.js"></script>
</body>
</html>