[Webpack] 궁금해서 혼자 정리하는 웹팩 2

Dan·2022년 11월 14일
0

웹팩

목록 보기
2/8
post-thumbnail

웹팩을 앞에 내용에 이어서 정리해보자.

Code Splitting


코드 스플리팅을 사용하면 코드를 다양한 번들로 분할하고, 요청에 따라 로드하거나 병렬로 로드할 수 있다. 더 작은 번들을 만들고 리소스의 우선순위를 올바르게 제어하면 로드 시간에 큰 영향을 끼칠 수 있다.

코드 스플리팅은 세 가지 방식으로 접근 할 수 있다.
1. Entry Points: entry 설정을 사용하여 코드를 수동으로 분할한다.
2. Prevent Duplication: Entry dependencies 또는 SplitChunksPlugin을 사용하여 중복 청크를 제거하고 청크를 분할한다.
3. Dynamic Imports: 모듈 내에서 인라인 함수 호출을 통해 코드를 분할한다.

Entry Points

코드를 분할하는 가장 쉽고 직관적인 방버이지만 다른 방법에 비해 수동적이고 몇 가지 함정이 존재한다.

webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- /src
 |- index.js
 |- another-module.js ++
|- /node_modules
  • another-module.js
import _ from "lodash";

console.log(_.join(['Another', 'module', 'loaded'],' '));
  • webpack.config.js
 const path = require('path');

 module.exports = {
  - entry: './src/index.js',
  mode: 'development',
  + entry: {
    + index: './src/index.js',
    + another: './src/another-module.js',
  + },
   output: {
   - filename: 'main.js',
   + filename: '[name].bundle.js',
     path: path.resolve(__dirname, 'dist'),
   },
 };

이 접근 방식의 함정

  • 엔트리 청크 사이에 중복된 모듈이 있는 경우 두 번들에 모두 포함된다.
  • 코어 애플리케이션 로직을 통한 코드의 동적 분할에는 사용할 수 없으며 유연하지 않다.

Prevent Duplication

Entry Dependencies

dependOn 옵션을 사용하면 청크간 모듈을 공유할 수 있다.

 const path = require('path');

 module.exports = {
   mode: 'development',
   entry: {
     index: {
       import: './src/index.js',
       dependOn: 'shared',
     },
     another: {
       import: './src/another-module.js',
       dependOn: 'shared',
     },
     // lodash 모듈을 dependOn을 통해 공유한다.
     shared: 'lodash',
   },
   output: {
     filename: '[name].bundle.js',
     path: path.resolve(__dirname, 'dist'),
   },
  optimization: {
    runtimeChunk: 'single',
  },
 };

SplitChuncksPlugin

splitchuncksplugin을 활용하면 기존 엔트리 청크 또는 완전히 새로운 청크로 공통 의존성을 추출할 수 있다.

  const path = require('path');

  module.exports = {
    mode: 'development',
    entry: {
      index: './src/index.js',
      another: './src/another-module.js',
    },
    output: {
      filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist'),
    },
   optimization: {
     splitChunks: {
       chunks: 'all',
     },
   },
  };

npm run build를 해보면 lodash가 별도의 청크로 분리되었고 메인 번들에서도 제거된 것을 확인 할 수 있다.

Dynamic Imports

웹팩의 동적 코드 스플리팅에 두 가지 유사한 기술이 사용되지만, ECMAScript제안을 준수하는 import()구문을 사용하는 방식을 권장한다.

  • webpack.config.js
const path = require('path');

module.exports = {
    mode: 'development',
    entry: {
        index: './src/index.js',
    },
    output: {
        filename: '[name].bundle.js',
        path: path.resolve(__dirname, 'dist'),
        clean: true,
    },
};
  • index.js
async function getComponent() {
    const element = document.createElement('div');
    const { default: _ } = await import('lodash');

    element.innerHTML = _.join(['Hello', 'webpack'], ' ');

    return element;
}

