6.14 Webpack

Babel과 Webpack을 이용한 ES6 환경 구축 ②

babel-webpack

2. Webpack

2.1 Webpack이란?

Webpack은 의존 관계에 있는 모듈들을 하나의 자바스크립트 파일로 번들링하는 모듈 번들러이다. Webpack을 사용하면 의존 모듈이 하나의 파일로 번들링되므로 별도의 모듈 로더가 필요없다. 그리고 다수의 자바스크립트 파일을 하나의 파일로 번들링하므로 html 파일에서 script 태그로 다수의 자바스크립트 파일을 로드해야 하는 번거로움도 사라진다.

Webpack

Webpack

Webpack과 Babel을 이용하여 ES6+ 개발 환경을 구축하여 보자. Webpack이 자바스크립트 파일을 번들링하기 전에 Babel을 로드하여 ES6+ 코드를 ES5 코드로 트랜스파일링하는 작업을 실행하도록 설정할 것이다. 그리고 Sass를 사용하는 경우, Sass 트랜스파일링도 Webpack에서 관리하도록 할 것이다.

2.2 Webpack 설치

아래 명령으로 Webpack을 설치한다.

# Webpack V4는 webpack-cli를 요구한다
$ npm install --save-dev webpack webpack-cli

설치가 완료된 이후 package.json 파일은 아래와 같다.

{
  "name": "es6-project",
  "version": "1.0.0",
  "scripts": {
    "build": "babel src/js -w -d dist/js"
  },
  "devDependencies": {
    "@babel/cli": "^7.7.0",
    "@babel/core": "^7.7.2",
    "@babel/plugin-proposal-class-properties": "^7.7.0",
    "@babel/preset-env": "^7.7.1",
    "webpack": "^4.41.2",
    "webpack-cli": "^3.3.10"
  }
}

2.3 babel-loader

Webpack이 모듈을 번들링할 때 Babel을 사용하여 ES6+ 코드를 ES5 코드로 트랜스파일링하도록 babel-loader를 설치한다.

# babel-loader 설치
$ npm install --save-dev babel-loader

이제 npm script를 변경하여 Babel 대신 Webpack을 실행하도록 수정하자. 아래와 같이 package.json 파일의 scripts를 변경한다. 완성된 package.json 파일은 아래와 같다.

{
  "name": "es6-project",
  "version": "1.0.0",
  "scripts": {
    "build": "webpack -w"
  },
  "devDependencies": {
    "@babel/cli": "^7.7.0",
    "@babel/core": "^7.7.2",
    "@babel/plugin-proposal-class-properties": "^7.7.0",
    "@babel/preset-env": "^7.7.1",
    "babel-loader": "^8.0.6",
    "webpack": "^4.41.2",
    "webpack-cli": "^3.3.10"
  }
}

2.4 webpack.config.js

webpack.config.js은 Webpack이 실행될 때 참조하는 설정 파일이다. 프로젝트 루트에 webpack.config.js 파일을 생성하고 아래와 같이 작성한다.

const path = require('path');

module.exports = {
  // enntry file
  entry: './src/js/main.js',
  // 컴파일 + 번들링된 js 파일이 저장될 경로와 이름 지정
  output: {
    path: path.resolve(__dirname, 'dist/js'),
    filename: 'bundle.js'
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        include: [
          path.resolve(__dirname, 'src/js')
        ],
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env'],
            plugins: ['@babel/plugin-proposal-class-properties']
          }
        }
      }
    ]
  },
  devtool: 'source-map',
  // https://webpack.js.org/concepts/mode/#mode-development
  mode: 'development'
};

이제 Webpack을 실행하여 트랜스파일링 및 번들링을 실행한다. 트랜스파일링은 Babel이 실행하고 번들링은 Webpack이 실행한다. 만약 이전에 실행시킨 빌드 명령이 실행 중인 상태라면 중지시키고 다시 아래 명령을 실행한다.

$ npm run build

> es6-project@1.0.0 build /Users/leeungmo/Desktop/es6-project
> webpack -w

webpack is watching the files…

Hash: f820f0d61705cdd50948
Version: webpack 4.41.2
Time: 1137ms
Built at: 2019. 11. 13. 오후 4:40:12
        Asset      Size  Chunks                   Chunk Names
    bundle.js   8.5 KiB    main  [emitted]        main
bundle.js.map  5.08 KiB    main  [emitted] [dev]  main
Entrypoint main = bundle.js bundle.js.map
[./src/js/lib.js] 3.66 KiB {main} [built]
[./src/js/main.js] 147 bytes {main} [built]

실행 결과 dist/js 폴더에 bundle.js이 생성되었다. 이 파일은 main.js, lib.js 모듈이 하나로 번들링된 결과물이다.

index.html을 아래와 같이 수정하고 브라우저에서 실행해 보자.

