프론트엔드 개발 환경 공부 #19 번들 결과 최적화

Jake Seo·2021년 9월 23일
1

번들 결과 최적화란?

번들링한 결과물의 코드가 커지면, 브라우저에서 해당 파일을 로드하는데 너무 많은 시간을 소모할 수 있다. 결과적으로 사용자가 기다려야 할 로딩 시간이 너무 길어진다. 그렇다면 번들링한 결과를 어떻게 최적화할 수 있을지 몇가지 방법에 대해 알아보자.

윈도우즈 환경에서 NODE_ENV 모드 설정 후에 웹팩 실행하기

SET 이용하기

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "SET NODE_ENV=production&webpack --progress",
    "start": "webpack-dev-server --progress"
  },

위와 같이 SET NODE_ENV=production&webpack --progress와 같은 방식으로 실행하면 된다.

cross-env 이용하기

설치

npm install -D cross-env
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "cross-env NODE_ENV=production webpack --progress",
    "start": "webpack-dev-server --progress"
  },

cross-env는 리눅스에서 사용하는 명령어들을 윈도우에서도 똑같이 지원할 수 있게 만든 의존성이다.

win-node-env 이용하기

설치

npm install -g win-node-env
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "NODE_ENV=production webpack --progress",
    "start": "webpack-dev-server --progress"
  },

이건 새로운 명령어를 생성해서 실행하는 방법인데, 환경이 바뀌었을 때 글로벌에 대한 의존성 추가를 명시하지 않으면 에러날 때 조금 헤맬 것 같아서 조금 걱정이 되는 방법이다.

그래도 이 방법을 쓰면 리눅스 환경에서 윈도우즈로 가져왔을 때, 아무런 명령어 변경도 안하고 그대로 실행 가능하다.

모드에 따른 최적화

Webpack Configuration Mode 공식문서

development 모드

웹팩 설정파일에서 mode 값을 설정하는 것이 최적화에서 생각보다 큰 의미를 갖는다. mode에는 총 3가지 값이 올 수 있는데,

development는 디버깅에 최적화된 모드이다.

지금까지 설정했던 development는 디버깅 편의를 위해 아래 두개의 플러그인을 사용하고 있었다.

  • NamedChunksPlugin (웹팩 v4)
  • NamedModulesPlugin (웹팩 v4)

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

production 모드

production은 배포에 최적화된 모드이다.

modeproduction으로 설정하면, 번들링한 결과물을 최소화하기 위해서 다음 7개 플러그인을 사용한다.

  • FlagDependencyUsagePlugin
  • FlagIncludedChunksPlugin
  • ModuleConcatenationPlugin
  • NoEmitOnErrorsPlugin
  • TerserPlugin
  • OccurrenceOrderPlugin (웹팩 v4)
  • SideEffectsFlagPlugin (웹팩 v4)

production 모드에서 DefinePlugin을 사용한다면, process.env.NODE_ENV의 값이 production으로 설정되어 애플리케이션에 전역변수로 주입된다.

모드 설정 코드 만들기

const mode = process.env.NODE_ENV || 'development'

module.exports = {
  mode,
}

webpack.config.js에 위와 같이 코드를 작성하면, process.env.NODE_ENV로 받은 값이 있을 때는 해당 mode로 실행시키고, 만일 없다면 기본 값인 development를 이용하게 된다.

프로덕션 빌드 결과물 확인해보기

코드를 알아볼 수 없게 난독화되어있다.

Optimization 속성에 따른 최적화

빌드 과정을 커스터마이징할 수 있는 속성이 optimization 속성이다.

CSS 최적화 - css-minimizer-webpack-plugin

HtmlWebpackPlugin이 html 파일을 압축했던것처럼 css 파일도 빈칸을 없애고 압축하고 싶다면 어떻게 해야 할까? css-minimizer-webpack-plugin을 사용하면 된다.

웹팩 버전 4 에서는 optimize-css-assets-webpack-plugin을 사용한다.

JS 최적화 - terser-webpack-plugin

TerserWebpackPlugin은 자바스크립트 코드를 난독화하고, debugger 구문을 제거한다. 이 외에도 콘솔로그를 제거하는 옵션도 있다. 배포 버전에는 굳이 console.log()가 필요하지 않기 때문이다.

플러그인 설치

npm i -D css-minimizer-webpack-plugin terser-webpack-plugin

웹팩 설정 추가

const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const TerserPlugin = require("terser-webpack-plugin");

module.exports = { 
  optimization: {
    minimize: true,
    minimizer: [new TerserPlugin(), new CssMinimizerPlugin()],
  },
}

terser-webpack-plugin은 자바스크립트를 minify하는데 사용되는 플러그인인데, 커스터마이즈된 minimize를 하려면 저렇게 다시 직접 설치해서 입력해주어야 한다.

css-minimizer-webpack-plugin을 사용하기 위함이다.

웹팩 설정 변경 - production에서만 minimize

const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const TerserPlugin = require("terser-webpack-plugin");

module.exports = { 
  optimization: {
    minimize: mode === "production",
    minimizer: [new TerserPlugin(), new CssMinimizerPlugin()],
  },
}

