본문 바로가기

Development/Web

Bundle 개념과 WebPack 사용 방법

들어가며

웹 페이지는 html, css, js, 이미지 파일과 같은 다양하고 많은 파일로 구성되어 있다. 이렇게 개발된 웹 페이지를 로딩하면 웹 페이지를 구성하는 파일들이 다운로드되는 것을 확인할 수 있다. 이와 같은 방법으로 웹 서버를 운영하면, 사용자 요청 수 증가에 따른 많은 비용이 발생, 성능 저하 등과 같은 문제가 발생할 수 있다. 이와 같은 문제를 해결하기 위해 등장한 개념이 Bundler이며, 단어의 의미 그대로 여러 파일을 묶어주는 개념이라고 할 수 있다. Bundler에 해당하는 도구로는 대표적으로 WebPack, Broserify, Parcel이 있는데, 이중에서 대중적으로 사용되는 WebPack에 대해서 공부한 내용을 정리해보았다.

module

웹팩이 어떠한 기능을 가져다주는지 이해하기 전에 간단한 예시를 통해 module에 대해서 먼저 알아보도록 하자. 예를 들어, hello.js와 world.js에 동일한 변수명 word을 통해 서로 다른 값을 h1 태그로 나타내보자.

// ./js/hello.js
let word = "Hello";

// ./js/world.js
let word = "World";
<!-- index.html -->

</head>
    <script src="./js/hello.js"></script>
    <script src="./js/world.js"></script>
</head>
<body>
    <h1 id="hello-world"></h1>
    <script>
        const h1 = document.querySelector("#hello-world");
        h1.innerHTML = word;
    </script>
</body>

위의 코드에서는 hello.js와 world.js를 통째로 불러들여 word 변수로 h1 태그에 텍스트를 입력하도록 하였다. 그 결과, world.js의 word는 이미 hello.js에서 선언되었기 때문에 해당 부분에서 오류를 발생시키고, h1에 'Hello'가 채워지는 것을 확인할 수 있다. 이번에는 아래 코드와 같이 작성하여 hello.js와 world.js를 서로 다른 모듈로 구분하고, 이를 통해 h1에 'Hello World'를 나타내도록 하였다.

// ./js/hello.js
let word = "Hello";
export default word;

// ./js/world.js
let word = "World";
export default word;
<!-- index.html -->

</head>
    <!-- <script src="./js/hello.js"></script> -->
    <!-- <script src="./js/world.js"></script> -->
</head>
<body>
    <h1 id="hello-world"></h1>
    <script type="module">
        import hello from './hello.js';
        import world from './world.js';

        const h1 = document.querySelector("#hello-world");
        h1.innerHTML = `${hello} ${world}`;
    </script>
</body>

먼저, hello.js와 world.js의 word 변수를 export ~로 모듈화하였고, 이를 index.html에서 각각 hello, world로 불러왔다. 그 결과 hello.js와 world.js는 같은 변수를 사용하고 있더라도 중첩되지 않기 때문에 정상적으로 'Hello World'가 나타난다. 또한, 웹 페이지의 콘솔창에서 word를 입력해보면 word가 정의되지 않았다는 오류 메시지를 볼 수 있는데, 이는 word라는 변수가 hello.js와 world.js에만 국한되며, index.html에서 hello.js, world.js를 불러온 순간부터는 word라는 변수가 각각 hello와 world라는 변수로 재설정되기 때문이다.

WebPack 등장

웹팩을 도입하면 코드의 동작은 그대로 유지하면서 코드의 구조는 더욱 효율적으로 개선할 수 있고, JS, CSS, 이미지와 같은 웹 페이지의 여러 구성 요소를 하나의 JS 파일로 묶을 수 있다(경우에 따라서는 이들을 각각의 파일로 분리할 수도 있다). 웹팩을 설치하기 위해서는 현재 진행 중인 프로젝트 폴더가 Node.js 패키지 프로젝트이어야 한다.

설치

(Node.js가 설치되어 있다는 가정 하에) 아래 명령어를 입력하여 현재 작업 중인 프로젝트 폴더를 Node.js 패키지 프로젝트로 설정해준다.

$ npm init

이어서 아래 명령어를 입력하여 webpack과 webpack-cli를 설치한다.

$ npm install -d webpack webpack-cli

실행

webpack을 실행하여 번들링하기 위해 먼저 위의 예시 코드 중 index.html의 body안에 작성한 javascript 코드를 js 폴더 안에 index.js 파일로 옮겨주자.

// ./js/index.js

import hello from './hello.js';
import world from './world.js';

const h1 = document.querySelector("#hello-world");
h1.innerHTML = `${hello} ${world}`;

