avatar
Published on

프론트엔드 사이즈 줄이기

Author
  • avatar
    Name
    yceffort

이 글을 대충 번역했습니다.

Table of Contents

webpack 4버전 이상의 경우 프로덕션 모드를 사용하기

webpack 4버전 부터 mode라고 하는 플래그가 추가되었다. development production을 설정해두는데, 이는 webpack에 현재 어떤 버전으로 빌드하려고 하는지 알려준다.

// webpack.config.js
module.exports = {
  mode: 'production',
};

production모드는 프로덕션 환경에서 실제 앱을 빌드할 때 사용한다. 이 모드를 사용하면 웹팩은 코드 최소화, dev 라이브러리 삭제, 등등의 최적화를 진행하게 된다.

minification 을 켜두기

minification이란 코드에서 띄어쓰기를 제거하거나, 변수명을 짧게 하는 등으로 코드의 양 자체를 줄이는 것이다.

function map(array, iteratee) {
  let index = -1
  const length = array == null ? 0 : array.length
  const result = new Array(length)

  while (++index < length) {
    result[index] = iteratee(array[index], index, array)
  }
  return result
}
// minified
function map(n, r) {
  let t = -1
  for (const a = null == n ? 0 : n.length, l = Array(a); ++t < a; )
    l[t] = r(n[t], t, n)
  return l
}

번들 수준의 minification

번들 수준의 minification은 컴파일 이후에 전체 번들을 압축하는 것이다.

// 1. 코드가 이렇게 있다
// comments.js
import './comments.css'
export function render(data, target) {
  console.log('Rendered!')
}
// 2. 웹팩이 대충 이런 모습으로 컴파일 한다.
// bundle.js (part of)
'use strict'
Object.defineProperty(__webpack_exports__, '__esModule', { value: true })
/* harmony export (immutable) */ __webpack_exports__['render'] = render
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__comments_css__ =
  __webpack_require__(1)
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__comments_css_js___default =
  __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0__comments_css__)

function render(data, target) {
  console.log('Rendered!')
}
// 3. 압축한다.
// minified bundle.js (part of)
'use strict'
function t(e, n) {
  console.log('Rendered!')
}
Object.defineProperty(n, '__esModule', { value: !0 }), (n.render = t)
var o = r(1)
r.n(o)
  • webpack4에서는 번들 수준 최소화가 프로덕션 모드 또는 명시되지 않은 모드에서 자동으로 진행된다.내부적으로는 Uglify minifier를 사용한다. 만약 최소화를 하고 싶지 않다면, development 모드를 키거나 optimization.minimize에 false를 주면 된다.
  • webpack3에서는 Uglify minifier를 직접 사용해야 한다. 해당 플러그인은 webpack에서 자동으로 딸려 오므로, 설정에 아래 코드를 추가하면 된다.
// webpack.config.js
const webpack = require('webpack')

module.exports = {
  plugins: [new webpack.optimize.UglifyJsPlugin()],
}

loader-specific 옵션

코드를 줄이는 두번째 방법은 loader-specific 옵션을 사용하는 것이다. loader 이 옵션을 사용하면, minifier가 줄이지 못하는 코드를 줄여줄 수 있다. 만약 css를 위해 css-loader를 사용하고 있다면, 파일은 아래와 같이 문자열로 컴파일 된다.

/* comments.css */
.comment {
  color: black;
}
// minified bundle.js (part of)
;(exports = module.exports = __webpack_require__(1)()),
  exports.push([module.i, '.comment {\r\n  color: black;\r\n}', ''])

minifier는 코드가 문자열이기 때문에 더 이상 최소화 할 수 없다. 이를 최소화 하기 위해서는, 아래와 같이 옵션을 추가하면 된다.

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          { loader: 'css-loader', options: { minimize: true } },
        ],
      },
    ],
  },
}

참고할 만한 것들

NODE_ENV=production 명시하기

webpack4에서 production 모드를 사용하고 있다면, 이미 해당 옵션은 켜져 있을 것입니다. 아래의 팁은 webpack3와 관련된 것입니다.

NODE_ENV의 값을 production로 해두면, 코드의 크기를 줄일 수 있다.

라이브러리들은 환경 변수인 NODE_ENV값을 감지하여 어떻게 동작할지를 판단한다. 예를 들어, vue.js의 경우 production으로 값이 주어져 있지 않다면, 아래와 같은 메시지를 보여준다.

// vue/dist/vue.runtime.esm.js
// …
if (process.env.NODE_ENV !== 'production') {
  warn('props must be strings when using array syntax.')
}
// …

리액트도 비슷하게 동작한다. development에서 빌드시 다음과 같은 경고문을 낼 수 있다.

// react/index.js
if (process.env.NODE_ENV === 'production') {
  module.exports = require('./cjs/react.production.min.js')
} else {
  module.exports = require('./cjs/react.development.js')
}

// react/cjs/react.development.js
// …
warning$3(
  componentClass.getDefaultProps.isReactClassApproved,
  'getDefaultProps is only used on classic React.createClass ' +
    'definitions. Use a static property named `defaultProps` instead.',
)
// …

