ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • JavaScript | Webpack 기초 정리
    JavaScript/JavaScript 2020. 4. 23. 14:46

    Webpack

    웹팩은 모듈 번들러로, 각각의 리소스 파일을 합쳐서 사용자에게 전달하기 좋은 형태로 만들어주는 역할을 함

    모듈(module): 각 리소스 파일
    번들(bundle): 웹팩 실행 후 나오는 결과 파일

    웹팩이 필요한 이유

    • SPA(단일 페이지 애플리케이션) 등장 이후 하나의 HTML이 수많은 자바스크립트 파일을 포함하기 때문에
      기존의 방식으로 자바스크립트 리소스를 관리하기가 어려워짐
    • HTML에서 모든 자바스크립트 리소스를 관리하면 계속 늘어나는 파일을 관리하기도 어렵고,
      실행 순서도 신셩써야 하며, 기존에 생성된 전역 변수를 덮어쓸 수도 있는 위험이 발생
    • script 태그로 외부 모듈을 불러오는 경우 주소에 오타가 있거나, 해당 사이트에 문제가 있는 경우 모듈 요청 실패
    • 외부 모듈이 필요없어지면 모든 자바스크립트 코드에서 해당 모듈의 코드를 제거할 때도 문제가 생길 수 있음
    • 외부 모듈이 필요없어진 경우 깜빡하고 script 태그를 지우지 않으면 불필요한 리소스의 다운로드가 발생

    웹팩 실행하기

    실습 프로젝트 생성

    $ mkdir webpack-init
    $ cd webpack-init
    $ npm init -y
    $ npm install webpack webpack-cli
    • webpack-cli를 이용하면 CLI(command line interface)에서 웹팩 실행 가능
    • create-react-app, next.js 등의 프레임워크에서는 웹팩을 세밀하게 다뤄야 하므로
      webpack-cli를 이용하지 않고 코드에서 직접 실행

    모듈 파일 생성

    프로젝트 루트에 src 폴더를 만들고 그 아래 util.js 생성

    export function sayHello(name) {
      console.log("hello", name);
    }

    모듈 사용 파일 생성

    src 폴더 밑에 index.js 파일 생성 후 아래 코드 작성

    import { sayHello } from "./util";
    
    function myFunc() {
      sayHello("mike");
      console.log("myFunc");
    }
    myFunc();

    실행 및 결과

    $ npx webpack
    • 웹팩을 실행하면 dist 폴더가 생기고 그 아래 main.js 번들 파일이 생성됨
    • index.js 모듈과 util.js 파일이 main.js 번들 파일로 합쳐짐
    • 별다른 설정 없이 웹팩을 실행하면 ./src/index.js 모듈을 입력받아 ./dist/main.js 번들 파일을 출력

    설정 파일 이용하기

    설정 파일 생성

    프로젝트 루트에 wepack.config.js 파일 생성 후 아래 코드 작성

    const path = require("path");
    
    module.exports = {
      entry: "./src/index.js",
      output: {
        filename: "main.js",
        path: path.resolve(__dirname, "dist"),
      },
      mode: "production",
      optimization: { minimizer: [] },
    };
    • entry: 입력 파일 경로 지정
    • output: 번들 파일(결과 파일) 경로/이름 지정
    • mode: 프로덕션 모드로 설정하면 자바스크립트 코드 압축을 포함한 여러가지 최적화 기능이 기본으로 적용됨
    • optimization: 여기서는 번들 파일의 내용을 쉽게 확인하기 위해 압축하지 않도록 설정

    실행 및 결과

    $ npx webpack
    /******/ (function(modules) { // webpackBootstrap
    /******/     // The module cache
    /******/     var installedModules = {};
    /******/
    /******/     // The require function
    /******/     function __webpack_require__(moduleId) {
    /******/
    /******/         // Check if module is in cache
    /******/         if(installedModules[moduleId]) {
    /******/             return installedModules[moduleId].exports;
    /******/         }
    /******/         // Create a new module (and put it into the cache)
    /******/         var module = installedModules[moduleId] = {
    /******/             i: moduleId,
    /******/             l: false,
    /******/             exports: {}
    /******/         };
    /******/
    /******/         // Execute the module function
    /******/         modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
    /******/
    /******/         // Flag the module as loaded
    /******/         module.l = true;
    /******/
    /******/         // Return the exports of the module
    /******/         return module.exports;
    /******/     }
    /******/
    /******/
    /******/     // expose the modules object (__webpack_modules__)
    /******/     __webpack_require__.m = modules;
    /******/
    /******/     // expose the module cache
    /******/     __webpack_require__.c = installedModules;
    /******/
    /******/     // define getter function for harmony exports
    /******/     __webpack_require__.d = function(exports, name, getter) {
    /******/         if(!__webpack_require__.o(exports, name)) {
    /******/             Object.defineProperty(exports, name, { enumerable: true, get: getter });
    /******/         }
    /******/     };
    /******/
    /******/     // define __esModule on exports
    /******/     __webpack_require__.r = function(exports) {
    /******/         if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
    /******/             Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
    /******/         }
    /******/         Object.defineProperty(exports, '__esModule', { value: true });
    /******/     };
    /******/
    /******/     // create a fake namespace object
    /******/     // mode & 1: value is a module id, require it
    /******/     // mode & 2: merge all properties of value into the ns
    /******/     // mode & 4: return value when already ns object
    /******/     // mode & 8|1: behave like require
    /******/     __webpack_require__.t = function(value, mode) {
    /******/         if(mode & 1) value = __webpack_require__(value);
    /******/         if(mode & 8) return value;
    /******/         if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
    /******/         var ns = Object.create(null);
    /******/         __webpack_require__.r(ns);
    /******/         Object.defineProperty(ns, 'default', { enumerable: true, value: value });
    /******/         if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
    /******/         return ns;
    /******/     };
    /******/
    /******/     // getDefaultExport function for compatibility with non-harmony modules
    /******/     __webpack_require__.n = function(module) {
    /******/         var getter = module && module.__esModule ?
    /******/             function getDefault() { return module['default']; } :
    /******/             function getModuleExports() { return module; };
    /******/         __webpack_require__.d(getter, 'a', getter);
    /******/         return getter;
    /******/     };
    /******/
    /******/     // Object.prototype.hasOwnProperty.call
    /******/     __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
    /******/
    /******/     // __webpack_public_path__
    /******/     __webpack_require__.p = "";
    /******/
    /******/
    /******/     // Load entry module and return exports
    /******/     return __webpack_require__(__webpack_require__.s = 0);
    /******/ })
    /************************************************************************/
    /******/ ([
    /* 0 */
    /***/ (function(module, __webpack_exports__, __webpack_require__) {
    
    "use strict";
    // ESM COMPAT FLAG
    __webpack_require__.r(__webpack_exports__);
    
    // CONCATENATED MODULE: ./src/util.js
    function sayHello(name) {
      console.log("hello", name);
    }
    
    // CONCATENATED MODULE: ./src/index.js
    
    
    function myFunc() {
      sayHello("mike");
      console.log("myFunc");
    }
    myFunc();
    
    
    /***/ })
    /******/ ]);
    • ./dist/main.js 번들 파일 전체가 즉시 실행 함수(Immediately Invoked Function Expression)로 묶여 있음
    • var installedModules = {}와 webpack_require 함수는 웹팩 런타임 코드로,
      설정 파일에서 entry를 여러 개 입력하면 각 entry에 의해 생성되는 번들 파일에는 웹팩 런타임 코드가 들어감
    • index.js와 util.js에 작성한 코드는 즉시 실행 함수의 매개변수로 입력됨
    • window, global 등의 전역 변수를 사용하는지, commonJS, AMD 등의 모듈 시스템을 사용하는지의 여부에 따라
      번들 파일의 내용은 달라질 수 있음

    로더 사용하기

    로더(loader)는 모듈을 입력으로 받아서 원하는 형태로 변환한 후 새로운 모듈을 출력해주는 함수

    자바스크립트 파일뿐만 아니라, 이미지, CSS, CSV 등 모든 파일은 모듈이 될 수 있음

    실습 프로젝트 생성

    $ mkdir webpack-loader
    $ cd webpack-loader
    $ npm init -y
    $ npm install webpack webpack-cli

    babel-loader 자바스크립트 파일 처리하기

    babel-loader를 사용하기 위해 다음의 패키지들 설치

    자바스크립트 코드에서 JSX문법으로 작성된 리액트 코드를 처리하기 위해 필요한 패키지들

    $ npm install babel-loader @babel/core @babel/preset-react react react-dom

    JSX 문법 사용 파일 생성

    프로젝트 루트에 src 폴더 생성후 index.js 파일 작성

    import React from "react";
    import ReactDOM from "react-dom";
    
    function App() {
      return (
        <div className="container">
          <h3 className="title">webpack example</h3>
        </div>
      );
    }
    
    ReactDOM.render(<App />, document.getElementById("root"));

    바벨 프리셋 설정

    루트에 babel.config.js 파일을 만들고 @babel/preset-react 프리셋 설정

    const presets = ["@babel/preset-react"];
    module.exports = { presets };

    babel-loader 설정

    루트에 webpack.config.js 파일 생성후 아래와 같이 작성

    const path = require("path");
    
    module.exports = {
      entry: "./src/index.js",
      output: {
        filename: "main.js",
        path: path.resolve(__dirname, "dist"),
      },
      module: {
        rules: [
          {
            test: /\.js$/,
            exclude: /node_modules/,
            use: "babel-loader",
          },
        ],
      },
      mode: "production",
    };
    • module: js확장자를 갖는 모듈은 babel-loader가 처리하도록 설정

    실행 및 결과

    $npx webpack

    App 컴포넌트를 렌더링할 HTML 파일 작성

    dist 폴더 밑에 index.html 파일 생성 후 아래 코드 작성

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
      </head>
      <body>
        <div id="root"></div>
        <script src="main.js"></script>
      </body>
    </html>
    • index.html 파일을 실행해보면 'webpack example'이 화면에 출력되는 것을 볼 수 있음
    • babel-loader를 설정하지 않고 웹팩을 실행하면 웹팩이 JSX문법을 이해하지 못하기 때문에 에러가 발생함

    css-loader CSS 파일 처리하기

    설치

    $ npm install css-loader

    CSS 파일 생성

    src 폴더 밑에 App.css 파일을 만들고 다음 내용 입력

    .container {
      border: 1px solid blue;
    }
    .title {
      color: red;
    }
    • index.js 파일 상단에
      import Style from './App.css';
      console.log({Style}); 코드 추가
    • 현재는 CSS 모듈을 처리하는 로더가 없기 때문에 웹팩 실행 시 에러 발생

    css-loader 설정

    webpack.config.js 파일에 아래 코드 추가

      module: {
        rules: [
        // ...
          { test: /\.css$/, use: "css-loader" },
        ],
        // ...
    • css 확장자를 갖는 모듈은 css-loader가 처리하도록 설정
    • 웹팩 실행 후 index.html을 브라우저에서 확인하면 콘솔에서는 Style 객체를 확인할 수 있지만
      실제로 적용 되지는 않은 모습을 볼 수 있음

    style-loader

    스타일을 실제로 적용하기 위해서는 style-loader가 필요

    설치

    $ npm install style-loader

    style-loader 설정

    webpack.config.js 파일의 내용을 다음과 같이 수정

    {
        test: /\.css$/,
        use: ["style-loader", "css-loader"],
    },
    • 로더를 배열로 입력하면 오른쪽 로더부터 실행됨
    • style-loader는 css-loader가 생성한 CSS 데이터를 style 태그로 만들어서
      번들 파일이 브라우저에서 실행될 때 HTML head에 삽입
    • 따라서 번들 파일이 실행되다가 에러가 발생하면 style 태그가 삽입되지 않을 수 있음
    • css-module 기능을 이용하면 스타일 코드 지역화 가능
      css-module은 css-loader가 제공하는 기능이며 css-loader는 이 외에도
      CSS코드에서 사용된 @import, url() 등의 처리를 도와줌

    실행 및 결과

    $ npx webpack
    • 웹팩 실행 후 index.html 파일을 브라우저에서 열어보면 스타일이 적용된 것을 확인할 수 있음

    기타 파일 처리하기

    파일 생성

    • src 폴더 밑에 임의의 PNG 파일을 icon.png 파일로 저장

    • 같은 폴더에 data.txt 파일을 만들고 아무 내용이나 입력

    • data.json 파일도 만든 후 아래와 같이 작성

    {
        "name": "mike",
        "age": 23
    }

    ./src/index.js에 파일 추가

    // ...
    import Icon from "./icon.png";
    import Json from "./data.json";
    import Text from "./data.txt";
    
    function App() {
      return (
        <div className="container">
          <h3 className="title">webpack example</h3>
          <div>{`name: ${Json.name}, age: ${Json.age}`}</div>
          <div>{`text: ${Text}`}</div>
          <img src={Icon} alt="" />
        </div>
      );
    }
    
    ReactDOM.render(<App />, document.getElementById("root"));
    
    • JSON, TXT, PNG 모듈 사용
    • JSON 모듈은 웹팩에서 기본적으로 처리해주기 때문에 별도의 로더를 설치하지 않아도 됨

    TXT, PNG 로더 설치

    $ npm install file-loader raw-loader
    • file-loader는 모듈의 내용을 그대로 복사해서 dist 폴더 밑에 복사본을 생성하고
      모듈을 사용하는 쪽에는 해당 모듈의 경로를 넘겨줌
    • raw-loader는 모듈의 내용을 그대로 자바스크립트 코드로 가져옴

    TXT, PNG 로더 설정

    webpack.config.js 파일에 file-loader와 raw-loader 설정 코드 추가

     module: {
        rules: [
          // ...
          {
            test: /\.(png|jpg|gif)$/,
            use: "file-loader",
          },
          {
            test: /\.txt$/,
            use: "raw-loader",
          },
          // ...
    • PNG 모듈은 file-loader가 처리하도록 설정
    • TXT 모듈은 raw-loader가 처리하도록 설정

    실행 및 결과

    $ npx webpack
    • 웹팩 실행 후 dist 폴더에 생성된 이미지 파일의 이름에는 해시값이 포함
    • 해시값은 이미지 파일을 수정하는 경우에만 변경되기 때문에 사용자에게 전달된 이미지 파일은 브라우저의 캐싱 효과를 최대한 활용할 수 있음
    • 브라우저에서 index.html 을 열어보면 결과가 잘 나오는 것을 확인할 수 있음

    이미지 파일 요청 횟수 줄이기

    이미지 파일을 번들 파일에 포함시키면 브라우저의 파일 요청 횟수를 줄일 수 있음
    그러나 번들 파일의 크기가 너무 커지면 자바스크립트가 늦게 실행되므로
    작은 이미지 파일만 포함시키는 게 좋음

    url-loader 설치

    $ npm install url-loader

    url-loader 설정

    webpack.config.js 파일에서 이전에 작성했던 file-loader 설정을 지우고 다음과 같이 url-loader 설정으로 변경

    // ...
          {
            test: /\.(png|jpg|gif)$/,
            use: [
              {
                loader: "url-loader",
                options: {
                  limit: 8192,
                },
              },
            ],
          },
    // ...
    • url-loader는 파일 크기가 limit 값보다 작은 경우에는 번들 파일에 파일의 내용을 포함시킴
    • 만약 파일의 크기가 limit보다 큰 경우에는 다른 로더가 처리할 수 있도록 fallback 옵션을 제공함
    • fallback 옵션을 입력하지 않으면 기본적으로 file-loader가 처리하도록 되어 있음

    실행 및 결과

    limit 옵션에 icon.png 보다 큰 값을 입력하고 웹팩 실행

    $ npx webpack
    • 브라우저에서 img 태그의 src 속성값을 확인해 보면 파일의 경로가 아니라 데이터가 입력된 것을 확인할 수 있음
    • 웹팩에서 권장하는 최대 용량은 244KiB로 초과 시 경고 메세지가 콘솔에 출력됨

    플러그인 사용하기

    플러그인은 로더보다 강력한 기능을 가짐
    로더는 특정 모듈에 대한 처리만 담당하는 반면
    플러그인은 웹팩이 실행되는 전체 과정에 개입

    실습 프로젝트 생성

    $ mkdir webpack-plugin
    $ cd webpack-plugin
    $ npm init -y
    $ npm install webpack webpack-cli

    간단한 리액트 코드 작성

    프로젝트 루트에 src 폴더를 만들고 그 아래 index.js 파일 생성

    import React from "react";
    import ReactDOM from "react-dom";
    
    function App() {
      return (
        <div>
          <h3>안녕하세요, 웹팩 플러그인 예제입니다.</h3>
          <p>html-webpack-plugin 플러그인을 사용합니다.</p>
        </div>
      );
    }
    
    ReactDOM.render(<App />, document.getElementById("root"));

    리액트 코드 컴파일을 위한 패키지 설치

    $ npm install @babel/core @babel/preset-react babel-loader react react-dom

    웹팩 설정

    프로젝트 루트에 webpack.config.js 파일 생성 후
    웹팩 기본 설정과 babel-loader 사용 설정

    const path = require("path");
    
    module.exports = {
      entry: "./src/index.js",
      output: {
        filename: `[name].[chunkhash].js`,
        path: path.resolve(__dirname, "dist"),
      },
      module: {
        rules: [
          {
            test: /\.js$/,
            use: {
              loader: "babel-loader",
              options: {
                presets: ["@babel/preset-react"],
              },
            },
          },
        ],
      },
    };
    • chunkhash를 사용하면 파일 내용이 수정될 때마다 파일 이름이 변경되도록 할 수 있음
    • 자바스크립트 모듈을 처리하기 위해 babel-loader 설정
    • babel.config.js 파일로 바벨을 설정할 수도 있지만 babel-loader에서 직접 바벨을 설정할 수도 있음

    html-webpack-plugin

    이전의 예제에서는 웹팩의 결과 파일을 확인하기 위해서 HTML 파일을 수동으로 작성했는데, 여기서는 번들 파일 이름에 chunkhash 옵션을 적용했기 때문에 파일의 내용이 변경될 때마다 HTML 파일의 내용도 수정해야 함
    이 작업을 자동으로 하는 플러그인이 html-webpack-plugin임

    설치

    $ npm install clean-webpack-plugin html-webpack-plugin
    • clean-webpack-plugin은 웹팩을 실행할 때마다 dist폴더를 정리함
      여기서는 번들 파일의 내용이 바뀌면 이름도 바뀌기 때문에 이전에 생성된 번들 파일을 처리하는 용도로 사용

    웹팩 플러그인 설정

    webpack.config.js 파일에 다음 설정 추가

    const path = require("path");
    const { CleanWebpackPlugin } = require("clean-webpack-plugin");
    const HtmlWebpackPlugin = require("html-webpack-plugin");
    
    module.exports = {
          // ...
        plugins: [
          new CleanWebpackPlugin(),
          new HtmlWebpackPlugin({
            template: "./template/index.html",
          }),
        ],
        // ...
    • 웹팩이 실행될 때마다 dist 폴더를 정리하도록 clean-webpack-plugin 설정
    • index.html 파일이 자동으로 생성되도록 html-webpack-plugin 설정
    • template: 원하는 형태를 기반으로 index.html 파일이 생성되도록 template 옵션 설정

    template 파일 생성

    루트에 template 폴더를 만들고 그 아래 index.html 파일 생성
    html-webpack-plugin이 생성하는 HTML에 포함시킬 내용을 index.html파일에 추가

    <html>
      <head>
        <title>
          웹팩 플러그인 예제
        </title>
      </head>
      <body>
        <div id="root"></div>
      </body>
    </html>

    실행 및 결과

    $ npx webpack
    <html>
      <head>
        <title>웹팩 플러그인 예제</title>
      </head>
      <body>
        <div id="root"></div>
        <script src="main.8f5ec2b09655bece5bef.js"></script>
      </body>
    </html>
    • ./dist/index.html 파일을 열어 보면 번들 파일이 script 태그로 등록됨
    • 브라우저에서 실행해보면 정상적으로 작동하는 것을 알 수 있음

    DefinePlugin

    모듈 내부에 있는 문자열을 대체해 주는 플러그인
    웹팩에 내장된 플러그인이기 때문에 별도의 설치가 필요 없음

    대체할 문자열 index.js 파일에 추가

    // ...
        <div>
          {/* ... */}
          <p>{`앱 버전은 ${APP_VERSION}입니다.`}</p>
          <p>{`10 * 10 = ${TEN * TEN}`}</p>
        </div>
    // ...
    • APP_VERSION, TEN 문자열을 원하는 문자열로 대체할 예정

    웹팩 설정

    // ...
    const webpack = require("webpack");
    
    module.exports = {
      // ...
      plugins: [
        // ...
        new webpack.DefinePlugin({
          APP_VERSION: '"1.2.3."',
          TEN: "10",
        }),
        // ...
    • DefinePlugin은 웹팩 모듈에 포함되어 있음
    • APP_VERSION 문자열을 '1.2.3' 으로 대체
    • TEN 문자열을 10으로 대체

    실행 및 결과

    $ npx webpack
    l.a.createElement(
      "div",
      null,
      l.a.createElement("h3", null, "안녕하세요, 웹팩 플러그인 예제입니다."),
      l.a.createElement("p", null, "html-webpack-plugin 플러그인을 사용합니다."),
      l.a.createElement("p", null, "앱 버전은 1.2.3.입니다."),
      l.a.createElement("p", null, "10 * 10 = 100")
    );
    • 웹팩 실행 후 번들 파일을 열어 보면 코드가 압축된 상태라 확인이 쉽지 않지만 위처럼 문자열이 변경되어 있는 것을 볼 수 있음
    • 브라우저에서 확인해 봐도 같은 결과가 출력됨

    ProvidePlugin

    미리 설정한 모듈을 자동으로 등록해 주는 플러그인
    웹팩에 기본으로 내장되어있기 때문에 별도의 설치가 필요 없음

    React에서 자주 사용되는 모듈

    import React from 'react'
    • JSX 문법을 사용하면 리액트 모듈을 사용하지 않는 것처럼 보이지만
      바벨이 JSX 문법을 React.createElement 코드로 변환해 주기 때문에 리액트 모듈이 필요함
    • 따라서 JSX 문법을 사용하는 파일을 작성한다면 React를 import 해주어야 함

    import문 주석 처리

    index.js 파일의 React import문을 주석 처리

    // import React from "react";
    // ...

    웹팩 설정

    webpack.config.js 파일에 ProvidePlugin을 설정하는 코드 추가

    // ...
      plugins: [
        // ...
        new webpack.ProvidePlugin({
          React: "react",
        }),
      ],
    // ...
    • React 모듈을 ProvidePlugin 설정에 추가

    실행 및 결과

    $ npx webpack
    • 웹팩을 실행해 보면 에러 없이 번들 파일이 생성됨
    • 브라우저에서 역시 문제 없이 화면이 출력됨
    • index.js에서 주석 처리한 것을 그대로 두고 webpack.config.js에서 ProvidePlugin을 제거한 뒤 웹팩을 실행하면
      번들 파일이 생성되기는 하지만 브라우저에서 확인하면 화면이 출력되지 않는 것을 볼 수 있음

    참고도서

    • [ 실전 리액트 프로그래밍 / 저자_이재승 / 출판사_프로그래밍 인사이트 ]

    이번엔 웹팩의 기초에 대해 공부해 보았습니다.

    웹팩은 여러 모듈들을 하나의 번들 파일로 만드는 역할을 하는데

    정확하고 효율적으로 설정할수록 그 가치는 더욱 높아질 것 같습니다.

    지금은 구조의 이해에 초점을 두고 공부해 나가야겠습니다.

    'JavaScript > JavaScript' 카테고리의 다른 글

    Redux | Redux 개요 및 주요 개념  (0) 2020.05.14
    JavaScript | Babel Plugin 제작하기  (0) 2020.04.22
    JavaScript | Babel 기초 정리  (0) 2020.04.22
    JavaScript | Event Handling 이벤트 처리  (0) 2020.04.09
    JavaScript | With문  (0) 2020.02.18

    댓글

Designed by Tistory.