위와 같이 간단하게 mode를 이용해 설정해주면 된다.

콘솔로그 제거 설정 해보기

  optimization: {
    minimize: mode === "production",
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          compress: {
            drop_console: true, // 콘솔 로그를 제거
          },
        },
      }),
      new CssMinimizerPlugin(),
    ],
  },

위와 같이 TerserPlugin에 옵션을 주면 콘솔로그를 제거할 수 있다.

결과 살펴보기

css가 압축되었다.

실행 결과에서 console.log도 물론 뜨지 않는다.

코드 스플리팅을 통한 최적화

최적화의 필요성

module.exports = {
  mode,
  entry: {
    app: "./src/app.js",
    math: "./src/math.js",
  },
  ...

만일 웹팩의 설정파일에서 위와 같이 entry가 2개이고, 두개에서 동일한 패키지를 사용한다고 가정해보자. 이를테면 현재 두 파일에서 공통으로 axios 패키지를 사용한다고 가정해보자.

최적화를 하지 않은채로 빌드했을 때

app.js

result.js

axios라는 동일한 코드들이 중복되고 있다.

optimization.splitChunks 사용하기

웹팩은 기본적으로 아래와 같은 조건 하에 chunks를 나눈다.

  • 새 chunk가 공유될 수 있거나 모듈이 node_modules 폴더에 있는 경우
  • 새 chunk가 20kb보다 클 경우
  • 요청에 의해 chunk가 로드될 때, 최대 병렬 요청의 수가 30개 이하일 경우
  • 초기 페이지 로드 시, 최대 병렬 요청 수가 30개 이하일 경우

마지막 2개의 조건을 만족하려하면, 청크의 크기는 더 큰것이 선호된다.

기본 설정은 아래와 같다.

module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks: 'async',
      minSize: 20000,
      minRemainingSize: 0,
      minChunks: 1,
      maxAsyncRequests: 30,
      maxInitialRequests: 30,
      enforceSizeThreshold: 50000,
      cacheGroups: {
        defaultVendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
          reuseExistingChunk: true,
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        },
      },
    },
  },
};

내가 이번에 해준 설정은 아래와 같다.

  optimization: {
    minimize: mode === "production",
    minimizer: [
      new CssMinimizerPlugin(),
    ],
    splitChunks: {
      chunks: "all",
    },
  },

그냥 단순히 optimization.splitChunks.chunks: "all"을 주었을 뿐이다. 나머지는 기본 설정으로 적용됐을 것이다.

코드 확인 결과

기존에 axioscreateInstance()하던 부분들은 사라지고, 단순히 require()로 불러오는 부분만 남아있다.

파일 확인 결과

그리고 배포 디렉토리에 정체를 알 수 없는 669.js850.js가 있는데, 해당 파일 내부에 Axios 코드가 들어있다.

용량도 669.js850.js가 가장 크다.

다이나믹 임포트 사용해보기

result.js 파일을 따로 번들링하여 다이나믹하게 임포트해보자.

app.js 코드 변경

import "./app.css";

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

console.log("hello world");

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

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

기존의 import 구문을 주석처리하고 아래쪽에 import() 메소드를 통해 "./result.js" 파일을 가져왔다. 그리고 파일명 문자열 앞에 /* webpackChunkName: "result" */ 주석도 의미가 있다. 반드시 적어주어야 한다.

webpack.config.js 수정

위와 같이 코드를 변경하고, 기존에 optimizer.splitChunks 부분은 주석처리해두었다.

적용 결과

app.js 안에 붙어있던, result.js 파일이 분리되었다.

externals - 번들하지 않아도 되는 것들

axios 같은 서드파티 라이브러리는 패키지로 제공될 때 이미 빌드 과정을 거쳤기 때문에, 빌드 프로세스가 필요 없다. 웹팩 설정 중 externals가 이런 기능을 제공한다.

// webpack.config.js
module.exports = {
  externals: {
    axios: 'axios'
  }
}

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

웹팩 코드 변경 - externals.axios

...
  externals: {
    axios: "axios",
  },
  optimization: {
    minimize: mode === "production",
    minimizer: [
      new CssMinimizerPlugin(),
    ],
  },
};

위와 같이 externals.axiosaxios라는 모듈을 쓸 때, 전역변수 axios가 있는 것처럼 빌드하라는 뜻이다. axios는 우리가 설치한 것이기 때문에 node_modules 내부에 존재한다.

웹팩 코드 변경 - CopyPlugin

  plugins: [
    new CopyPlugin({
      patterns: [
        {
          from: "./node_modules/axios/dist/axios.min.js",
          to: "./axios.min.js",
        },
      ],
    }),
  ],

위와 같이 node_modules에 있는 패키지를 dist/axios.min.js라는 파일로 불러온다고 적어주면 된다.

배포 결과

axios.min.js가 따로 빠졌다. 이렇게 하면 웹팩에서 쓸데없는 빌드 시간도 줄일 수 있다.

profile
풀스택 웹개발자로 일하고 있는 Jake Seo입니다. 주로 Jake Seo라는 닉네임을 많이 씁니다. 프론트엔드: Javascript, React 백엔드: Spring Framework에 관심이 있습니다.

0개의 댓글