위의 코드를 보면 알 수 있듯이, 현재 프로젝트의 메인이 되는 index.js에서 hello.js와 world.js를 불러들인 것을 확인할 수 있다. 이는 클라이언트가 index.js를 다운로드 받으면 hello.js와 world.js 또한 함께 다운로드 받아야 하는 상황임을 의미하는데, hello.js와 world.js를 index.js에 번들링하여 이를 해결해보도록 하자.


✔ webpack-cli

번들링된 결과를 public 폴더에 보관하기 위해 public 폴더를 생성한 후 아래와 같은 명령어를 입력한다.

$ npx webpack --entry ./js/index.js --output-path ./public --output-filename index_bundle.js 

이렇게 번들링된 index_bundle.js는 아래와 같은 코드로 작성되어 있으며, 이를 index.html에서 불러오면 div에 'Hello World'가 입력되어 있는 것을 확인할 수 있다.

// ./public/index_bundle.js

(()=>{"use strict";document.querySelector("#hello-world").innerHTML="Hello World"})();
<!-- index.html -->

<body>
    <h1 id="hello-world"></h1>
    <script src="./public/index_bundle.js"></script>
</body>

뿐만 아니라 개발자 도구에서 네트워크 탭의 내용을 살펴보면, 기존에는 index.html과 hello.js, world.js를 다운로드 받은 반면에, index.html과 index_bundle.js만 다운로드 받은 것을 확인할 수 있다.


✔ webpack.config.js

위에서는 webpack-cli를 통해 번들링하는 것을 다루어보았는데, 매번 번들링을 할 때마다 결코 짧지 않은 cli를 직접 입력해야 하는 불편함이 있다. 이와 같은 문제는 webpack의 config 파일을 통해 체계적이고, 간편하게 번들링할 수 있도록 설정하여 해결할 수 있다. 현재 프로젝트 폴더에 webpack.config.js라는 파일을 생성하고, 위에서 실행하였던 webpack-cli의 내용을 아래와 같은 코드로 작성한다.

// webpack.config.js

const path = require('path');

module.exports = {
    entry: "./js/index.js",
    output: {
        path: path.resolve(__dirname, 'public'),
        filename: "index_bundle.js"
    }
};

이렇게 설정을 마친 후에는 아래와 같이 비교적 간단한 cli를 입력하여 webpack을 실행시킬 수 있다.

$ npx webpack --config webpack.config.js

✔ npm start

바로 위에서 입력한 webpack-cli를 package.json 파일의 scripts에 추가하여 npm 명령어로 실행할 수도 있다. 먼저, package.json 파일의 scripts에 아래와 같은 코드를 추가한 후 저장한다.

{
  "scripts": {
    "bundle": "npx webpack --config webpack.config.js"
  }
}

그리고 터미널에서 npm run bundle을 입력하면 webpack이 실행되는 것을 확인할 수 있다.

$ npm run bundle

자동 실행

코드를 수정할 때마다 번들링 확인해야 하는 과정이 꽤 번거롭게 느껴질 수 있다. 웹팩에서는 파일의 변화를 감시하는 명령어가 있는데, 아래와 같이 --watch를 입력하면 코드가 수정될 때마다 자동으로 웹팩이 실행되도록 할 수 있다.

$ npx webpack --watch

Webpack 설정

mode

webpack.config.js에서 웹팩의 실행 모드를 설정할 수 있는데, 위의 코드와 같이 모드를 설정하지 않을 경우, production 모드로 번들링된다.

// webpack.config.js

const path = require('path');

module.exports = {
    // mode: "production",
    entry: "./js/index.js",
    output: {
        path: path.resolve(__dirname, 'public'),
        filename: "index_bundle.js"
    }
};

production 모드로 번들링된 코드는 아래와 같으며, 파일의 용량을 최소한으로 줄여 배포하기 위해 띄어쓰기, 줄바꿈, 들여쓰기 등이 전부 생략되어 가독성이 매우 떨어진다는 것을 확인할 수 있다.

// production 모드로 번들링된 파일

(()=>{"use strict";document.querySelector("#hello-world").innerHTML="Hello World"})();

위의 파일은 비교적 짧은 코드로 구성되어 있기에 가독성에 크게 영향을 받지 않지만, 수많은 기능의 코드가 번들링된 파일의 경우에는 알아보기 매우 어렵다. 만약, 번들링된 파일에 예기치 못한 오류가 발생하여 이를 확인해야 한다면 매우 난감할 것인데, 이와 같은 경우 development 모드로 웹팩을 실행하면 비교적 알아보기 쉬운 코드로 번들링된다.

// webpack.config.js