<!DOCTYPE html>
<html>
<body>
  <script src="./dist/js/bundle.js"></script>
</body>
</html>

module success

모듈 로딩 성공

main.js, lib.js 모듈이 하나로 번들링된 bundle.js가 브라우저에서 문제없이 실행된 것을 확인할 수 있다.

2.5 babel-polyfill

Babel을 사용하여 ES6+ 코드를 ES5 이하로 트랜스파일링하여도 브라우저가 지원하지 않는 코드가 남아 있을 수 있다. 예를 들어, ES6에서 추가된 Promise, Object.assign, Array.from 등은 ES5 이하로 트랜스파일링하여도 대체할 ES5 기능이 없기 때문에 그대로 남아 있다.

src/js/main.js를 아래와 같이 수정하여 ES6에서 추가된 Promise, Object.assign, Array.from 등이 어떻게 트랜스파일링되는지 확인해 보자.

// src/js/main.js
import { pi, power, Foo } from './lib';

console.log(pi);
console.log(power(pi, pi));

const f = new Foo();
console.log(f.foo());
console.log(f.bar());

// polyfill이 필요한 코드
console.log(new Promise((resolve, reject) => {
  setTimeout(() => resolve(1), 100);
}));

// polyfill이 필요한 코드
console.log(Object.assign({}, { x: 1 }, { y: 2 }));

// polyfill이 필요한 코드
console.log(Array.from([1, 2, 3], v => v + v));

다시 트랜스파일링과 번들링을 실행한 다음, dist/js/bundle.js을 확인해보자.

...
// 190 line
console.log(new Promise(function (resolve, reject) {
  setTimeout(function () {
    return resolve(1);
  }, 100);
})); // polyfill이 필요한 코드

console.log(Object.assign({}, {
  x: 1
}, {
  y: 2
})); // polyfill이 필요한 코드

console.log(Array.from([1, 2, 3], function (v) {
  return v + v;
}));
...

위와 같이 Promise, Object.assign, Array.from 등과 같이 ES5 이하로 대체할 수 없는 기능은 트랜스파일링이 되지 않는다.

따라서 오래된 브라우저에서도 ES6+에서 새롭게 추가된 객체나 메소드를 사용하기 위해서는 @babel/polyfill을 설치해야 한다.

$ npm install @babel/polyfill

설치가 완료된 이후 package.json 파일은 아래와 같다.

{
  "name": "es6-project",
  "version": "1.0.0",
  "scripts": {
    "build": "webpack -w"
  },
  "devDependencies": {
    "@babel/cli": "^7.7.0",
    "@babel/core": "^7.7.2",
    "@babel/plugin-proposal-class-properties": "^7.7.0",
    "@babel/preset-env": "^7.7.1",
    "babel-loader": "^8.0.6",
    "webpack": "^4.41.2",
    "webpack-cli": "^3.3.10"
  },
  "dependencies": {
    "@babel/polyfill": "^7.7.0"
  }
}

babel-polyfill은 개발 환경에서만 사용하는 것이 아니라 실제 환경에서도 사용하여야 하므로 --save-dev 옵션으로 개발 설치를 하지 않도록 한다.

ES6의 import를 사용하는 경우에는 진입점의 선두에서 먼저 폴리필을 로드하도록 한다.

// src/js/main.js
import "@babel/polyfill";
...

webpack을 사용하는 경우에는 위 방법을 대신 폴리필을 webpack.config.js 파일의 entry 배열에 추가한다.

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