getComponent().then((component) => {
    document.body.appendChild(component);
});

import()는 promise를 반환하므로 async 함수와 함께 사용해 위와 같이 코드를 단순화 할 수 있다. 이제 빌드를 실행해보면 lodash가 별도의 번들로 분리되어 있는것을 확인 할 수 있을것이다.

Caching


웹팩 컴파일로 생성 된 파일의 내용이 변경되지 않는 한 캐싱을 통해 네트워크 트래픽과 사이트의 로드 속도를 줄일 수 있다. 웹팩을 통해 캐싱처리 하는 방법을 알아보도록 하자.

Output Filenames

웹팩은 substitutions이라고 하는 [문자열]을 사용하여 파일 이름을 템플릿화 하는 방법을 제공하는데 output.filename.[contenthash]를 사용하면 애셋의 콘텐츠에 따라 고유한 해시를 이름으로 추가하게 된다.

  • webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    entry: './src/index.js',
    plugins: [
        new HtmlWebpackPlugin({
            title: 'Caching',
        }),
    ],
    output: {
      ++filename: '[name].[contenthash].js',
        path: path.resolve(__dirname, 'dist'),
        clean: true,
    },
};

위와 같은 설정으로 npm run build를 실행하게되면 번들의 이름이 아래와 같이 해시로 생성 된 것을 볼 수 있다. 하지만 빌드를 다시 실행시 동일한 해시값으로 번들이 유지될 것이라 생각하지만 그렇지 않다. 그 이유는 웹팩이 런타임과 매니페스트를 엔트리 청크에 포함하고 있기 떄문이다.

asset 486.e1b45d15e809cbacd309.js 68.9 KiB [emitted][immutable] [minimized] (id hint: vendors) 1 related asset
asset main.6330fdc98adee062c417.js 3.25 KiB [emitted][immutable] [minimized] (name: main)

Extracting Boilerplate

lodash 또는 react와 같은 타사 라이브러리는 로컬 소스보다 변경 횟수가 적기 때문에 별도의 vendor 청크로 추출하는 것이 좋은 방법이다. 별도의 청크로 추출함으로 써 클라이언트는 최신상태를 유지하되 서버에 더 적은 요청을 할 수 있게 된다. 아래와 같은 방법으로 node_modules안에 있는 라이브러리들을 vendor 청크로 추출해보자.

  const path = require('path');
  const HtmlWebpackPlugin = require('html-webpack-plugin');

  module.exports = {
    entry: './src/index.js',
    plugins: [
      new HtmlWebpackPlugin({
      title: 'Caching',
      }),
    ],
    output: {
      filename: '[name].[contenthash].js',
      path: path.resolve(__dirname, 'dist'),
      clean: true,
    },
 ++   optimization: {
 ++     runtimeChunk: 'single',
 ++    splitChunks: {
 ++      cacheGroups: {
 ++        vendor: {
 ++          test: /[\\/]node_modules[\\/]/,
 ++          name: 'vendors',
 ++          chunks: 'all',
         },
       },
     },
    },
  };

빌드를 시켜보면 main 번들에 node_modules 디렉터리의 vendor 코드가 포함되지 않기에 240bytes로 줄어든 것을 볼 수 있다.

Module Identifiers

프로젝트에 다른 모듈 test.js를 추가한 다음 index.js에서 import해서 빌드할 시 main번들의 해시만 변경 될 것이라 예상하지만 vendors 랑 runtime도 함께 변경되는것을 볼 수 있다.

// test.js 추가 전
runtime.cc17ae2a94ec771e9221.js   1.42 KiB       0  [emitted]  runtime
vendors.a42c3ca0d742766d7a28.js   69.4 KiB       1  [emitted]  vendors
   main.abf44fedb7d11d4312d7.js  240 bytes       2  [emitted]  main
                     index.html  353 bytes          [emitted]


