Posts [프론트엔드 개발환경의 이해와 실습] 웹팩 최적화
Post
Cancel

[프론트엔드 개발환경의 이해와 실습] 웹팩 최적화

웹팩 최적화

코드가 많아지면 번들링된 결과물도 커지기 마련이다. 거의 메가바이트 단위로 커질수도 있는데 브라우져 성능에 영향을 줄 수 있다. 파일을 다운로드하는데 시간이 많이 걸리기 때문이다. 이번 포스팅에서는 번들링한 결과물을 어떻게 최적화 할수 있는지 몇가지 방법에 대해 알아볼 것이다.

1) production 모드

웹팩에 내장되어 있는 최적화 방법중 mode 값을 설정하는 방식이 가장 기본이다. 세 가지 값이 올 수 있는데 지금까지 설정한 "development"디버깅 편의를 위해 아래 두 개 플러그인을 사용한다.

  • NamedChunksPlugin
  • NamedModulesPlugin

DefinePlugin을 사용한다면 process.env.NODE_ENV 값이 "development"로 설정되어 어플리케이션에 전역변수로 주입된다.

반면 mode를 "production"으로 설정하면 자바스크립트 결과물을 최소화 하기 위해 다음 일곱 개 플러그인을 사용한다.

  • FlagDependencyUsagePlugin
  • FlagIncludedChunksPlugin
  • ModuleConcatenationPlugin
  • NoEmitOnErrorsPlugin
  • OccurrenceOrderPlugin
  • SideEffectsFlagPlugin
  • TerserPlugin

DefinePlugin을 사용한다면 process.env.NODE_ENV 값이 “production” 으로 설정되어 어플리케이션 전역변수로 들어간다.

그럼 환경변수 NODE_ENV 값에 따라 모드를 설정하도록 웹팩 설정 코드를 다음과 같이 추가할 수 있겠다.

  • webpack.config.js
1
2
3
4
5
const mode = process.env.NODE_ENV || "development" // 기본값을 development로 설정

module.exports = {
  mode,
}

빌드 시에 이를 운영 모드로 설정하여 실행하도록 npm 스크립트를 추가한다.

  • package.json
1
2
3
4
5
6
{
  "scripts": {
    "start": "webpack-dev-server --progress",
    "build": "NODE_ENV=production webpack --progress"
  }
}

start는 개발 서버를 구동하기 때문에 환경변수를 설정하지 않고 기본값 development를 사용할 것이다. 배포용으로 만들 build는 환경변수를 production으로 설정했고 웹팩 mode에 설정된다.

빌드한 뒤 결과물을 확인해 보자.

1
npm run build

스크린샷 2022-03-27 오후 2 37 49 development 모드의 js 번들 파일

스크린샷 2022-03-27 오후 2 38 21 production 모드의 js 번들 파일

모드에 따른 빌드 결과물들을 비교해 보면 확연한 차이를 볼 수 있다. production모드는 난독화 작업이 들어가기에 알아보기 힘든 코드로 이루어진 것을 확인할 수 있다.

2) optimization 속성으로 최적화

빌드 과정을 커스터마이징할 수 있는 여지를 제공하는데 그것이 바로 optimization 속성이다.

HtmlWebpackPlugin이 html 파일을 압축한것 처럼 css 파일도 빈칸을 없애는 압축을 하려면 어떻게 해야할까? optimize-css-assets-webpack-plugin을 사용하면 된다.

플러그인을 다운로드 하고,

1
npm i -D optimize-css-assets-webpack-plugin

웹팩 설정을 추가한다.

  • webpack.config.js
1
2
3
4
5
6
7
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin")

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

optimization.minimizer는 웹팩이 결과물을 압축할때 사용할 플러그인을 넣는 배열이다. 설치한 OptimizeCSSAssetsPlugin을 전달해서 빌드 결과물중 css 파일을 압축하도록 했다.

빌드하뒤 확인하면 css 파일도 압축된 것을 확인할 수 있다.

스크린샷 2022-03-27 오후 2 55 17

mode=production일 경우 사용되는 TerserWebpackPlugin은 자바스크립트 코드를 난독화하고 debugger 구문을 제거한다. 기본 설정 외에도 콘솔 로그를 제거하는 옵션도 있는데 배포 버전에는 로그를 감추는 것이 좋을 수도 있기 때문이다.

