ihl 2021. 5. 19. 01:17

1. 웹팩의 도입배경

 1) 모듈

<html>
  <body>
    <script src="./a.js"></script> //변수 num이 포함된 a.js
    <script src="./b.js"></script> //변수 num이 포함된 b.js
    <script>
      console.log(num); //?
    </script>
  </body>
</html>

  웹 개발을 하다보면 CSS, JS 등 다양한 종류의 리소스 파일들을 여러 개 생성하게 된다. 만약 하나의 html 파일에서 2개의 JS파일이 필요하다 생각해보자. 이 경우 script 태그를 2개 사용하면 된다. 그런데 위와 같이 불러온 JS파일 두 개가 모두 같은 이름의 변수를 사용한다 생각해보자. console.log에 찍히는 num은 어떤 JS파일의 num이 될까? script태그는 위와 같이 코드의 예상치 못한 충돌을 발생시킬 수 있다.

 

//a.js
const num = 1;
export default num; 

//b.js
const num = 2;
export default num; 

  이러한 충돌문제를 해결하기 위한 것이 모듈이라는 개념이다. 모듈은 모듈별로 격리된 문맥을 갖게된다. 직접 모듈을 만들어보자. 먼저 html에서 불러올 a.js와 b.js에서 num을 export(모듈 내보내기)한다.

 

<html>
  <body>
    <script type="module">
      import a_num from './a.js';
      import b_num from './b.js';
      console.log(a_num); //a.js의 num
      console.log(b_num); //b.js의 num
    </script>
  </body>
</html>

  html 파일에서는 script의 type을 module로 지정한 후 a.js와 b.js를 import(모듈 가져오기) 한다. 이 때 각 파일에서 export된 값을 담을 변수를 할당하고 이를 사용한다. 이렇게 하면 2개의 script태그로 두 파일을 가져왔을 때와 다르게 a.js의 num과 b.js의 num을 구분할 수 있다. 모듈은 오래된 브라우저에서는 지원되지 않는데 웹팩을 사용하면 브라우저와 상관없이 여러 JS파일을 모듈로 사용할 수 있다.

 

 2) 네트워크 요청

모듈 번들링

  수많은 파일을 모듈화하여 그대로 웹 서비스화하면 수많은 파일들을 각각 서버로부터 불러오게 된다. JS모듈 10개로 이루어진 웹 서비스가 있다면 10번의 네트워크 요청이 필요한 것이다. 그런데 이러한 네트워크 요청은 리소스적으로 비용이 비싼 편이기 때문에 많은 네트워크 요청은 페이지 로딩을 느리게 한다. 

 

  특히 HTTP/1의 경우 브라우저마다 한 출처(도메인)에 대한 TCP 연결 수가 제한되어있다. 예를 들어 크롬은 한 출처에 대해 6개의 연결을 허용한다. 그런데 내가 10개의 모듈을 사용하게 된다면 7번째 요청부터는 이전 요청에 대한 응답을 받은 후에야 서버에 요청을 보낼 수 있는 것이다.

 

  번들러는 여러개의 리소스 파일들을 하나로 묶어주는 역할을 하며 번들러를 사용함으로써 페이지 로딩 시 네트워크 요청을 최소화할 수 있게 된다. 이외에도 모듈화, transpile(최신 문법<->이전 문법), 코드 최적화, 작업(HTML, CSS, JS, 이미지 압축, CSS 전처리기 변환) 자동화를 제공하기도 한다. 이러한 번들러 중 하나가 웹팩이다.

 

2. 웹팩 사용해보기

 1) 설치하기

npm install -D webpack webpack-cli

  웹팩과 webpack-cli를 npm 옵션 D로 설치한다. 4버전 부터는 웹팩 설정파일이 필수가 아니기 때문에 설정파일을 만들고 싶지 않다면 명령어 만으로도 번들링을 할 수도 있다. 하지만 옵션을 번들링 할 때마다 써주는 것 보다는 config파일을 만들어 관리하는 것을 더 추천한다.

 

 2) Entry point 만들기