// test.js 추가 후
                           Asset       Size  Chunks                    Chunk Names
  runtime.1400d5af64fc1b7b3a45.js    5.85 kB      0  [emitted]         runtime
  vendor.a7561fb0e9a071baadb9.js     541 kB       1  [emitted]  [big]  vendor
    main.b746e3eb72875af2caa9.js    1.22 kB       2  [emitted]         main
                      index.html  352 bytes          [emitted]

이는 module.id가 해석 순서에 따라 증가하기 떄문이다.
1. 새로운 콘텐츠로 인해 main 번들이 변경됨
2. module.id가 바뀌어 vendor 번들이 변경됨
3. runtime번들은 이제 새로운 모듈에 대한 참조를 포함하게 됨

이를 해결하기 위해 optimization.moduleIds 'deterministic' 옵션을 설정해주면 해결 된다.

Evironment Variables

개발환경과 배포환경을 명확하게 구분하기 위해서 웹팩도 환경 변수를 사용할 수 있다.

npx webpack --env goal=local --env production --progress

할당없이 env 변수를 사용하명 --env production이 기본적으로 할당된다.

const path = require('path');

module.exports = (env) => {
  // 여기에서 env.<변수> 를 사용하세요.
  console.log('Goal: ', env.goal); // 'local'
  console.log('Production: ', env.production); // true

  return {
    entry: './src/index.js',
    output: {
      filename: 'bundle.js',
      path: path.resolve(__dirname, 'dist'),
    },
  };
};

Build Performance


빌드/컴파일 성능을 개선하기 위한 방법을 살펴보자.

Loaders

최소한 필요한 모듈에만 로더를 적용시킨다.

module.exports = {
  //...
  module: {
    rules: [
      {
        test: /\.js$/,
        loader: 'babel-loader',
      },
    ],
  },
};

// 위와 같은 방식보다는 아래와 같이 include를 사용해 실제로 변환해야하난 모듈에만 로더를 적용한다.

const path = require('path');

module.exports = {
  //...
  module: {
    rules: [
      {
        test: /\.js$/,
        include: path.resolve(__dirname, 'src'),
        loader: 'babel-loader',
      },
    ],
  },
};

Resolving

  • 파일 시스템의 호출 수가 증되기 때문에 resolve.modules, resolve.extensions, resolve.mainFiles, resolve.descriptionFiles의 항목을 최소화한다.
  • 컨텍스트에 특정적이지 않은 커스텀 해석 플러그인 사용하는 경우 resolve.cacheWithContext:false를 설정한다.

Smaller = Faster

컴파일의 총 크기를 줄이면 빌드 성능을 올릴 수 있다.

  • 더 적고 작은 라이브러리 사용
  • 다중 페이지 애플리케이션에서 SplitChuncksPlugin 사용
  • 다중 페이지 어플리케이션의 async모드에서 SplitChunksPlugin 사용
  • 사용하지 않는 코드를 제거
  • 현재 개발중인 코드의 일부만 컴파일

Hot Module Replacement(HMR)


HMR은 모든 종류의 모듈을 세로고침 할 필요 없이 런타임에 업데이트 할 수 있는 기능이다.

Enabling HMR

HMR을 사용하기 위해선 webpack-dev-server설정을 업데이트하고 webpack의 내장 HMR 플러그인을 사용하면 된다.

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require("webpack");

module.exports = {
    entry: {
        app: './src/index.js',
        // hot module replacement를 위한 런타임 코드
        hot: 'webpack/hot/dev-server.js',
        // 웹 소켓 전송, hot 및 live 리로드 로직을 위한 개발 서버 클라이언트
        client: 'webpack-dev-server/client/index.js?hot=true&live-reload=true',
    },
    devtool: 'inline-source-map',
    devServer: {
        static: './dist',
        // 웹 소켓 전송, hot 및 live 리로드 로직을 위한 개발 서버 클라이언트
        hot: false,
        client: false,
    },
    plugins: [
        new HtmlWebpackPlugin({
            title: 'Hot Module Replacement',
        }),
        // hot module replacement를 위한 플러그인
        new webpack.HotModuleReplacementPlugin(),
    ],
    output: {
        filename: '[name].bundle.js',
        path: path.resolve(__dirname, 'dist'),
        clean: true,
    },
};