이 플러그인을 설치한 뒤,

1
npm i -D terser-webpack-plugin

optimization.minimizer 배열에 추가한다.

  • webpack.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const TerserPlugin = require("terser-webpack-plugin")

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

스크린샷 2022-03-27 오후 2 57 40

빌드된 결과물을 보면 콘솔 로그가 제거된 것을 확인할 수 있다.

3) 코드 스플리팅(분할)

이렇게 해서 js, css를 압축했음에도 프로젝트가 커지면 파일을 다운로드하는데도 시간이 걸릴 것이고, 브라우저 성능에 영향을 줄 것이다.

이럴땐 큰 파일을 아예 쪼개서 분할하는 방법이 있다. 큰 파일 하나를 다운로드 하는것 보다 작은 파일 여러개를 동시에 다운로드하는 것이 더 빠르기 때문이다.

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

  • webpack.config.js
1
2
3
4
5
6
module.exports = {
  entry: {
    main: "./src/app.js",
    result: "./src/result.js"
  },
}

빌드하면 엔트리가 두 개 생성되고 물론 하나의 엔트리일 때보다 용량이 조금 줄었다.

스크린샷 2022-03-27 오후 3 10 22

모듈을 어떻게 분리하는냐에 따라 이 결과물의 크기를 조절할 수 있는데 지금은 거의 변화가 없다. HtmlWebpackPlugin에 의해 html 코드에서 두 파일을 로딩하는 코드도 추가된다.

하지만 두 파일을 비교해 보면 중복코드가 있다.

스크린샷 2022-03-27 오후 3 12 08 main.js 파일의 axios 중복 코드

스크린샷 2022-03-27 오후 3 12 19 result.js 파일의 axios 중복 코드

axios 모듈인데 main, result 둘 다 axios를 사용하기 때문이다.

SplitChunksPlugin은 코드를 분리할때 중복을 예방하는 플러그인이다. optization.splitChucks 속성을 설정하는 방식이다.

  • webpack.config.js
1
2
3
4
5
6
7
module.exports = {
  optimization: {
    splitChunks: {
      chunks: "all",
    },
  },
}

다시 빌드해보면 main과 result에서 중복된 코드가 사리지고 vendors~main~result.js 파일이 생기고 사라진 중복 코드가 들어가 있는 것을 확인할 수 있다.

스크린샷 2022-03-27 오후 3 14 55

이런 방식은 엔트리 포인트를 적절히 분리해야기 때문에 손이 많이 가는 편이다. 반면 자동으로 변경해 주는 방식이 있는데 이를 다이나믹 임포트라고 부른다.

다이나믹 임포트

기존 app.js 를 아래와 같이 변경해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import form from "./form";
// import result from "./result";
import "./app.css";

let formEl;
let resultEl;

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

  import(/* webpackChunkName: "result" */"./result.js").then(async m => {
    const result = m.default;
    resultEl = document.createElement("div");
    resultEl.innerHTML = await result.render();
    document.body.appendChild(resultEl);
  })
});

상단에서 import로 가져왔던것을 함수 내부에서 가져오도록 변경하였다. import() 함수로 가져올 result 모듈 경로를 전달하는데 주석으로 webpackHunkName: "result"를 전달했다. 이것은 웹펙이 이 파일을 처리할때 청크로 분리하는데 그 청크 이름을 설정한 것이다.

변경한 웹팩 설정 파일도 다시 복구해야 한다. 엔트리 포인트를 다시 main만 남겨두고 optimization에 설정한 SplitChunksPlugin 옵션도 제거한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
module.exports = {
  entry: {
    main: "./src/app.js",
    // result: "./src/result.js"
  },
  optimization: {
    minimizer: mode === "production" ? [
      new OptimizeCSSAssertsPlugin(),
      new TerserPlugin({
        terserOptions: {
          compress: {
            drop_console: true // 콘솔 로그를 제거한다.
          }
        }
      })
    ] : [],
    // splitChunks: {
    //   chunks: "all" // 중복되는 코드를 제거하고 엔트리 포인트를 다시 빌드한다.
    // }
  },

빌드하면 자동으로 파일이 분리되었다.

스크린샷 2022-03-27 오후 3 29 25

엔트리를 분리하지 않아도 app과 result의 중복코드를 vendors~result.js 파일로 분리할 수있다. 다이나믹 임포트로 모듈을 가져오면 단일 엔트리를 유지하면서 코드를 분리할 수 있다.

추가로 아래와 같이 별도 함수로 추출하여 다이나믹 임포트를 설정할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function getResult() {
  return import(/* webpackChunkName: "result" */ "./result").then(m => {
    return m.default
  })
}

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

  getResult().then(async m => {
    const result = m;
    resultEl = document.createElement("div");
    resultEl.innerHTML = await result.render();
    document.body.appendChild(resultEl);
  })
});