module.exports = {
  // entry files
  entry: ['@babel/polyfill', './src/js/main.js'],
  ...

위와 같이 webpack.config.js 파일을 수정하여 폴리필을 반영해보자. 빌드 명령이 실행 중인 상태라면 중지시키고 다시 아래 명령을 실행한다.

$ npm run build

> es6-project@1.0.0 build /Users/leeungmo/Desktop/es6-project
> webpack -w

webpack is watching the files…

Hash: ff92f8edebb4e2a8613d
Version: webpack 4.41.2
Time: 1971ms
Built at: 2019. 11. 13. 오후 4:55:26
        Asset     Size  Chunks                   Chunk Names
    bundle.js  408 KiB    main  [emitted]        main
bundle.js.map  324 KiB    main  [emitted] [dev]  main
Entrypoint main = bundle.js bundle.js.map
[0] multi @babel/polyfill ./src/js/main.js 40 bytes {main} [built]
[./src/js/lib.js] 3.66 KiB {main} [built]
[./src/js/main.js] 491 bytes {main} [built]
    + 307 hidden modules

dist/js/bundle.js을 확인해보면 아래와 같이 polyfill이 추가된 것을 확인할 수 있다.

babel-polyfill

추가된 폴리필

2.6 Sass 컴파일

이번에는 Webpack을 통해 Sass를 컴파일하는 방법에 대해 살펴보자. Sass를 컴파일한 결과물인 css를 bundle.js 파일에 포함시키는 방법과 별도의 css 파일로 분리하는 방법이 있다.

2.6.1 컴파일된 css를 bundle.js 파일에 포함시키는 방법

필요한 패키지를 설치하자. node-sass는 node.js 환경에서 사용할 수 있는 Sass 라이브러리이다. 실제로 Sass를 css로 컴파일하는 것은 node-sass이다. style-loader, css-loader, sass-loader는 Webpack 플러그인이다.

$ npm install node-sass style-loader css-loader sass-loader --save-dev

설치가 완료된 이후 package.json 파일은 아래와 같다.

{
  "name": "es6-project",
  "version": "1.0.0",
  "scripts": {
    "build": "webpack -w"
  },
  "devDependencies": {
    "@babel/cli": "^7.7.0",
    "@babel/core": "^7.7.2",
    "@babel/plugin-proposal-class-properties": "^7.7.0",
    "@babel/preset-env": "^7.7.1",
    "babel-loader": "^8.0.6",
    "css-loader": "^3.2.0",
    "node-sass": "^4.13.0",
    "sass-loader": "^8.0.0",
    "style-loader": "^1.0.0",
    "webpack": "^4.41.2",
    "webpack-cli": "^3.3.10"
  },
  "dependencies": {
    "@babel/polyfill": "^7.7.0"
  }
}

webpack.config.js 파일을 아래와 같이 수정한다.

const path = require('path');

module.exports = {
  // entry files
  entry: ['@babel/polyfill', './src/js/main.js', './src/sass/main.scss'],
  // 컴파일 + 번들링된 js 파일이 저장될 경로와 이름 지정
  output: {
    path: path.resolve(__dirname, 'dist/js'),
    filename: 'bundle.js'
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        include: [
          path.resolve(__dirname, 'src/js')
        ],
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env'],
            plugins: ['@babel/plugin-proposal-class-properties']
          }
        }
      },
      {
        test: /\.scss$/,
        use: [
          "style-loader", // creates style nodes from JS strings
          "css-loader",   // translates CSS into CommonJS
          "sass-loader"   // compiles Sass to CSS, using Node Sass by default
        ],
        exclude: /node_modules/
      }
    ]
  },
  devtool: 'source-map',
  // https://webpack.js.org/concepts/mode/#mode-development
  mode: 'development'
};

테스트를 위해 3개의 Sass 파일을 src/sass 폴더와 src/sass/partials 폴더에 추가한다.

// src/sass/main.scss
@import "partials/vars";
@import "partials/body";
// src/sass/partials/_vars.scss
$font_color: #333;
$font_family: Arial, sans-serif;
$font_size: 16px;
$line_height: percentage(20px / $font_size);
// src/sass/partials/_body.scss
body {
  color: $font_color;

  // Property Nesting
  font: {
    size: $font_size;
    family: $font_family;
  }

  line-height: $line_height;
}

빌드 명령이 실행 중인 상태라면 중지시키고 다시 아래 명령을 실행한다.

$ npm run build

> es6-project@1.0.0 build /Users/leeungmo/Desktop/es6-project
> webpack -w

webpack is watching the files…

Hash: b790cd4af9709201321f
Version: webpack 4.41.2
Time: 2370ms
Built at: 2019. 11. 13. 오후 5:09:35
        Asset     Size  Chunks                   Chunk Names
    bundle.js  420 KiB    main  [emitted]        main
bundle.js.map  337 KiB    main  [emitted] [dev]  main
Entrypoint main = bundle.js bundle.js.map
[0] multi @babel/polyfill ./src/js/main.js ./src/sass/main.scss 52 bytes {main} [built]
[./node_modules/css-loader/dist/cjs.js!./node_modules/sass-loader/dist/cjs.js!./src/sass/main.scss] 244 bytes {main} [built]
[./src/js/lib.js] 3.66 KiB {main} [built]
[./src/js/main.js] 491 bytes {main} [built]
[./src/sass/main.scss] 452 bytes {main} [built]
    + 309 hidden modules

CSS가 적용되는 것을 확인하기 위해 index.html을 아래와 같이 수정하자.

<!DOCTYPE html>
<html>
  <head>
    <script src="./dist/js/bundle.js"></script>
  </head>
<body>
  Hello world!
</body>
</html>

아래와 같이 CSS가 적용된 것을 확인할 수 있다.

webpack-sass

컴파일된 CSS는 bundle.js에 포함되어 있다.

webpack-sass

2.6.2 컴파일된 CSS를 별도의 CSS 파일로 분리하는 방법