//entry.js
import a_num from './a.js';
import b_num from './b.js';
console.log(a_num); //a.js의 num
console.log(b_num); //b.js의 num

  웹팩에서 Entry는 모듈들의 진입지점을 의미한다. 즉, 엔트리는 서로 의존관계에 있는 여러 모듈들을 사용하는 시작점으로, 엔트리는 여러 개가 존재할 수 있다. 예를 들어 entry.js는 a.js와 b.js를 포함하며 이들을 사용하고 있다.

 

<html>
  <body>
    <script src="./public/entry_bundle.js"></script>
  </body>
</html>

  HTML 파일도 a와 b를 import 하는 대신 웹팩을 이용하여 번들링된 결과파일을 사용하도록 변경한다. 나는 entry.js-a.js-b.js 파일들을 public폴더 내의 entry-bundle.js로 번들링할 것이기 때문에 위와 같이 변경하였다.

 

3) 설정파일 만들기

  지금까지 작성한 파일들의 경로는 다음과 같다. public은 웹팩 번들링 결과를 담을 빈 폴더이다. 웹팩 설정 파일은 webpack.config.js라는 이름으로 프로젝트의 루트 경로에 작성한다. 

 

const path = require('path');
module.exports = {
  entry: "./src/entry.js",
  output: {
    path: path.resolve(__dirname, "public"),
    filename: 'entry_bundle.js'
  }
}

  webpack.config.js의 내용은 위와 같다. 우선 entry에 엔트리 파일 경로를 적고, output에 번들링 결과파일의 경로와 이름에 대해 작성한다. 

 

npx webpack

  webpack.config.js 라는 이름으로 작성했다면 npx webpack이라는 명령어로 번들링을 실행할 수 있다. 만약 다른 이름으로 설정했다면 --config옵션으로 설정 파일을 지정하고 실행한다.

 

번들링 결과

  번들링된 결과파일은 위와 같다.  a와 b의 num이 각각 1과 2로 대체되고 코드의 공백이 사라진 것을 확인할 수 있다. 이렇듯 웹팩은 모듈을 합치는 것뿐만 아니라 공백을 없애 파일의 크기를 최소화(압축)시킨다. 

 

development 모드

  웹팩은 development/production 2개의 모드가 존재하며, 모드에 따라 결과물이 다르다. 모드를 지정하지 않으면 production모드로 번들링이 진행된다. 위 캡처은 development 모드로 실행한 결과이다. development 모드의 결과물은 웹팩 로그를 포함하며 코드를 압축하지 않는다.

 

  설정파일은 개발용과 배포용 2개를 만드는 것이 좋다. 각각 모드와 필요한 파일을 설정한 후 --config 옵션으로 선택하여 빌드하는 것이다.

 

const path = require('path');
module.exports = {
  mode: 'development',
  entry: {
    a: './src/a.js',
    b: './src/b.js',
  },
  output: {
    path: path.resolve(__dirname, 'public'),
    filename: '[name]_bundle.js',
  },
};

  엔트리가 여러 개인 경우 entry를 객체로 지정한 후 output속성을 따로 지정한다. 위 코드는 엔트리가 a.js와 b.js인 코드이다. 번들 결과는 public폴더에 a_bundle.js, b_bundle.js로 생성된다. filename 속성의 [name]이 entry의 이름(key)를 의미하기 때문이다.

 

3. Loader

npm install -D css-loader, style-loader

  웹팩은 기본적으로 자바스크립트 파일만 읽을 수 있다. 그렇다면 CSS와 이미지파일은 번들링할 수 없는 것일까? 로더는 CSS와 이미지 파일을 번들링하기 위해 이들을 자바스크립트로 변환하거나 이미지를 문자열로 변환한다. CSS파일을 번들링 하기 위해 우선 css-loader와 style-loader를 설치해야한다.

 

//entry.js
import a_num from './a.js';
import b_num from './b.js';
import './style.css';
console.log(a_num); //a.js의 num
console.log(b_num); //b.js의 num

  적용할 CSS를 작성한 후 엔트리 파일에 import한다.

 