이런 체크는 production에서는 불필요하여 코드의 사이즈를 늘릴 뿐이다. webpack4에서는 아래와 같이 하면된다.

module.exports = {
  optimization: {
    nodeEnv: 'production',
    minimize: true,

이 코드는 process.env.NODE_ENV를 모두 production으로 바꿔버리는 효과를 가지고 있다. 또한 minifer는 process.env.NODE_ENV !== 'production' 코드를 모두 날려버린다. 어쨌든 false라 절대 탈 수 없는 코드 이기 때문이다.

ES Module 사용하기

프론트엔드 사이즈를 줄이는 또한가지 방법은 ES modules을 사용하는 것이다. ES modules을 사용해야 웹팩에서 트리쉐이킹을 할 수 있다. 트리쉐이킹이란 번들러가 전체 디펜던시 트리를 싹 뒤져서, 사용하지 않는 부분을 삭제해 버리는 것이다. 따라서 ESModule syntax를 사용해야 트리쉐이킹이 가능하다.

// comments.js
export const render = () => {
  return 'Rendered!'
}
export const commentRestEndpoint = '/rest/comments'

// index.js
import { render } from './comments.js'
render()

웹팩이 commentRestEndpoint는 안쓰는 것으로 판단해 따로 export하지 않는다.

// bundle.js (part that corresponds to comments.js)
;(function (module, __webpack_exports__, __webpack_require__) {
  'use strict'
  const render = () => {
    return 'Rendered!'
  }
  /* harmony export (immutable) */ __webpack_exports__['a'] = render

  const commentRestEndpoint = '/rest/comments'
  /* unused harmony export commentRestEndpoint */
})

그리고 사용하지 않는 코드를 minifier가 날려버린다.

// bundle.js (part that corresponds to comments.js)
;(function (n, e) {
  'use strict'
  var r = function () {
    return 'Rendered!'
  }
  e.b = r
})

웹팩에서 minifier가 없다면 트리쉐이킹이 동작하지 않습니다. 사용하지 않는 코드를 export하지 않는 것 (트리쉐이킹)과 사용하지 않는 코드를 지우는 것(minifier)은 한쌍이기 때문입니다.

ESModules을 CommonJS 로 컴파일 하지 말기를 바랍니다.

이미지 크기 줄이기

이미지는 페이지 크기의 절반 이상을 차지한다. 렌더링을 블락하는 자바스크립트 만큼은 치명적이지 않지만, 어쨌든 전체 네트워크 통신에서 많은 부분을 잡아먹는 것은 사실이다. url-loader svg-url-loader image-webpack-loader 등으로 최적화 할 필요가 있다.

url-loader는 작은 정적 파일을 앱에 삽입한다. 별도의 설정이 없을시 파일을 전달 받았다면, 해당 파일을 번들링하고 번들링된 주소를 리턴한다. 그러나 limit가 존재한다면, 해당 이미지를 더 작은 Base64 데이터로 인코딩하여 바꿔치기한다.

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.(jpe?g|png|gif)$/,
        loader: 'url-loader',
        options: {
          // Inline files smaller than 10 kB (10240 bytes)
          limit: 10 * 1024,
        },
      },
    ],
  },
}
// index.js
import imageUrl from './image.png'
// → If image.png is smaller than 10 kB, `imageUrl` will include
// the encoded image: 'data:image/png;base64,iVBORw0KGg…'
// → If image.png is larger than 10 kB, the loader will create a new file,
// and `imageUrl` will include its url: `/2fcd56a1920be.png`

svg-url-loaderurl-loader와 비슷하지만, 파일을 URL encoding한다는 점이 다르다. 이는 SVG파일에 유용한데, 그 이유는 SVG는 단순히 텍스트 이므로, 인코딩시 더 사이즈가 줄어들기 때문이다.

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.svg$/,
        loader: 'svg-url-loader',
        options: {
          // Inline files smaller than 10 kB (10240 bytes)
          limit: 10 * 1024,
          // Remove the quotes from the url
          // (they’re unnecessary in most cases)
          noquotes: true,
        },
      },
    ],
  },
}

image-webpack-loader는 이미지 자체를 압축해준다. JPG, PNG, GIF, SVG를 지원한다. 이 옵션은 앞선 두 예시 처럼 따로 임베딩 해주지는 않는다. 함께 사용하기 위해서는, 아래처럼 하면 된다.

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.(jpe?g|png|gif|svg)$/,
        loader: 'image-webpack-loader',
        // This will apply the loader before the other ones
        enforce: 'pre',
      },
    ],
  },
}

디펜던시 최적화하기

절반이상의 자바스크립트 번들 사이즈는 디펜던시에서 오며, 그 중 일부분은 불필요할 수 있다.