Sass 파일이 방대해지면 자바스크립트 파일에서 분리하는 것이 효율적일 수 있다. bundle.js 파일에 컴파일된 css를 포함시키지 말고 별도의 css 파일로 분리해서 하나의 파일로 번들링해보자. 이때 사용하는 플러그인은 mini-css-extract-plugin이다.

Webpack v4 이전 버전에서는 extract-text-webpack-plugin을 사용했었다. Webpack v4부터 css와 관련한 파일 분리는 mini-css-extract-plugin을 사용하도록 변경되었다.

mini-css-extract-plugin을 설치하자.

$ npm install --save-dev mini-css-extract-plugin

설치가 완료된 이후 package.json 파일은 아래와 같다.

{
  "name": "es6-project",
  "version": "1.0.0",
  "scripts": {
    "build": "webpack -w"
  },
  "devDependencies": {
    "@babel/cli": "^7.7.0",
    "@babel/core": "^7.7.2",
    "@babel/plugin-proposal-class-properties": "^7.7.0",
    "@babel/preset-env": "^7.7.1",
    "babel-loader": "^8.0.6",
    "css-loader": "^3.2.0",
    "mini-css-extract-plugin": "^0.8.0",
    "node-sass": "^4.13.0",
    "sass-loader": "^8.0.0",
    "style-loader": "^1.0.0",
    "webpack": "^4.41.2",
    "webpack-cli": "^3.3.10"
  },
  "dependencies": {
    "@babel/polyfill": "^7.7.0"
  }
}

webpack.config.js 파일을 아래와 같이 수정한다.

const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  // entry files
  entry: ['@babel/polyfill', './src/js/main.js', './src/sass/main.scss'],
  // 컴파일 + 번들링된 js 파일이 저장될 경로와 이름 지정
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'js/bundle.js'
  },
  plugins: [
    // 컴파일 + 번들링 CSS 파일이 저장될 경로와 이름 지정
    new MiniCssExtractPlugin({ filename: 'css/style.css' })
  ],
  module: {
    rules: [
      {
        test: /\.js$/,
        include: [
          path.resolve(__dirname, 'src/js')
        ],
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env'],
            plugins: ['@babel/plugin-proposal-class-properties']
          }
        },
        exclude: /node_modules/
      },
      {
        test: /\.scss$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
          'sass-loader'
        ],
        exclude: /node_modules/
      }
    ]
  },
  devtool: 'source-map',
  // https://webpack.js.org/concepts/mode/#mode-development
  mode: 'development'
};

빌드 명령이 실행 중인 상태라면 중지시키고 다시 아래 명령을 실행한다.

$ npm run build

> es6-project@1.0.0 build /Users/leeungmo/Desktop/es6-project
> webpack -w

webpack is watching the files…

Hash: 533f7b41094e8037b3c2
Version: webpack 4.41.2
Time: 2227ms
Built at: 2019. 11. 14. 오전 8:48:35
            Asset       Size  Chunks                   Chunk Names
    css/style.css  136 bytes    main  [emitted]        main
css/style.css.map  279 bytes    main  [emitted] [dev]  main
     js/bundle.js    408 KiB    main  [emitted]        main
 js/bundle.js.map    324 KiB    main  [emitted] [dev]  main
Entrypoint main = css/style.css js/bundle.js css/style.css.map js/bundle.js.map
[0] multi @babel/polyfill ./src/js/main.js ./src/sass/main.scss 52 bytes {main} [built]
[./src/js/lib.js] 3.66 KiB {main} [built]
[./src/js/main.js] 491 bytes {main} [built]
[./src/sass/main.scss] 39 bytes {main} [built]
    + 308 hidden modules
Child mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!node_modules/sass-loader/dist/cjs.js!src/sass/main.scss:
    Entrypoint mini-css-extract-plugin = *
    [./node_modules/css-loader/dist/cjs.js!./node_modules/sass-loader/dist/cjs.js!./src/sass/main.scss] 244 bytes {mini-css-extract-plugin} [built]
        + 1 hidden module

아래와 같이 css 폴더가 생성되고 style.css 파일이 저장되었다. 컴파일되고 하나의 파일로 번들링된 css가 bundle.js 파일에 포함되지 않고 별도 파일로 분리된 것이다.

body {
  color: #333;
  font-size: 16px;
  font-family: Arial, sans-serif;
  line-height: 125%;
}


/*# sourceMappingURL=style.css.map*/

이제 index.html에서 style.css 파일을 로드하도록 하자.

<!DOCTYPE html>
<html>
  <head>
    <link href="./dist/css/style.css" rel="stylesheet"></link>
    <script src="./dist/js/bundle.js"></script>
  </head>
<body>
  Hello world!
</body>
</html>

아래와 같이 CSS가 적용된 것을 확인할 수 있다.

webpack-sass

Reference

Back to top
Close