const path = require('path');

module.exports = {
    mode: "development",
    entry: "./js/index.js",
    output: {
        path: path.resolve(__dirname, 'public'),
        filename: "index_bundle.js"
    }
};

물론, development 모드로 번들링하여도 쉽게 알아보기에는 어렵겠지만, 띄어쓰기, 들여쓰기가 되어 있기 때문에 비교적 가독성이 높은 상태로 파일이 생성된다.

// production 모드로 번들링된 파일

/******/ (() => { // webpackBootstrap
/******/     "use strict";
/******/     var __webpack_modules__ = ({
// ( ...중략... )
/******/ })();

loader

현재까지는 js 파일을 웹팩으로 번들링하는 방법에 대해서 다루어보았다. 그렇다면 CSS 파일은 어떻게 번들링하는지에 대해서도 알아보아야 하는데, 이때 등장하는 개념이 로더이다. 웹팩 공식 사이트의 대문을 장식하는 아래의 그림을 살펴보면, 웹 페이지를 구성하는 여러 파일(js, hbs, png, sass 등)의 데이터는 번들링을 통해 js. css. jpg, png 파일로 변환된다는 것을 알 수 있다.

이처럼 웹팩은 js 뿐만 아니라 css나 이미지 파일까지도 번들링을 해주는 신박한 기능을 제공해주는데, 이는 로더를 통해 동작한다. 웹팩을 잘 다루는 지는 '로더를 얼마나 많이 알고 있는가', '로더를 얼마나 자유자재로 다룰 줄 아는가'에 따라 정해질 만큼 웹팩에서의 로더는 굉장히 중요하다. 예를 들어, css를 번들링해보도록 해보자. css를 번들링하기 위해서는 아래와 같이 두 개의 로더를 설치해야 한다.

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

설치를 마친 후에는 아래와 같이 webpack.config.js 파일에서 module 설정을 해준다.

// webpack.config.js

const path = require('path');

module.exports = {
    // ( ...중략... )
    module: {
        rules: [
            {
                test: /\.css$/,
                use: [
                    "css-loader"
                ]
            }
        ]
    }
};

테스트를 위해서 css 폴더에 style.css를 생성 후 아래와 같이 스타일 시트를 작성하였다.

/* ./css/style.css */

#hello-world {
    color: blue;
}

이어서, 위의 css 파일을 번들링할 js 폴더의 index.js로 불러들인 후 이를 출력하도록 하였다.

/* ./js/index.js */

import hello from './hello.js';
import world from './world.js';
import style from '../css/style.css';

const h1 = document.querySelector("#hello-world");
h1.innerHTML = `${hello} ${world}`;
console.log(style);

웹팩을 실행하고, 웹 페이지에 접속해보자. 그러면 스타일이 적용되지 않고, 콘솔창에 style.css의 내용이 Array로 출력된 것을 확인할 수 있다.

그 이유는 webpack.config.js의 module에 추가한 css-loader가 단순히 css 파일을 읽어오는 모듈이기 때문이다. 따라서, css-loader로 읽은 css를 스타일로 적용시켜주는 style-loader까지 module에 추가해주어야 한다.

// webpack.config.js

const path = require('path');

module.exports = {
    // ( ...중략... )
    module: {
        rules: [
            {
                test: /\.css$/,
                use: [
                    "style-loader",
                    "css-loader"
                ]
            }
        ]
    }
};

다시 웹팩을 실행하여 번들링하고 웹 페이지를 보면 스타일이 적용된 것을 확인할 수 있다. 이와 마찬가지로 이미지 등 다양한 형태의 파일에 해당하는 로더를 사용하여 번들링 할 수 있으며, 파일 형태별 로더 사용법은 웹팩 공식 페이지의 Loaders 문서에 기재되어 있다.

output

위에서는 웹팩으로 번들링한 파일을 하나의 파일(index_bundle.js)로 생성하였는데, 이번에는 번들링된 파일을 생성하기 위한 설정 방법에 대해서 알아보자. 만약, 우리가 웹 사이트가 index.html 뿐만 아니라 about.html도 있고, index.html에서 a 태그를 통해 about.html로 이동할 수 있다고 가정해보자.

<!-- index.html -->

<body>
    <h1 id="hello-world"></h1>
    <a href="./about.html">About</a>
    <script src="./public/index_bundle.js"></script>
</body>
// ./js/about.js

const h1 = document.querySelector("#about-title");
h1.innerText = "About";
<!-- about.html -->

<body>
    <h1 id="about-title"></h1>
    <a href="./index.html">Index</a>
    <script src="./public/about_bundle.js"></script>
</body>

이어서 index.html에서는 index_bundle.js를 불러오도록 하고, about.html에서는 about_bundle.js를 불러오도록록 하려고 한다. 즉, 한 번의 웹팩 실행으로 서로 다른 두 개의 파일을 생성하는 것이 목표이다. 이와 같이 번들링되는 파일이 2개 이상인 경우에는 webpack.config.js 파일의 entry를 아래와 같이 객체로 바꾸어준다.

const path = require('path');

module.exports = {
    mode: "production",
    entry: {
        index: "./js/index.js",
        about: "./js/about.js"
    },
    output: {
        path: path.resolve(__dirname, 'public'),
        filename: "index_bundle.js"
    },
    // ( ...후략... )
}

이대로 실행하면 index.js파일과 about.js파일이 하나의 파일(index_bundle.js)로 생성된다. 따라서, output되는 파일명이 entry 객체의 key로 지정되도록 아래와 같이 수정해준다.

const path = require('path');

module.exports = {
    mode: "production",
    entry: {
        index: "./js/index.js",
        about: "./js/about.js"
    },
    output: {
        path: path.resolve(__dirname, 'public'),
        filename: "[name]_bundle.js"
    },
    // ( ...후략... )
}

여기서 [name]은 entry 객체의 name(index, about)을 의미하며, 이를 변수로 사용하고자 할 때 이와 같이 나타낸다.

plugins

위의 index.html과 about.html에서는 각자 다른 js파일을 불러오도록 직접 설정하였다. 이번에는 웹팩 플러그인을 사용하여 번들링하는 동시에 index.html과 about.html에 각자 다른 js 파일을 불러오는 방법에 대해서 알아보겠다. 먼저, index.html과 about.html에서 각각 index_bundle.js와 about_bundle.js를 불러오는 부분을 지워주도록 하자.

<!-- index.html -->

<body>
    <h1 id="hello-world"></h1>
    <a href="./about.html">About</a>
    <!-- <script src="./public/about_bundle.js"></script> -->
</body>
<!-- about.html -->

<body>
    <h1 id="about-title"></h1>
    <a href="./index.html">Index</a>
    <!-- <script src="./public/about_bundle.js"></script> -->
</body>

그리고 웹팩 공식 사이트를 참고하여 HtmlWebpackPlugin을 설치한다.

$ npm install --save-dev html-webpack-plugin

웹팩을 실행할 때 플러그인이 실행되도록 하기 위해서 webpack.config.js에서 해당 플러그인을 불러오고, plugins에 추가한다.

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

module.exports = {
    mode: "production",
    entry: {
        index: "./js/index.js",
        about: "./js/about.js"
    },
    output: {
        path: path.resolve(__dirname, 'public'),
        filename: "[name]_bundle.js"
    },
    module: { ... },
    plugins: [new HtmlWebpackPlugin()]         
};

그리고 웹팩을 실행하면 public 폴더에 index.html 파일이 생성된 것을 확인할 수 있다. 그리고 index.html의 코드를 보면 index_bundle.js와 about_bundle.js를 모두 불러온 것을 확인할 수 있는데, 이는 우리가 원하는 결과가 아니다. index_bundle.js는 index.html에서만, about_bundle.js는 about.html에서만 불러오도록 설정하기 위해서는 webpack.config.js의 plugins를 아래와 같이 수정해주어야 한다.

{
    // ( ...생략... )
    plugins: [
        new HtmlWebpackPlugin({
            template: "./index.html",
            filename: "./index.html",
            chunks: ["index"]
        }),
        new HtmlWebpackPlugin({
            template: "./about.html",
            filename: "./about.html",
            chunks: ["about"]
        })
    ]
};

이를 자세히 나타내면 다음과 같다.

new HtmlWebpackPlugin({
    template: "번들할 html의 파일명",
    filename: "번들된 html의 파일명",
    chunks: ["html에 불러올 entry 객체의 key"]
})

마치며

한 때 React로 웹 개발을 공부하면서, webpack.config.js이라는 파일을 몇 번 본 적이 있는데, 가벼운 호기심 정도만 갖고 있을 뿐이었지, 크게 신경쓰지는 않았었다. 그러다가 우연히 유튜브를 보던 중에 Egoing님의 생활코딩 채널에서 WebPack에 관련된 영상이 추천 영상으로 올라와서 정주행하였고, 공부한 내용을 개발일지로 정리하게 되었다. 또 공부하고 싶은 내용 중에 Docker가 있는데, 생활코딩 채널에 Docker에 관련된 재생목록이 있는 것을 확인하였고, 조만간 Docker 기초 내용을 공부하고 이를 개발일지로 정리하겠다.