const path = require('path');
module.exports = {
  mode: 'production',
  entry: './src/entry.js',
  output: {
    path: path.resolve(__dirname, 'public'),
    filename: 'entry_bundle.js',
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
      },
    ],
  },
};

  설정파일에 module 속성을 추가한다.  module.rules는 사용하려는 로더의 규칙을 의미한다. test 속성은 로더를 적용할 파일로 보통 정규표현식의 형태를 사용한다. use는 적용할 로더를 지정한다. 로더의 적용순서는 배열의 마지막부터 적용되므로 css-loader > style-loader 순으로 적용된다.

 

(()=>{"use strict";var e,n,t,r={426:(e,n,t)=>{t.d(n,{Z:()=>i});var r=t(645),o=t.n(r)()((function(e){return e[1]}));o.push([e.id,"body {\r\n  background-color: indianred;\r\n}\r\n",""]);const i=o},645:e=>{e.exports=function(e){var n=[];return n.toString=function(){return this.map((function(n){var t=e(n);return n[2]?"@media ".concat(n[2]," {").concat(t,"}"):t})).join("")},n.i=function(e,t,r){"string"==typeof e&&(e=[[null,e,""]]);var o={};if(r)for(var i=0;i<this.length;i++){var a=this[i][0];null!=a&&(o[a]=!0)}for(var c=0;c<e.length;c++){var s=[].concat(e[c]);r&&o[s[0]]||(t&&(s[2]?s[2]="".concat(t," and ").concat(s[2]):s[2]=t),n.push(s))}},n}},379:(e,n,t)=>{var r,o=function(){var e={};return function(n){if(void 0===e[n]){var t=document.querySelector(n);if(window.HTMLIFrameElement&&t instanceof window.HTMLIFrameElement)try{t=t.contentDocument.head}catch(e){t=null}e[n]=t}return e[n]}}(),i=[];function a(e){for(var n=-1,t=0;t<i.length;t++)if(i[t].identifier===e){n=t;break}return n}function c(e,n){for(var t={},r=[],o=0;o<e.length;o++){var c=e[o],s=n.base?c[0]+n.base:c[0],u=t[s]||0,l="".concat(s," ").concat(u);t[s]=u+1;var d=a(l),f={css:c[1],media:c[2],sourceMap:c[3]};-1!==d?(i[d].references++,i[d].updater(f)):i.push({identifier:l,updater:h(f,n),references:1}),r.push(l)}return r}function s(e){var n=document.createElement("style"),r=e.attributes||{};if(void 0===r.nonce){var i=t.nc;i&&(r.nonce=i)}if(Object.keys(r).forEach((function(e){n.setAttribute(e,r[e])})),"function"==typeof e.insert)e.insert(n);else{var a=o(e.insert||"head");if(!a)throw new Error("Couldn't find a style target. This probably means that the value for the 'insert' parameter is invalid.");a.appendChild(n)}return n}var u,l=(u=[],function(e,n){return u[e]=n,u.filter(Boolean).join("\n")});function d(e,n,t,r){var o=t?"":r.media?"@media ".concat(r.media," {").concat(r.css,"}"):r.css;if(e.styleSheet)e.styleSheet.cssText=l(n,o);else{var i=document.createTextNode(o),a=e.childNodes;a[n]&&e.removeChild(a[n]),a.length?e.insertBefore(i,a[n]):e.appendChild(i)}}function f(e,n,t){var r=t.css,o=t.media,i=t.sourceMap;if(o?e.setAttribute("media",o):e.removeAttribute("media"),i&&"undefined"!=typeof btoa&&(r+="\n/*# sourceMappingURL=data:application/json;base64,".concat(btoa(unescape(encodeURIComponent(JSON.stringify(i))))," */")),e.styleSheet)e.styleSheet.cssText=r;else{for(;e.firstChild;)e.removeChild(e.firstChild);e.appendChild(document.createTextNode(r))}}var p=null,v=0;function h(e,n){var t,r,o;if(n.singleton){var i=v++;t=p||(p=s(n)),r=d.bind(null,t,i,!1),o=d.bind(null,t,i,!0)}else t=s(n),r=f.bind(null,t,n),o=function(){!function(e){if(null===e.parentNode)return!1;e.parentNode.removeChild(e)}(t)};return r(e),function(n){if(n){if(n.css===e.css&&n.media===e.media&&n.sourceMap===e.sourceMap)return;r(e=n)}else o()}}e.exports=function(e,n){(n=n||{}).singleton||"boolean"==typeof n.singleton||(n.singleton=(void 0===r&&(r=Boolean(window&&document&&document.all&&!window.atob)),r));var t=c(e=e||[],n);return function(e){if(e=e||[],"[object Array]"===Object.prototype.toString.call(e)){for(var r=0;r<t.length;r++){var o=a(t[r]);i[o].references--}for(var s=c(e,n),u=0;u<t.length;u++){var l=a(t[u]);0===i[l].references&&(i[l].updater(),i.splice(l,1))}t=s}}}}},o={};function i(e){var n=o[e];if(void 0!==n)return n.exports;var t=o[e]={id:e,exports:{}};return r[e](t,t.exports,i),t.exports}i.n=e=>{var n=e&&e.__esModule?()=>e.default:()=>e;return i.d(n,{a:n}),n},i.d=(e,n)=>{for(var t in n)i.o(n,t)&&!i.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:n[t]})},i.o=(e,n)=>Object.prototype.hasOwnProperty.call(e,n),e=i(379),n=i.n(e),t=i(426),n()(t.Z,{insert:"head",singleton:!1}),t.Z.locals,console.log(1),console.log(2)})();

  번들링 결과파일은 위와 같다. 

 