Note: 이런식으로 코드를 스플리팅 하는 것은 개발 초기단계에는 불필요하다. 코드가 많아지고 번들 결과물이 1메가 이상으로 커져버리면 그때가서 코드를 분리해도 늦지 않는다.

4) externals

조금만 더 생각해 보면 최적화해 볼 수 있는 부분이 있다. 바로 axios같은 써드파티 라이브러리다. 패키지로 제공될때 이미 빌드 과정을 거쳤기 때문에 빌드 프로세스에서 제외하는 것이 좋다. 웹팩 설정중 externals가 바로 이러한 기능을 제공한다

  • webpack.config.js
1
2
3
4
5
module.exports = {
  externals: {
    axios: "axios", //  웹팩으로 빌드할떄 axios모듈을 사용하는 부분이 있으면 전역변수 axios를 사용하는 것으로 간주하라는 설정
  },
}

externals에 추가하면 웹팩은 코드에서 axios를 사용하더라도 번들에 포함하지 않고 빌드한다. 대신 이를 전역 변수로 접근하도록하는데 키로 설정한 axios가 그 이름이다.

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

1
npm i -D copy-webpack-plugin

플러그인을 사용해서 라이브러리를 복사한다.

1
2
3
4
5
6
7
8
9
10
11
12
const CopyPlugin = require("copy-webpack-plugin")

module.exports = {
  plugins: [
    new CopyPlugin([
      {
        from: "./node_modules/axios/dist/axios.min.js",
        to: "./axios.min.js", // 목적지 파일에 들어간다
      },
    ]),
  ],
}

마지막으로 index.html에서는 axios를 로딩하는 코드를 추가한다.

1
2
3
4
<!-- src/index.html -->
  <script type="text/javascript" src="axios.min.js"></script>
</body>
</html>

axios는 이렇게 직접 추가했지만 번들링한 결과물은 htmlWebpackPlugin이 주입해 주는 것을 잊지말자.

다시 빌드해 보면…

스크린샷 2022-03-27 오후 3 50 57

axios는 빌드하지 않고 복사만 한다. app와 result 모듈이 분리되었다. 이전에는 공통의 코드인 axios가 vender~.js로 분리되었는데 지금은 파일조차 없다.

만약 써드파티 라이브러리 외에 공통의 코드가 있다면 이 파일로 분리되었을 것이다. 이렇게 써드파티 라이브러리를 externals로 분리하면 용량이 감소뿐만 아니라 빌드시간도 줄어들고 덩달아 개발 환경도 가벼워질 수 있다.

정리

웹팩 사용방법에 대해 좀더 알아 보았다.

개발 서버를 띄워 파일 감지, api 서버 연동 등 개발 환경을 좀 더 편리하게 구성할 수 있었다. 특히 핫 모듈 리플레이스먼트는 일부 모듈의 변경만 감지하여 페이지 갱신 없이 변경사항을 브라우져에 렌더링할 수 있다.

웹팩 최적화 방법에 대해서도 알아보았다. mode 옵션을 production으로 설정하면 웹팩 내장 플러그인이 프로덕션 모드로 동작한다. 번들링 결과물 크기가 커지면 브라우져에서 다운로딩하는 성능이 떨어질수 있는데 코드 스플리팅 기법을 사용해서 해결할 수 있다. 써드파티 라이브러리는 externals로 옮겨 빌드 과정에서 제외할수 있다.

출처

This post is licensed under CC BY 4.0 by the author.

[프론트엔드 개발환경의 이해와 실습] 웹팩 개발 서버

[프론트엔드 개발환경의 이해와 실습] 웹팩 핫로딩