프런트엔드 크기 줄이기

webpack을 사용하여 앱을 최대한 작게 만드는 방법

애플리케이션을 최적화할 때 가장 먼저 해야 할 일은 애플리케이션을 최대한 작게 만드는 것입니다. webpack으로 추가하는 방법은 다음과 같습니다.

프로덕션 모드 사용 (webpack 4만 해당)

Webpack 4에서는 새로운 mode 플래그를 도입했습니다. 이 플래그를 'development' 또는 'production'로 설정하여 특정 환경에 맞게 애플리케이션을 빌드한다는 것을 webpack에 알릴 수 있습니다.

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

프로덕션용 앱을 빌드할 때는 production 모드를 사용 설정해야 합니다. 이렇게 하면 축소, 라이브러리에서 개발 전용 코드 삭제 의 최적화가 webpack에 적용됩니다.

추가 자료

압축 사용 설정

압축은 추가 공백을 삭제하고 변수 이름을 줄이는 등의 방식으로 코드를 압축하는 것입니다. 다음과 같습니다.

// Original code
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 code
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}

Webpack은 코드를 축소하는 두 가지 방법, 즉 번들 수준 압축로더별 옵션을 지원합니다. 동시에 사용해야 합니다.

번들 수준 압축

번들 수준 축소는 컴파일 후 전체 번들을 압축합니다. 검색 테마의 작동 원리는 다음과 같습니다.

  1. 코드는 다음과 같이 작성합니다.

    // comments.js
    import './comments.css';
    export function render(data, target) {
      console.log('Rendered!');
    }
    
  2. Webpack은 다음과 같이 컴파일합니다.

    // 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)
    

webpack 4에서는 프로덕션 모드에서와 관계없이 번들 수준 축소가 자동으로 사용 설정됩니다. 내부적으로 UglifyJS 축소기를 사용합니다. 축소를 사용 중지해야 하는 경우 개발 모드를 사용하거나 falseoptimization.minimize 옵션에 전달하면 됩니다.

webpack 3에서는 UglifyJS 플러그인을 직접 사용해야 합니다. 플러그인은 webpack과 함께 제공됩니다. 사용 설정하려면 구성의 plugins 섹션에 추가합니다.

// webpack.config.js
const webpack = require('webpack');

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

로더 관련 옵션

코드를 축소하는 두 번째 방법은 로더별 옵션 (로더란 무엇인가요)입니다. 로더 옵션을 사용하면 축소기가 축소할 수 없는 항목을 압축할 수 있습니다. 예를 들어 css-loader를 사용하여 CSS 파일을 가져오면 파일이 문자열로 컴파일됩니다.

/* 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}",""]);

이 코드는 문자열이므로 축소기가 이 코드를 압축할 수 없습니다. 파일 콘텐츠를 축소하려면 로더가 다음을 수행하도록 구성해야 합니다.

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

추가 자료

NODE_ENV=production 지정

프런트엔드 크기를 줄이는 또 다른 방법은 코드의 NODE_ENV 환경 변수production 값으로 설정하는 것입니다.

라이브러리는 NODE_ENV 변수를 읽고 개발 모드와 프로덕션 모드 중 어느 모드에서 작동해야 하는지 감지합니다. 일부 라이브러리는 이 변수에 따라 다르게 동작합니다. 예를 들어 NODE_ENVproduction로 설정되어 있지 않으면 Vue.js는 추가 검사를 실행하고 경고를 출력합니다.

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

React는 유사하게 작동합니다. 다음 경고가 포함된 개발 빌드를 로드합니다.

// 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.'
);
// …

이러한 검사와 경고는 일반적으로 프로덕션에서 필요하지 않지만 코드에 남아 라이브러리 크기를 늘립니다. webpack 4에서 optimization.nodeEnv: 'production' 옵션을 추가하여 삭제합니다.

// webpack.config.js (for webpack 4)
module.exports = {
  optimization: {
    nodeEnv: 'production',
    minimize: true,
  },
};

웹팩 3에서는 대신 DefinePlugin를 사용합니다.

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

module.exports = {
  plugins: [
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': '"production"'
    }),
    new webpack.optimize.UglifyJsPlugin()
  ]
};

optimization.nodeEnv 옵션과 DefinePlugin 모두 동일한 방식으로 작동합니다. 즉, 모든 process.env.NODE_ENV 항목을 지정된 값으로 바꿉니다. 위의 구성을 사용하면 다음과 같습니다.

  1. Webpack이 모든 process.env.NODE_ENV 항목을 "production"로 바꿉니다.

    // vue/dist/vue.runtime.esm.js
    if (typeof val === 'string') {
      name = camelize(val);
      res[name] = { type: null };
    } else if (process.env.NODE_ENV !== 'production') {
      warn('props must be strings when using array syntax.');
    }
    

    // vue/dist/vue.runtime.esm.js
    if (typeof val === 'string') {
      name = camelize(val);
      res[name] = { type: null };
    } else if ("production" !== 'production') {
      warn('props must be strings when using array syntax.');
    }
    
  2. 그러면 축소기는 이러한 모든 if 브랜치를 삭제합니다. "production" !== 'production'이 항상 false이고 플러그인은 이러한 브랜치 내의 코드가 실행되지 않는다고 인식하기 때문입니다.

    // vue/dist/vue.runtime.esm.js
    if (typeof val === 'string') {
      name = camelize(val);
      res[name] = { type: null };
    } else if ("production" !== 'production') {
      warn('props must be strings when using array syntax.');
    }
    

    // vue/dist/vue.runtime.esm.js (without minification)
    if (typeof val === 'string') {
      name = camelize(val);
      res[name] = { type: null };
    }
    

추가 자료

ES 모듈 사용

프런트엔드 크기를 줄이는 또 다른 방법은 ES 모듈을 사용하는 것입니다.

ES 모듈을 사용하면 webpack이 트리 쉐이킹을 수행할 수 있게 됩니다. 트리 쉐이킹은 번들러가 전체 종속 항목 트리를 순회하고 사용되는 종속 항목을 확인하고 사용되지 않는 종속 항목을 삭제하는 것입니다. 따라서 ES 모듈 구문을 사용하면 webpack에서 사용되지 않는 코드를 제거할 수 있습니다.

  1. 여러 내보내기가 포함된 파일을 작성하지만 앱에서는 둘 중 하나만 사용합니다.

    // comments.js
    export const render = () => { return 'Rendered!'; };
    export const commentRestEndpoint = '/rest/comments';
    
    // index.js
    import { render } from './comments.js';
    render();
    
  2. Webpack은 commentRestEndpoint가 사용되지 않으며 번들에 별도의 내보내기 지점을 생성하지 않음을 인식합니다.

    // 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 */
    })
    
  3. 축소기는 사용되지 않는 변수를 삭제합니다.

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