결과

  css-loader는 CSS를 JavaScript로 변환하며, style-loader는 css-loader로 가져온 코드를 웹 페이지 내의 style 태그로 추가한다. 따라서 반드시 css-loader > style-loader 순으로 적용되어야 한다.

 

rules: [
  {
    test: /\.css$/,
    use: [
      { loader: 'style-loader' },
      {
        loader: 'css-loader',
        options: { modules: true },
      },
    ],
  },

  로더에 옵션을 주고 싶다면 위와 같이 options를 이용하면 된다. 웹팩에서 사용하는 로더에 대한 자세한 사항은 https://webpack.js.org/loaders/ 페이지를 참고하자.

 

4. Plugin

const path = require('path');
const webpack = require('webpack');
module.exports = {
  /*생략...*/
  module: {
    rules: [ { test: /\.css$/, use: ['style-loader', 'css-loader'] } ],
  },
  plugins: [
    new webpack.BannerPlugin({
      banner: "ihl's code",
    }),
  ],
};

  Plugin은 변환 외에 압축, 복사, 폴더 제거 등의 부가적인 기능을 제공한다. 그 중 BannerPlugin은 결과물에 빌드정보 등추가적인 정보를 입력할 수 있다. 나는 결과물에 ihl's code라는 문자열을 추가해보려고 한다.

 

  결과 폴더에 entry_bundle.js.LICENSE.txt라는 파일이 새롭게 생성되었다. 안에는 내가 입력한 ihl's code 라는 문자열이 포함되어있으며, entry_bundle.js에도 코드 맨 위에 For license information please see entry_bundle.js.LICENSE.txt라는 주석이 추가되어있다.

 

  이 외에도 환경 정보를 정의하는 DefinePlugin, 폴더를 제거하는 CleanWebpackPlugin, HTML 템플릿을 지정하여 동적으로 HTML 파일을 생성하는 HTMLTemplatePlugin, 웹팩의 번들링 소요시간을 측정해주는 speed-measure-webpack-plugin 등 다양한 플러그인이 존재한다.

 

 


생활코딩 웹팩: https://opentutorials.org/module/4566

웹팩 공식홈페이지: https://webpack.js.org/concepts/

Naver D2 - webpack: https://d2.naver.com/helloworld/0239818

Toast - 번들러: https://ui.toast.com/fe-guide/ko_BUNDLER

모듈: https://ko.javascript.info/modules-intro