Front-end/개발환경

최적화 - 웹팩 심화편 | 김정환

AGAL 2020. 6. 17. 22:37
반응형

[출처 : https://jeonghwan-kim.github.io]

4. 최적화

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

 

4-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 :

const mode = process.env.NODE_ENV || 'development'; // 기본값을 development로 설정

module.exports = {
  mode,
}

* key와 value의 값이 같으면 key만 입력해도 된다. (ES6 문법)

 

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

package.json :

{
  "scripts": {
    "start": "webpack-dev-server --progress",
    "build": "NODE_ENV=production webpack --progress"
  }
}

* "'NODE_ENV'은(는) 내부 또는 외부 명령, 실행할 수 있는 프로그램, 또는 배치 파일이 아닙니다." 에러 발생 시 

$ npm install -g win-node-env node-env를 설치

 

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

 

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

$ npm run build

 

왼쪽에 development로 설정해서 빌드한 결과물과 비교해 보면 오른쪽에 production으로 빌드한 결과물의 확연한 차이를 볼 수 있다.

 

4-2. optimazation 속성으로 최적화

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

HtmlWebpackPlugin이 html 파일을 압축한것 처럼 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(),
    ] : [],
  },
}

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

 

빌드하뒤 확인하면 css 파일도 압축되었다.

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

 

이 플러그인을 설치한 뒤,

$ npm i -D terser-webpack-plugin

 

optionmization.minimizer 배열에 추가한다.

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",
    form: "./src/form.js",
  }
}

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

 

dist 폴더 내의 파일 목록을 보면,

$ ls -lh dist 

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

 

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

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

 

SplitChunksPlugin은 코드를 분리할때 중복을 예방하는 플러그인이다.

optization.splitChucks 속성을 설정하는 방식이다.

webpack.config.js :

module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
    },
  },
}

다시 빌드해보자.

$ npm run build

main.js, form.js외에도 vendors~form~main.js 파일도 생겼다. 마지막 파일은 두 엔트리의 중복 코드를 담은 파일이다. axios로 검색하면 main.js와 form.js에서는 없고 vendors~form~main.js에만 있다.

 

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

 

기존 컨트롤러 코드를 보면 이렇다.

/src/app.js :

import form from './form';

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

import/from으로 컨트롤러 모듈을 가져와서 사용했다.

 

이를 동적으로 임포트하려면 다음처럼 변경한다.

//import form from "./form";

document.addEventListener("DOMContentLoaded", async () => {
  import(/* webpackChunkName: "form" */ "./form.js").then(m => {
    const form = m.default;
    formEl = document.createElement("div");
    formEl.innerHTML = form.render();
    document.body.appendChild(formEl);
  })
})

import() 함수로 가져올 컨트롤러 모듈 경로를 전달하는데 주석으로 webpackHunkName: “form”를 전달했다. 이것은 웹펙이 이 파일을 처리할때 청크로 분리하는데 그 청그 이름을 설정한 것이다.

 

그리고 나서 프라미스를 상수 form에 담아서 모듈을 가져와 사용하였다.

 

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

webpack.config.js : 

module.exports = {
  entry: {
    main: "./src/app.js",
    // controller: "./src/controller.js",  // 분리한 엔트리 포인트 제거
  },  
  optimization: {
    // splitChunks: {  // SplitChunksPlugin 옵션 제거
    //  chunks: 'all',
    // },
  },
}

 

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

다이나믹 임포트로 모듈을 가져오면 단일 엔트리를 유지하면서 코드를 분리할 수 있다.

엔트리를 분리하지 않아도 form와 app의 중복코드를 vendors~form.js 파일로 분리한다.

 

4-4. externals

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

 

웹팩 설정중 externals가 바로 이러한 기능을 제공한다.

webpack.config.js :

module.exports = {
  externals: {
    axios: 'axios',
  },
}

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

 

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

 

파일을 복사하는 CopyWebpackPlugin을 설치한다.

$ npm i -D copy-webpack-plugin

 

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

webpack.config.js :

const CopyPlugin = require('copy-webpack-plugin');

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

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

src/index.html :

 <script type="text/javascript" src="axios.min.js"></script>
</body>
</html>

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

 

다시 빌드해 보면,

axios는 빌드하지 않고 복사만 한다. form와 main이 분리되었다. 이전에는 공통의 코드인 axios가 vender~.js로 분리되었는데 지금은 파일조차 없다. 만약 써드파티 라이브러리 외에 공통의 코드가 있다면 이 파일로 분리되었을 것이다.

 

이렇게 써드파티 라이브러리를 externals로 분리하면 용량이 감소뿐만 아니라 빌드시간도 줄어들고 덩달아 개발 환경도 가벼워질 수 있다.


SUMMARY

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

 

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

 

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

 


 

TODO 1. 

환경변수 NODE_ENV에 따라 development나 production 값을 설정하세요 

더보기

webpack.config.js : 

const mode = process.env.NODE_ENV || "development";

module.exports = {
  mode,
}

TODO 2.

최적화 설정을 구성하세요 

더보기

$ npm i optimize-css-assets-webpack-plugin

$ npm i terser-webpack-plugin

 

webpack.config.js :

const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  optimization: {
    minimizer: mode === 'production'
    ? [
      new OptimizeCssAssetsPlugin(),
      new TerserPlugin({
        terserOptions: {
          compress: {
       	  	drop_console: true, // console.log()를 제거한다.
          },
        },
      }),
    ]
    : [],
  },
}

pakage.json :

{
  "scripts": {
    "build": "NODE_ENV=production webpack --progress",
  },
}

TODO 3.

외부 라이브러리 axios를 로딩할수 있도록 웹팩에서 파일을 복사하세요

더보기

$ npm i copy-webpack-plugin

 

webpack.config.js :

const CopyPlugin = require('copy-webpack-plugin');

module.exports = {
  externals: {
    axios: "axios",
  },
  plugins: [
    new CopyPlugin({
      patterns: [
        {
          from: './node_modules/axios/dist/axios.min.js',
          to: './axios.min.js'
        }
      ]
    })
  ]
}
반응형