이는 라이브러리가 ES 모듈로 작성된 경우에도 작동합니다.

하지만 webpack에 내장된 최소화기 (UglifyJsPlugin)를 정확하게 사용할 필요는 없습니다. 데드 코드 삭제를 지원하는 축소기(예: Babel Minify 플러그인 또는 Google 클로저 컴파일러 플러그인)를 사용하면 됩니다.

추가 자료

이미지 최적화

이미지가 페이지 크기의 절반 이상을 차지합니다. 이는 JavaScript만큼 중요하지는 않지만 (예: 렌더링을 차단하지 않음) 여전히 대역폭의 상당 부분을 차지합니다. url-loader, svg-url-loader, image-webpack-loader를 사용하여 webpack에서 최적화하세요.

url-loader는 작은 정적 파일을 앱에 인라인합니다. 구성 없이는 전달된 파일을 가져와 컴파일된 번들 옆에 두고 해당 파일의 URL을 반환합니다. 하지만 limit 옵션을 지정하면 이 한도보다 작은 파일을 Base64 데이터 URL로 인코딩하고 이 URL을 반환합니다. 이렇게 하면 이미지가 자바스크립트 코드에 인라인으로 삽입되고 HTTP 요청이 저장됩니다.

// 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와 동일하게 작동합니다. 단, Base64 인코딩이 아닌 URL 인코딩으로 파일을 인코딩한다는 점이 다릅니다. 이는 SVG 이미지에 유용합니다. SVG 파일은 일반 텍스트이기 때문에 인코딩이 크기 측면에서 더 효과적입니다.

module.exports = {
  module: {
    rules: [
      {
        test: /\.svg$/,
        loader: "svg-url-loader",
        options: {
          limit: 10 * 1024,
          noquotes: true
        }
      }
    ]
  }
};

image-webpack-loader는 통과하는 이미지를 압축합니다. JPG, PNG, GIF, SVG 이미지를 지원하므로 이러한 모든 유형에 사용할 것입니다.

이 로더는 앱에 이미지를 삽입하지 않으므로 url-loadersvg-url-loader와 페어링하여 작동해야 합니다. 두 규칙 (JPG/PNG/GIF 이미지용 하나, SVG 규칙용 하나)에 복사하여 붙여넣는 것을 방지하기 위해 이 로더를 enforce: 'pre'를 사용하는 별도의 규칙으로 포함합니다.

// 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'
      }
    ]
  }
};

로더의 기본 설정은 이미 준비가 되어 있지만 추가로 구성하려면 플러그인 옵션을 참고하세요. 지정할 옵션을 선택하려면 애디 오스마니의 훌륭한 이미지 최적화 가이드를 참고하세요.

추가 자료

종속 항목 최적화

평균 JavaScript 크기의 절반 이상이 종속 항목에서 비롯되며, 그 중 일부는 불필요할 수도 있습니다.

