프런트엔드 크기 줄이기

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,
  },
};

Webpack 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 Closure Compiler 플러그인)를 사용하면 됩니다.

추가 자료

이미지 최적화

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

url-loader는 작은 정적 파일을 앱에 인라인합니다. 구성이 없으면 전달된 파일을 가져와 컴파일된 번들 옆에 배치하고 파일의 URL을 반환합니다. 하지만 limit 옵션을 지정하면 이 제한보다 작은 파일을 Base64 데이터 URL로 인코딩하고 이 URL을 반환합니다. 이렇게 하면 이미지가 JavaScript 코드에 인라인 처리되고 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-loader는 Base64 대신 URL 인코딩을 사용하여 파일을 인코딩한다는 점을 제외하면 url-loader와 동일하게 작동합니다. 이는 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 이미지용 1개와 SVG 이미지용 1개)에 복사하여 붙여넣지 않도록 이 로더를 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'
      }
    ]
  }
};

로더의 기본 설정은 이미 사용할 수 있지만 추가로 구성하려면 플러그인 옵션을 참고하세요. 지정할 옵션을 선택하려면 Addy Osmani의 이미지 최적화 가이드를 참고하세요.

추가 자료

종속 항목 최적화

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

예를 들어 Lodash(v4.17.4 기준)는 번들에 최소화된 코드 72KB를 추가합니다. 하지만 메서드 중 20개만 사용한다면 약 65KB의 축소된 코드는 아무것도 하지 않습니다.

또 다른 예로는 Moment.js가 있습니다. 2.19.1 버전은 축소된 코드가 223KB나 되며 이는 매우 큽니다. 페이지의 평균 JavaScript 크기는 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
  }
};

webpack 3에서는 ModuleConcatenationPlugin를 사용합니다.

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

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

추가 자료

webpack 코드와 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에 종속 항목을 노출하지 않으면 상황이 더 복잡해집니다. 그러나 webpack 이외의 코드가 이러한 종속 항목을 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 () {  });

webpack 이외의 코드에서 동일한 URL을 사용하여 종속 항목을 로드하는 경우 이러한 파일은 한 번만 로드되며 추가 요청에서 로더 캐시를 사용합니다.

추가 자료

요약

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