Lodash의 경우 번들 시에 72kb를 차지하지만, 몇가지 메소드를 사용하지 않는다면 크기를 줄일 수 있다. Moment.js는 무려 223KB를 차지하는데, 이는 평균 페이지당 자바스크립트 사이즈를 감안했을때 452KB 엄청나게 큰 비중을 차지한다. 하지만 이중 170kb는 Locale관련 내용이다. 만약 Moment.js를 가지고 다양한 언어를 지원할 필요가 없다면, 이런 파일은 크기만 차지하게 된다.

이런 패키지들은 쉽게 최적화가 가능하다. 여기를 참고해보자.

ES Module을 위한 module concatenation 켜두기 (aka 스코프 호이스팅)

웹팩에서 번들을 만들때, 각 모듈을 함수로 래핑한다.

// index.js
import { render } from './comments.js'
render()

// comments.js
export function render(data, target) {
  console.log('Rendered!')
}
// bundle.js (part  of)
/* 0 */
;(function (module, __webpack_exports__, __webpack_require__) {
  'use strict'
  Object.defineProperty(__webpack_exports__, '__esModule', { value: true })
  var __WEBPACK_IMPORTED_MODULE_0__comments_js__ = __webpack_require__(1)
  Object(__WEBPACK_IMPORTED_MODULE_0__comments_js__['a' /* render */])()
}),
  /* 1 */
  function (module, __webpack_exports__, __webpack_require__) {
    'use strict'
    __webpack_exports__['a'] = render
    function render(data, target) {
      console.log('Rendered!')
    }
  }

과거 이러한 방식은 CommonJS나 AMD 모듈로 부터 분리시키기 위해 필요했다. 그러나 이러한 방식은 각 모듈의 사이즈를 키우고 퍼포먼스를 저하시킨다.

웹팩3 부터 module concatenation를 활용한 번들링이 가능해졌다. concatenation 모듈이 하는 것을 살펴보자.

// index.js
import { render } from './comments.js'
render()

// comments.js
export function render(data, target) {
  console.log('Rendered!')
}
// Unlike the previous snippet, this bundle has only one module
// which includes the code from both files

// bundle.js (part of; compiled with ModuleConcatenationPlugin)
/* 0 */
;(function (module, __webpack_exports__, __webpack_require__) {
  'use strict'
  Object.defineProperty(__webpack_exports__, '__esModule', { value: true })

  // CONCATENATED MODULE: ./comments.js
  function render(data, target) {
    console.log('Rendered!')
  }

  // CONCATENATED MODULE: ./index.js
  render()
})

차이가 보이는가? 플레인 번들에서는, 모듈 0이 모듈 1에 있는 render를 필요로 했다. module concatenation을 활용하면, require 대신 1번 모듈을 바로 호출하는 것을 볼 수 있다. 번들이 모듈의 수를 줄여주었고, 마찬가지로 오버헤드도 줄어들었다.

웹팩4

// webpack.config.js (for webpack 4)
module.exports = {
  optimization: {
    concatenateModules: true,
  },
}

웹팩3

// webpack.config.js (for webpack 3)
const webpack = require('webpack')

module.exports = {
  plugins: [new webpack.optimize.ModuleConcatenationPlugin()],
}

웹팩 코드와 웹팩으로 번들링 되지 않은 코드를 같이 슨다면 externals를 사용하라

만약 두개의 코드가 같은 디펜던시를 가지고 있다면, 이를 공유해서 여러번 같은 코드를 다운로드하는 것을 막을 수 있다.

window에 있을 경우

// webpack.config.js
module.exports = {
  externals: {
    react: 'React',
    'react-dom': 'ReactDOM',
  },
}

만약 이렇게 설정해둔다면, 웹팩은 reactreact-dom을 번들링하지 않는다. 대신 아래와 비슷한 일을 한다.

// bundle.js (part of)
;(function (module, exports) {
  // A module that exports `window.React`. Without `externals`,
  // this module would include the whole React bundle
  module.exports = React
}),
  function (module, exports) {
    // A module that exports `window.ReactDOM`. Without `externals`,
    // this module would include the whole ReactDOM bundle
    module.exports = ReactDOM
  }

AMD 패키지의 경우

// webpack.config.js
module.exports = {
  output: { libraryTarget: 'amd' },

  externals: {
    react: { amd: '/libraries/react.min.js' },
    'react-dom': { amd: '/libraries/react-dom.min.js' },
  },
}

웹팩은 위 라이브러리를 주소로 번들링 할 것이다.

// bundle.js (beginning)
define(["/libraries/react.min.js", "/libraries/react-dom.min.js"], function () {});

요약

  • webpack4 이상의 버전에서는 production모드를 활성화 시켜라
  • 번들 수준의 minifier와 loader 옵션을 활용하여 코드의 크기를 줄여라
  • 개발 단계에서만 필요한 코드는 NODE_ENV production으로 관리하라
  • 트리쉐이킹을 위해 ESModule을 사용하라
  • 이미지를 압축하라
  • 디펜던시 라이브러리를 최적화 하라
  • module concatenation을 켜둬라
  • 필요하다면 externals를 사용하라