HMR with Stylesheets

CSS에 HMR을 적용하는것은 굉장히 간단하다. style-loader을 적용시키면 HMR이 알아서 style 태그를 패치해와 변화가 있을때 마다 새로고침을 한다.

npm install --save-dev style-loader css-loader

  • webpack.config.js
   module: {
     rules: [
       {
         test: /\.css$/,
         use: ['style-loader', 'css-loader'],
       },
     ],
   },

Tree Shaking


Tree Shaking은 사용되지 않는 코드를 제거하기 위해 Javascript 컨텍스트에서 일반적으로 사용되는 용어이다. 나무를 흔들어서 힘이 약해지거나 쓸모 없어진 열매를 떨어트린다는 의미랑 비슷하지 않을까 싶다.

Add a Utility

두 함수를 내보내는 새 유틸리티 파일 src/math.js를 추가해보자.

webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
  |- bundle.js
  |- index.html
|- /src
  |- index.js
 |- math.js
|- /node_modules
  • src/math.js
export function square(x) {
  return x * x;
}

export function cube(x) {
  return x * x * x;
}

mode를 development로 설정하여 번들이 압축되지 않게 한다.

  • webpack.config.js
const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
 mode: 'development',
 optimization: {
   usedExports: true,
 },
};
  • src/index.js

math 모듈에 있는 cube만 import해서 사용해보자.

import { cube } from './math.js';

function component() {

    const element = document.createElement('pre');


    element.innerHTML = [
        'Hello webpack!',
        '5 cubed is equal to ' + cube(5)
    ].join('\n\n');

    return element;
}

document.body.appendChild(component());

이제 이것을 빌드 해보면

/* 1 */
/***/ (function (module, __webpack_exports__, __webpack_require__) {
  'use strict';
  /* unused harmony export square */
  /* harmony export (immutable) */ __webpack_exports__['a'] = cube;
  function square(x) {
    return x * x;
  }

  function cube(x) {
    return x * x * x;
  }
});

위처럼 사용하지 않은 square은 비록 export하진 않았지만 번들에서 포함하고 있는 것을 확인할 수 있다.

Mark the file as side-effect-free

'사이드 이펙트'는 하나 이상의 export를 보여주는 것 이외에도 import할 때 특별한 동작을 수행하는 코드이다. 예를 들면 전체 스코프에 영향을 미치며 일반적으로 export를 제공하지 않는 폴리필이 있다.

위와 같은 코드는 사이드 이펙트가 포함되지 않음으로 간단하게 package.json에서 'sideEffects'를 false옵션으로 해주면 사용되지 않는 export를 제거할 수 있다. 하지만 코드에 사이드 이팩트가 존재한다면 불리언 값 대신 배열을 사용할 수 있다.

{
  "name": "your-project",
  "sideEffects": ["./src/some-side-effectful-file.js", "*.css"]
}

Clarifying tree shaking and sideEffects

sideEffect와 usedExports(트리쉐이킹)의 최적화는 다르다. 일반적으로는 sideEffects는 전체 모듈 및 파일, 전체 하위 트리를 건널뛸 수 있기 떄문에 훨씬 더 효율적이다.

Minify the Output

import 와 export 구문을 통해 "사용하지 않는 코드"를 삭제했지만 번들에서도 삭제하기 위해선 mode 옵션이 production으로 설정을 해야한다.

const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
 mode: 'production',
};
profile
만들고 싶은게 많은 개발자

0개의 댓글