예를 들어 Lodash (v4.17.4 기준)는 번들에 축소된 코드 72KB를 추가합니다. 그러나 메서드 중 20개만 사용하는 경우 약 65KB의 축소된 코드는 아무 작업도 실행하지 않습니다.

또 다른 예는 Moment.js입니다. 2.19.1 버전에서는 축소된 코드가 223KB로 매우 큽니다. 이는 페이지의 평균 자바스크립트 크기가 2017년 10월 기준 452KB였습니다. 하지만 이 크기의 170KB는 현지화 파일입니다. 여러 언어로 Moment.js를 사용하지 않으면 이러한 파일이 목적 없이 번들을 팽창시킵니다.

이러한 모든 종속 항목은 쉽게 최적화할 수 있습니다. GitHub 저장소에서 최적화 방법을 모았습니다. 확인해 보세요.

ES 모듈의 모듈 연결 사용 설정 (범위 호이스팅이라고도 함)

번들을 빌드할 때 webpack은 각 모듈을 함수로 래핑합니다.

// 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 모듈을 서로 격리하기 위해 이 설정이 필요했습니다. 그러나 이로 인해 각 모듈의 크기와 성능 오버헤드가 추가되었습니다.

Webpack 2에는 CommonJS 및 AMD 모듈과 달리 ES 모듈을 위한 지원이 도입되었습니다. 이 모듈은 각 모듈을 함수로 래핑하지 않고 번들로 제공할 수 있습니다. webpack 3에서는 모듈 연결을 사용하여 이러한 번들을 만들 수 있었습니다. 모듈 연결은 다음과 같은 역할을 합니다.

// 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가 요구되었습니다. 모듈 연결을 사용하면 require가 간단히 필수 함수로 대체되고 모듈 1은 삭제됩니다. 번들은 모듈이 더 적고 모듈 오버헤드도 적습니다.

이 동작을 사용 설정하려면 webpack 4에서 optimization.concatenateModules 옵션을 사용 설정합니다.

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

웹팩 3에서는 다음과 같이 ModuleConcatenationPlugin를 사용합니다.

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

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

추가 자료

webpack 코드와 웹팩이 아닌 코드가 모두 있는 경우 externals를 사용합니다.

대규모 프로젝트에 일부 코드는 webpack으로 컴파일되어 있고, 그렇지 않은 대규모 프로젝트가 있을 수 있습니다. 동영상 호스팅 사이트와 마찬가지로 플레이어 위젯이 webpack으로 빌드될 수 있지만 주변 페이지는 다음과 같지 않을 수 있습니다.

동영상 호스팅 사이트의 스크린샷
(완전한 임의의 동영상 호스팅 사이트)

두 코드에 공통 종속 항목이 있는 경우 코드를 여러 번 다운로드하지 않도록 공유할 수 있습니다. 이 작업은 webpack의 externals 옵션을 사용하면 됩니다. 이 옵션은 모듈을 변수 또는 기타 외부 가져오기로 바꿉니다.

window에서 종속 항목을 사용할 수 있는지 여부

webpack이 아닌 코드가 window의 변수로 사용할 수 있는 종속 항목에 의존하는 경우 다음과 같이 변수 이름에 별칭 종속 항목 이름을 추가합니다.

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

이 구성을 사용하면 webpack이 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이 아닌 코드가 종속 항목을 window에 노출하지 않으면 작업이 더 복잡합니다. 하지만 웹팩이 아닌 코드가 이러한 종속 항목을 AMD 패키지로 사용하는 경우에는 동일한 코드를 두 번 로드하지 않아도 됩니다.

이를 위해 다음과 같이 webpack 코드를 AMD 번들로 컴파일하고 별칭 모듈을 라이브러리 URL에 컴파일합니다.

// webpack.config.js
module.exports = {
  output: {
    libraryTarget: 'amd'
  },
  externals: {
    'react': {
      amd: '/libraries/react.min.js'
    },
    'react-dom': {
      amd: '/libraries/react-dom.min.js'
    }
  }
};

Webpack은 번들을 define()로 래핑하고 다음 URL에 종속되도록 합니다.

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

웹팩이 아닌 코드가 동일한 URL을 사용하여 종속 항목을 로드하는 경우 이러한 파일은 한 번만 로드됩니다. 추가 요청은 로더 캐시를 사용합니다.

추가 자료

요약

  • webpack 4를 사용하는 경우 프로덕션 모드 사용 설정
  • 번들 수준 축소기 및 로더 옵션으로 코드 최소화
  • NODE_ENVproduction로 바꿔 개발 전용 코드를 삭제합니다.
  • ES 모듈을 사용하여 트리 쉐이킹 사용 설정
  • 이미지 압축
  • 종속 항목별 최적화 적용
  • 모듈 연결 사용 설정
  • 적절한 경우 externals을(를) 사용하세요.