번들링과 웹팩

ZeroJun·2022년 7월 25일
4

React

목록 보기
10/13

번들링

번들링이란 여러 제품이나 코드, 프로그램을 묶어서 패키지로 제공하는 행위를 의미한다.
프론트엔드 개발자에게 번들은 "사용자에게 웹 애플리케이션을 제공하기 위한 묶음"이다. 사용자가 브라우저를 열고 주소를 입력하면, 해당 주소에서 프론트엔드 개발자가 번들링한 여러 파일을 받는다. 이 파일을 브라우저가 실행하여 멋진 웹 애플리케이션을 사용자에게 제공하게 된다.

그렇다면 작성한 HTML, CSS, JavaScript 파일을 그대로 전송하는 것에는 어떤 문제가 있을까?

  • 두 개의 .js파일에서 같은 변수를 사용하고 있어서, 변수간 충돌이 일어난다.
  • 딱 한번 불러오는 프레임워크 코드가 8MB라서, 인터넷 속도가 느린 국가의 모바일 환경에서 사용자가 불편을 호소한다.
  • 배포 코드가 너무 읽기 쉬워 개발을 할 줄 아는 사용자가 프론트엔드 애플리케이션을 임의로 조작하여 피해가 발생한다.

이러한 문제로 인해 소프트웨어를 잘 만들어도 사용자에게 배포하기 위한 번들링이 꼭 필요하다.

Webpack

Webpackdms 2022년 7월 현재 프론트엔드 애플리케이션 배포를 위해서 가장 많이 사용하는 번들러다. 실리콘벨리나 국내 IT 대기업을 막론하고 프론트엔드 애플리케이션을 대규모 유저에게 제공하기 위해 가장 많이 사용하는 방법이다.

Webpack은 여러 개의 파일을 하나의 파일로 합쳐주는 모듈 번들러를 의미한다. 모듈 번들러란 HTML, CSS, JavaScript 등의 자원을 전부 각각의 모듈로 보고 이를 조합해 하나의 묶음으로 번들링하는 도구다.

모듈 번들러(Module Bundler)의 등장

모던 웹으로 발전하면서 JavaScript 코드의 양이 절대적으로 많이 증가했고, 또 대규모의 의존성 트리를 가지고 있는 대형 웹 애플리케이션이 등장함으로써 세분화된 모듈 파일이 폭발적으로 증가했다. 이 모듈 단위의 파일들을 호출을 해 브라우저에 띄워야 하는데, JavaScript의 특성에 따라 발생하기 쉬운 각 변수들의 스코프 문제를 해결해야 하고, 각 자원을 호출할 때에 생겨나는 네트워크 쪽의 코스트도 신경써줘야만 했다.

그래서 이런 복잡성에 대응하기 위해 하나의 시작점(Ex. React App의 index.js 등)으로부터 의존성을 가지는 모듈을 추적하여 하나의 결과물을 만들어내는 모듈 번들러가 등장했다.

Webpack에서의 모듈

Webpack에서의 모듈은 JavaScript의 모듈에만 국한하지 않는다. HTML, CSS, 혹은 .jpg나 .png 같은 이미지 파일들도 전부 포함한 포괄적인 개념이다. 따라서 Webpack은 주요 구성 요소인 로더(loader)를 통해 다양한 파일도 번들링이 가능하다.

빌드와 번들링

빌드는 개발이 완료된 앱을 배포하기 위해 하나의 폴더 (directory)로 구성하여 준비하는 작업을 말한다. React앱을 기준으로 설명하면, npm run build를 실행하면 React build작업이 진행되고, index.html 파일에 압축되어 배포에 최적화된 상태를 제공해준다.

번들링은 말 그대로 묶음의 개념이다. 파일을 묶는 작업 그 자체를 말하며 파일은 의존적 관계에 있는 파일들(import, export) 그 자체 혹은 내부적으로 포함되어 있는 모듈을 의미한다. 정확히 말하면 모듈 간의 의존성 관계를 파악해 그룹화 하는 작업이라고 볼 수 있다.

Webpack의 필요성

Webpack이 필요한 가장 큰 이유는 웹 애플리케이션의 빠른 로딩속도와 높은 성능을 위해서다. 웹페이지를 구성하는 코드의 양이 많은 것을 "무겁다"라고 표현하는데, 이것이 무거우면 무거울수록 웹 페이지의 로딩 속도와 성능은 저하된다. 일반적으로 유저는 하나의 웹사이트에 접근하는 순간부터 3초 이내에 웹페이지가 뜨지 않으면 굉장히 많은 수가 더는 기다리지 않고 이탈을 선택한다.

만약 Webpack이 없다면 각 자원들을 일일히 서버에 요청해 얻어와야 하지만, 있다면 같은 타입의 파일들은 묶어서 요청 및 응답을 받을 수 있기 때문에 네트워크 코스트가 획기적으로 줄어든다.

또한 Webpack loaader를 사용하면 일부 브라우저에서 지원하지 않는 JavaScript ES6의 문법들을 ES5로 변환해주는 babel-loader를 사용할 수 있게 된다.

그리고 Webpack4 버전 이상부터는 Develoment, Production 두 가지의 모드를 지원한다. 여기서 Production모드로 번들링을 진행할 경우, 코드 난독화, 압축, 최적화(Tree Shanking)작업을 지원하기도 한다. 한마디로 사용화 된 프로그램을 사용자가 느끼기에 더욱 쾌적한 환경 및 보안까지 신경쓰면서 노출시킬 수 있다는 점에서도 Webpackdml 필요성은 굉장히 높은 편이다.

웹팩의 핵심 개념

웹팩 공식문서에서는 아래 항목을 핵심 개념으로 제안하고 있다. 아래 개념에 대해 제대로 이해하고 있어야 한다.

  • Entry
  • Output
  • Loaders
  • Plugins
  • Mode
  • Broswer compatibility

아래 코드는 webpack의 config 파일 예시다.

module.exports = {
  target: ["web", "es5"],
  entry: "./src/script.js",
  output: {
    path: path.resolve(__dirname, "docs"),
    filename: "app.bundle.js",
    clean: true
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, "css-loader"],
        exclude: /node_modules/,
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, "src", "index.html"),
    }),
    new MiniCssExtractPlugin(),
  ],
  optimization: {
    minimizer: [
      new CssMinimizerPlugin(),
    ]
  }
};

Target

Webpack은 다양한 환경과 target을 컴파일한다. target의 기본값은 web이다. 적용하지 않으면 이 기본값으로 적용된다.이 부분에는 web외에도 다양한 환경을 컴파일 할 수 이쓴ㄴ데, exX를 넣으면 지정된 ECMAScript버전으로 컴파일할 수 있다.

module.exports = {
	target: ["web", "es5"], 
}

해당 config 파일에서는 es5를 배열 안에 넣었다. 따라서 이 config파일은 브라우저와 동일한 환경에서 사용하기 위하여 컴파일할 것이며, 작성된 코드를 es5버전으로 컴파일 하겠다고 지정한 것임을 알 수 있다. Browser Compatibility와 연관된 속성으로 볼 수 있다.

Entry

일반적인 문맥에서 entry의 뜻은 "입구"다. 박물관 입구, 놀이동산 입구 등 문맥에서 사용되는 영어단어다. webpack에서의 entry는 프론트엔드 개발자가 작성한 코드의 "시작점"으로 이해하면 편하다. React도 index.js에서 HTML 엘리먼트 하나에 React코드를 적용하는 것 부터 시작한다. (실제 webpack을 사용하기도 했다.)

Entry속성은 Entry point라고도 하며, webpack이 내부의 디펜던스 그래프를 생성하기 위해 사용해야 하는 모듈이다. Webpack은 이 Entry point를 기반으로 직간접적으로 의존하는 다른 모듈과 라이브러리를 찾아낼 수 있다.

//기본 값
module.exports = {
	...
  entry: "./src/index.js",
}

//지정 값
module.export = {
  ...
  entry: "./src/script.js",
}

기본값 외에 webpack설정에서 이런식으로 entry속성을 설정하여 다른 entry point를 지정할 수 있다.

Output

Output속성은 생성된 번들을 내보낼 위치와 이 파일의 이름을 지정하는 방법을 webpack에 알려주는 역할을 한다.

const path = require('path');

module.exports = {
 ...
 output: {
  path: path.resolve(__dirname, "docs"), // 절대 경로로 설정해야 한다.
  filename: "app.bundle.js",
  clean: true
 },
};

기본 출력 파일의 경우에는 ./dist/main.js로, 생성된 기타 파일의 경우에는 ./dist폴더로 설정된다. 위의 예제에서는 output.filename과 output.path속성을 사용하여 webpack에 번들의 이름과 내보낼 위치를 알려주고 있다. path속성을 사용할 때는 path 모듈을 사용해야만 한다.

Loader(로더)

Webpack은 기본적으로 JavaScript와 JSON파일만 이해한다. 그러나 loader를 사용하면 Webpack이 다른 유형의 파일을 처리하거나, 그들을 유효한 모듈로 변환해 애플리케이션에 사용하거나 디펜던시 그래프에 추가할 수 있다.

module.exports = {
  ...
  module: {
   rules : [
     {
       test: /\.css$/,
       use: [MiniCssExtractPlugin.loader, "css-loader"],
       exclude: /node_modules/,
     },
   ],
  },
};

상위 수준에서 loader는 webpack 설정에 몇 가지 속성을 가진다.

  • test : 변환이 필요한 파일들을 식별하기 위한 속성
  • use : 변환을 수행하는데 사용되는 로더를 가리키는 속성
  • exclude : 바벨로 컴파일하지 않을 파일이나 폴더를 지정. (반대로 include속성을 이용해 반드시 컴파일해야 할 파일이나 폴더 지정 가능)

여기서 test와 use속성은 필수 속성이다. 이런 속성을 넣어 규칙을 정하기 위해서는 module.rules아래에 정의해야한다. 그저 rules아래에 정의하면 webpack은 경고를 하게 된다.

Plugins

Plugins를 사용하면 번들을 최적화하거나 에셋을 관리하고, 또는 환경변수 주입 등 광범위한 작업을 수행할 수 있게 된다.

const webpack = require('webpack');
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = {
  ...
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, "src", "index.html"),
    }),
    new MiniCssExtractPlugin(),
  ],
};

플러그인을 사용하기 위해서는 require()를 통해 플러그인을 먼저 요청해야 한다. 그리고 plugins배열에 사용하고자 하는 플러그인을 추가해야 한다. 대부분의 플러그인은 사용자가 옵션을 통해 지정할 수 있다. 다른 목적으로 플러그인을 여러 번 사용하도록 설정할 수 있기 때문에 new연산자를 사용해 호출하여 플러그인의 인스턴스를 만들어줘야 한다.

위의 예제에서 html-webpack-plugin은 생성된 모든 번들을 자동으로 삽입하여 애플리케이션용 HTML파일을 생성해준다. mini-css-extract-plugin은 css를 별도의 파일로 추출해 css를 포함하는 js파일 당 css파일을 작성해주게끔 지원한다.

Optimization

Webpack은 버전 4부터 선택한 항목에 따라 최적화를 실행한다.

module.exports = {
  ...
  optimization: {
    minimizer: [
      new CssMinimizerPlugin(),
    ]
  }
};

최적화하기 위해 다양한 옵션이 지원되는데, 대표적으로 minimize와 minimizer등을 사용한다.

  • minimize : TerserPlugin또는 optimization.minimize에 명시된 plugins로 bundle파일을 최소화(=압축)시키는 작업 여부를 결정

-minimizer : default minimizer를 커스텀된 TerserPlugin 인스턴스를 제공해서 재정의할 수 있다.

위의 예제에서는 mini-css-extract-plugin에 관련된 번들을 최소화하도록 지시하고 있다.

번들링 연습해보기

once method를 활용하여 Lorem Ipsum을 딱 한번만 출력하는 애플리케이션을 웹팩을 이용하여 빌드해보기

디렉터리 생성

cd ~/Desktop
mkdir fe-sprint-webpack-tutorial
cd fe-sprint-webpack-tutorial

npm init

npm init -y

npm을 활용할 준비를 한다. 현재 디렉터리 상황에 맞게 package.json파일이 생성된다.

{
  "name": "fe-sprint-webpack-tutorial",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

index.js파일 생성

// index.js
const shout = (...sentences) => console.log(...sentences);

자체 함수 require하여 사용해보기

// src/underbar.js
const _ = {
  once(func) {
    // 아래 변수들은 아래 선언/리턴되는 함수 안에서 참조.
    // 리턴되는 함수의 scope 내에 존재하므로, 리턴되는 함수를 언제 실행해도 이 변수들에 접근할 수 있다.
    let result;
    let alreadyCalled = false;

    return function (...args) {
      if (!alreadyCalled) {
        alreadyCalled = true;
        result = func(...args);
      }
      return result;
    };
  },
};

module.exports = _; // 다른 파일에서 사용할 수 있게 export
// src/index.js
const _ = require('./underbar.js')

const shout = (...sentences) => console.log(...sentences);

const loremIpsum = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis egestas feugiat elit, ac tincidunt neque vestibulum at. Mauris a eros sit amet urna efficitur tempus."

const shoutOnce = _.once(shout);

shoutOnce(loremIpsum);
shoutOnce(loremIpsum);
shoutOnce(loremIpsum);
shoutOnce(loremIpsum);

loremIpsum을 여러 번 출력시도해도 한번만 출력된다.

웹팩 설치하기

지금까지의 결과물을 하나로 합치기 위해 웹팩을 이용한다. 웹팩을 사용하기 위해서는 npm으로 webpack, webpack-cil를 설치해야 한다. webpack은 이 프로젝트를 번들링하기 위한 라이브러리지만, 실제 프로젝트에 사용하지 않기 때문에 devDependency 옵션을 설정하고 사용한다.

npm install -D webpack webpack-cli

webpack은 우선 번들링을 원하는 파일을 먼저 확인하고, import한 라이브러리나 코드가 있으면 해당 코드도 모두 인식하여 하나의 번들 안으로 모두 넣는다. 여기서 번들링을 원하는 파일의 위치를 entry, 번들링의 결과물을 output이라고 한다.

웹팩 config파일 작성

webpack 설정 파일인 webpack.config.js 파일에 entry와 output 정보를 아래와 같이 적을 수 있다. 최근 webpack은 설정파일 없이도 작동할 수 있게 업데이트 되었지만, webpack 설정을 다룰 줄 알아야 향후 더욱 다양한 외부 리소스를 사용하기 편하다.

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

module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'), // './dist'의 절대 경로를 리턴합니다.
    filename: 'app.bundle.js',
  },
};

번들링하기

번들링을 하기 위해서는 아래와 같은 명령어를 입력해야 한다. 해당 명령어를 입력하고 번들링 결과를 확인한다.

npx webpack

dist/app.bundle.js에 아래와 같이 이상한 코드가 나오면 성공이다. webpack은 사용자가 프론트엔드 웹 애플리케이션을 빠르게 전달받을 수 있게 코드를 최소화(minifiy)한다. 이렇게 minify한 코드는 악의적으로 편집하기 어렵기도 하다.

이 코드를 실행하면 결과가 src/index.js와 같다.

npm run build 설정하기

다른 개발자와 협업하는 경우, 지금까지의 여정을 간단하게 대신할 수 있는 스크립트를 만들어두면 좋다. npm script를 새롭게 만든다. 아래와 같이 작성하면 이제 npm run build 스크립트로 언제든 번들링할 수 있다.

{
  "name": "fe-sprint-webpack-tutorial",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
		"build": "webpack", // here
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "devDependencies": {
    "webpack": "^5.73.0",
    "webpack-cli": "^4.10.0"
  }
}

webpack의 주요컨셉 #1

지금은 entry 경로의 index.js파일이 필요한 디펜던시가 underbar.js 하나밖에 없다. 이런 경우에는 사실 번들링은 불필요하다. 그러나 온전한 프론트엔드 애플리케이션을 위해 필요한 라이브러리는 가면 갈수록 많아지기 때문에, 이를 잘 번들링할 수 있어야 한다. 웹팩 공식문서의 아래 도면이 이를 잘 보여준다.

브라우저에서 작동하도록 제작하기

index.html생성

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Shout Lorem Ipsum</title>
</head>
<body>
  <div id="app"></div>
  <script src="index.js"></script>
</body>
</html>

src/index.html을 실행해보면 아무것도 뜨지 않고 error가 난다.

require문법은 Node.js문법으로 브라우저에서 지원하지 않기 때문이다. 사실 create-react-app도 webpack을 사용한다. Node.js에서만 작동할 법한 코드를 모든 브라우저에서 잘 작동할 수 있도록 번들링을 해주었기 때문에 잘 작동했던 것이다. index.html을 dist 디렉터리로 옮겨서 번들파일과 연결시켜보자.

<!-- dist/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Shout Lorem Ipsum Once</title>
</head>
<body>
  <div id="app"></div>
  <script src="app.bundle.js"></script>
</body>
</html>

src/index.js도 단순히 cosole.log만 찍기 보다는, dom관련 코드를 좀더 추가해서 브라우정서 볼 수 있게 수정한다.

const _ = require('./underbar.js');

const shout = (...sentences) => console.log(...sentences);
const shoutToHTML = (...sentences) => {
  const app = document.querySelector('#app');
  app.append(...sentences.map(sentence => {
    const shoutHere = document.createElement('div');
    shoutHere.className = 'shout';
    shoutHere.textContent = sentence;
    return shoutHere;
  }))
  return;
};

const loremIpsum = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis egestas feugiat elit, ac tincidunt neque vestibulum at. Mauris a eros sit amet urna efficitur tempus."

const shoutOnce = _.once(shout);
const shoutToHTMLOnce = _.once(shoutToHTML);

shoutOnce(loremIpsum);
shoutOnce(loremIpsum);
shoutOnce(loremIpsum);
shoutOnce(loremIpsum);

shoutToHTMLOnce(loremIpsum);
shoutToHTMLOnce(loremIpsum);
shoutToHTMLOnce(loremIpsum);
shoutToHTMLOnce(loremIpsum);

이렇게 src/index.js 에서 새롭게 작성한 코드는 dist의 app.bundle.js에 적용되지 않았기 때문에, 다시 npm run build명령어로 번들링을 진행하고 나서 dist/index.html을 열면 아래와 비슷한 화면을 확인할 수 있다.

웹페이지가 밋밋하니 H1 요소를 추가하고 CSS를 적용하자.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="style.css">
  <title>Shout Lorem Ipsum Once</title>
</head>
<body>
  <main>
    <h1>Shout Lorem Ipsum Once</h1>
    <div id="app"></div>
  </main>
  <script src="app.bundle.js"></script>
</body>
</html>
/* dist/style.css */
* {
  box-sizing: border-box;
  border: 0;
  padding: 0;
  margin: 0;
}

main {
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: column;
}

div.shout {
  padding: 12px;
  margin: 4px;
  border-radius: 8px;
  border: 0.5px solid gray;
}

webpack은 이렇게 JavaScript와 CSS를 함께 배포해야할 때, 좀더 쉽게 배포할 수 있게 로더(loader)를 제공한다. create-react-app에서 자유롭게 css를 import할 수 있었던 이유도 webpack에서 css를 불러올 수 있는 툴을 사용했기 때문이다.

dist/style.css 파일의 위치를 src/style.css로 옮기고 파일을 index.js에서 require("./style.css"); 로 불러와지는지 확인하면 node.js는 그 자체만으로는 css를 읽을 수 없어 문법 에러가 발생한다.

webpack도 마찬가지다. 친절하게도 아래와 같은 에러 메세지를 출력한다.

You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders

이 파일을 다루기 위해서는 적절한 로더가 필요한 것 같다. 현재 이 파일을 처리하기 위한 로더가 설정되어 있지 않다. 에러에서 제공한 공식문서 링크에서 아래의 로더 목록을 찾을 수 있다. 이번에 필요한 로더는 style-loader, css-loader다.

css-loader는 css를 js파일 내에서 불러올 수 있고, style-lodaer는 불러온 css를 style요소 내에 담아준다.

npm으로 두 로더를 설치하고, webpack.config.js를 조정한다.

npm i -D css-loader style-loader
// webpack.config.js
const path = require("path");

module.exports = {
  entry: "./src/index.js",
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "app.bundle.js",
  },
  module: {
    rules: [
      {
				// 파일명이 .css로 끝나는 모든 파일에 적용
        test: /\.css$/,
				// 배열 마지막 요소부터 오른쪽에서 왼쪽 순으로 적용
				// 먼저 css-loader가 적용되고, styled-loader가 적용되어야 한다.
				// 순서 주의!
        use: ["style-loader", "css-loader"],
				// loader가 node_modules 안의 있는 내용도 처리하기 때문에
				// node_modules는 제외해야 합니다
        exclude: /node_modules/,
      },
    ],
  },
};

이후 빌드를 하면 번들링이 성공한다. warning이 있어도 에러가 없으면 성공이다.

app.bundle.js파일을 열어보면 style 요소를 자동 생성하고, 지금까지 작성한 css를 넣는 방식이다. 실제 앱을 동작시켜보면 스타일이 잘 적용되어 있고, 이제 html에 css를 불러오는 link 태그는 필요없어졌다.

webpack의 주요 컨셉 #2

loader는 JavaScript, Json이 아닌 파일을 불러오는 역할을 한다.

HTML도 번들에 포함시키기

먼저 dist/index.html을 src 디렉터리로 옮긴다.

.
├── dist
│   └── app.bundle.js
├── package-lock.json
├── package.json
├── src
│   ├── index.html
│   ├── index.js
│   ├── style.css
│   └── underbar.js
└── webpack.config.js

이번에는 html-webpack-plugin을 설치한다. 플러그인은 로더처럼 자바스크립트 파일로 불러오는 작업은 아니지만, 번들링에 유용한 다양한 툴을 적용할 수 있다.

npm i -D html-webpack-plugin

설치 후 webpack.config.js파일에 해당 플러그인을 적용한다. 잘 적용되었으면, npm run build로 결과를 확인한다.

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

module.exports = {
  entry: "./src/index.js",
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "app.bundle.js",
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ["style-loader", "css-loader"],
        exclude: /node_modules/,
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, "src", "index.html"),
    }),
  ],
};
// 노드 스크립트의 __dirname은 현재 JavaScript 파일이 있는 폴더의 경로를 반환한다.

아래와 같이 src/index.html파일을 번들링하여 dist/index.html 파일을 새로 생성한 것을 볼 수 있다.

최소화한 HTML을 보기 쉽게 바꾸면 아래와 비슷한 index.html 파일을 확인할 수 있다. CSS는 app.bundle.js 파일에서 넣어주고, JavaScript는 html-webpack-plugin이 자동으로

<script defer="defer" src="app.bundle.js"></script>

위의 요소를 추가해준 모습을 볼 수 있다.

<!doctype html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>Shout Lorem Ipsum Once</title>
  <script defer="defer" src="app.bundle.js"></script>
</head>

<body>
  <main>
    <h1>Shout Lorem Ipsum Once</h1>
    <div id="app"></div>
  </main>
</body>

</html>

Webpack의 주요 컨셉 #3

  • 플러그인은 번들링 과정 중에 개발자가 원하는 다양한 작업을 할 수 있도록 도와준다.
  • 그 중 html-webpack-plugin은 번들링 과정 중 html 파일을 자신이 원하는 형태로 가공하여 번들에 포함할 수 있게 돕는다.

로더와 다르게, 플러그인은 명칭 하나로 해당 플러그인의 역할을 파악하기 애매하다. 이런 경우는 사용량을 기준으로 많이 사용하는 플러그인이 왜 많이 사용되는지 먼저 찾아보는 방식으로 학습의 우선순위를 정하는게 좋다. 물론 사용량이 높다고 자신이 원하는 플러그인이 아닐 수도 있다. 개발환경을 구성할 때, 내가 필요한게 무엇인지 정확하게 구분하는게 중요한 이유다.

플러그인은 각자 기능이 천차만별이기 때문에, 내가 정말 필요한지 알아보고 적용해야 한다. 여러 기술을 혼합해서 쓰다가 필요없는 파일을 생성하거나 효율성이 떨어지는 코드를 생성하여 번들에 넣는 경우가 없어야 한다.

❗️기존 프로젝트 번들링 후 깃허브 페이지 배포하기

배포를 위한 환경설정

먼저 프로젝트 환경 구축을 위해 설정한 것들은 다음과 같다.

// package.json 폴더 생성
npm init -y

// 웹팩 설치 (실제 서비스에선 필요 없는 코드이므로 개발모드)
npm install -D webpack webpack-cli

// 번들링시 자바스크립트파일에 css코드를 삽입하기 위한 로더 설치
npm i -D css-loader style-loader

// 자바스크립트가 아닌 html파일을 번들링 하기 위한 html 플러그인 설치
npm i -D html-webpack-plugin

npm run build를 통해 손쉽게 빌드를 하기 위해 package.json에 다음을 추가한다.

  "scripts": {
    "build": "webpack", // 추가
    "test": "echo \"Error: no test specified\" && exit 1"
  },

설치한 옵션들을 적용하기 위해 webpack.config.js를 다음과 같이 작성한다.

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

module.exports = {
  entry: "./src/index.js",
  output: {
    path: path.resolve(__dirname, "docs"), // './dist'의 절대 경로를 리턴합니다.
    filename: '[name].bundle.js', // 파일name동적생성
    clean: true, // 빌드전 빌드폴더 정리
  },
  module: {
    rules: [
      {
        // 파일명이 .css로 끝나는 모든 파일에 적용
        test: /\.css$/,
        // 배열 마지막 요소부터 오른쪽에서 왼쪽 순으로 적용
        // 먼저 css-loader가 적용되고, styled-loader가 적용되어야 한다.
        // 순서 주의!
        use: ["style-loader", "css-loader"],
        // loader가 node_modules 안의 있는 내용도 처리하기 때문에
        // node_modules는 제외해야 합니다
        exclude: /node_modules/,
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, "src", "index.html"),
    }),
  ],
};

여기서 원래 webpack의 기본 Output 폴더명은 dist지만 깃허브 페이지 배포를 위해 docs로 변경해준다. 깃허브 페이지는 저장소의 루트 경로에서 /docs 디렉토리를 통해 저장소의 내용을 /docs 디렉토리를 통해 관리하기 때문이다.

그 후 css와 각종 js파일을 모두 index.js에 포함시킨다. (이렇게 포함시킨 후 앱을 실행하면 에러가 날 것이다. require는 본래 node의 문법이고 브라우저에서 실행시킬 수 없기 때문이다.)
html에는 모든 js파일이나 css파일 참조를 제거한다. (plug in에서 index.html을 포함시켜준 것과 같으므로 index.js도 제거한다.)

// index.js
require("./style.css");
const agoraStatesDiscussions = require("./data.js");

그 밖에 웹팩을 강력하게 사용하기 위해선 아래 링크를 참조한다.

https://webpack.kr/configuration/mode/

포함시키는 코드 작성 후 웹팩을 통해 빌드한 후 폴더 구조는 다음과 같다.

.
├── docs
│   ├── app.bundle.js
│   └── index.html
├── package-lock.json
├── package.json
├── src
│   ├── data.js
│   ├── index.html
│   ├── index.js
│   └── style.css
└── webpack.config.js

번들링 후 app.bundle.js에 style코드와 data가 포함되어 있는 것을 볼 수 있다.

이제 docs폴더의 index.html을 실행하면 기존 프로젝트와 같은 화면을 확인할 수 있다.

개발서버 설정

웹팩을 통해 배포하기 위한 코드를 작성하면 require등의 코드가 브라우저에서 실행되지 않는 등의 문제 때문에 실시간으로 개발 상황을 확인하기 불편하다. 이 때 웹팩에서 제공하는 개발서버 기능을 활용하면 된다.

먼저 개발서버 기능을 설치한다.

npm install -D webpack-dev-server

webpack.config.js에 다음을 추가하여 개발 서버에 파일위치를 알려준다.

  mode: "development", // mode에 대한 것은 공식문서 참조 (이것을 production으로 설정하면 웹팩 애러가 뜬다. 개발 서버를 사용할 땐 development로 두자.
  devServer: {
    static: './docs',
  },
   
  // 단일 HTML 페이지에 하나 이상의 엔트리 포인트가 있는 경우
  // optimization.runtimeChunk: 'single' 을 추가한다.
  optimization: {
    runtimeChunk: 'single',
  },

간편한 서버 실행을 위해 package.json에 스크립트를 추가한다.

"start": "webpack serve --open",

이제 npm start를 통해 서버를 띄운 후 개발중인 파일을 수정 후 저장하면 자동으로 빌드되어 서버에 반영된다.

배포하기

우선 이렇게 작성한 코드를 깃허브에 push한다. git ignore을 통해 node_module등 저장소에 올라갈 필요 없는 것은 제외시켜주자.

그 후 리포지토리의 setting의 Code and automation메뉴에서 Pages를 클릭하면 아래와 같은 화면이 나온다.

배포할 브랜치와 디렉토리를 설정한 후 save를 누른다.(지금은 main 브랜치의 docs폴더가 될 것이다.)

save후 조금 기다리면 아래와 같은 표시가 뜬다. (이 표시가 떠야 배포가 된 것이고, 이전까지는 회색 ready상태로 뜬다.)

배포된 url은 다음과 같다.

https://jiyj725.github.io/practice-bundling/

배포된 사이트의 element를 확인하면 html에서 오직 빌드한 js파일만 참조하는 것을 확인할 수 있다.

개발 / 배포 모드에 따른 config 나누기

개발과 배포시 webpack을 다르게 적용하고 싶은 경우 다음과 같이 처리할 수 있다.

base config만들기



const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");

module.exports = {
  target: ["web", "es5"],
  entry: "./src/script.js",
  output: {
    path: path.resolve(__dirname, "docs"),
    filename: "[fullhash].bundle.js", // filename랜덤적용
    clean: true
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, "css-loader"],
        exclude: /node_modules/,
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, "src", "index.html"),
    }),
    new MiniCssExtractPlugin(),
  ],
  optimization: {
    minimizer: [
      new CssMinimizerPlugin(),
    ],
  },
};

webpack-merge를 활용하여 dev모드와 prod모드 config만들기

npm install --save-dev webpack-merge
const { merge } = require('webpack-merge')
const baseConfig = require('./webpack.config.base')

module.exports = merge(baseConfig, {
  mode: 'development', // dev모드
  devServer: {
    port: 3001
  }
})
const { merge } = require('webpack-merge')
const baseConfig = require('./webpack.config.base')

module.exports = merge(baseConfig, {
  mode: 'production' // prod모드
})

package.json에서 개발서버 구동시, 배포시 scripts 설정

  "scripts": {
    "build": "webpack --config webpack.config.prod.js",
    "dev": "webpack-dev-server --open --config webpack.config.dev.js"
  },

이렇게 하면 개발할땐 npm run dev로 개발 서버를 구동시키고, 배포할 땐 npm run build를 하게 되는데 이때 적용되는 webpack모드가 상황에 맞게 달라지게 된다.

웹팩과 리액트

리액트는 점점 복잡해지는 프론트엔드 개발의 여러 문제를 해결하기 위해서 생겨났다. 2010년대 당시 주류였던 앵귤러는 하나의 프레임워크로서 정형화되고 체계화된 프론트엔드 개발 경험을 제공해서 많은 환경을 받았다. 다만, 프레임워크라는 점 때문에 기본적으로 필요한 코드의 양이 많았고, 배우는데 필요한 시간도 오래 걸렸고, 번들 사이즈가 커지고 성능 문제도 점점 커져나가고 있었다.

리액트는 당시 앵귤러의 단점을 보완할 수 있는 대체재로서 뷰와 함께 거론되기 시작했다. 리액트가 주목받던 이유 중 하나가 "프레임워크가 아니고 라이브러리"라는 점 때문이었다. 프론트엔드 개발에 꼭 필요한 점 말고는 코드를 추가하지 말고, 더 필요한 것이 있으면 개발자가 설치하라는 의견을 제시했다.

리액트 개발진은 개발자가 어떤 기술 스택을 사용할지 미리 가정하지 않는다. 그래서 개발자가 새로운 코드를 다시 작성할 필요 없이 기능을 추가할 수 있다.

당시 앵귤러의 갑작스러운 버전 변경과 논란이 겹쳐 리액트의 이런 자유도는 많은 프론트엔드 개발자의 관심을 받았다. 반대로 이런 특성 때문에 생기는 문제도 많아지는데, "리액트"만 알아서는 개발하기가 어렵다는 점이다. react, react-dom, react-scripts, create-react-app, react-router-dom, storybook, styled-component등 부가 라이브러리가 많다는 것이 예시다.

이렇게 알아야 하는 점이 많아지다 보니, 리액트 개발진은 이런 문제를 한 번에 해결할 수 있는 create-react-app이라는 툴 체인을 개발하여 초급 리액트 개발자가 쉽게 리액트에 접근할 수 있도록 했다. 리액트를 "간단하게"시작하기 위해 create-react-app에서 사용되는 툴 목록은 어마어마하다. create-react-app의 큰 부분인 react-scripts에 사용되는 라이브러리 목록만 봐도 알 수 있다. (여기서도 웹팩이 사용된다.)

{
	// ... 
  "dependencies": {
    "@babel/core": "^7.16.0",
    "@pmmmwh/react-refresh-webpack-plugin": "^0.5.3",
    "@svgr/webpack": "^5.5.0",
    "babel-jest": "^27.4.2",
    "babel-loader": "^8.2.3",
    "babel-plugin-named-asset-import": "^0.3.8",
    "babel-preset-react-app": "^10.0.1",
    "bfj": "^7.0.2",
    "browserslist": "^4.18.1",
    "camelcase": "^6.2.1",
    "case-sensitive-paths-webpack-plugin": "^2.4.0",
    "css-loader": "^6.5.1",
    "css-minimizer-webpack-plugin": "^3.2.0",
    "dotenv": "^10.0.0",
    "dotenv-expand": "^5.1.0",
    "eslint": "^8.3.0",
    "eslint-config-react-app": "^7.0.1",
    "eslint-webpack-plugin": "^3.1.1",
    "file-loader": "^6.2.0",
    "fs-extra": "^10.0.0",
    "html-webpack-plugin": "^5.5.0",
    "identity-obj-proxy": "^3.0.0",
    "jest": "^27.4.3",
    "jest-resolve": "^27.4.2",
    "jest-watch-typeahead": "^1.0.0",
    "mini-css-extract-plugin": "^2.4.5",
    "postcss": "^8.4.4",
    "postcss-flexbugs-fixes": "^5.0.2",
    "postcss-loader": "^6.2.1",
    "postcss-normalize": "^10.0.1",
    "postcss-preset-env": "^7.0.1",
    "prompts": "^2.4.2",
    "react-app-polyfill": "^3.0.0",
    "react-dev-utils": "^12.0.1",
    "react-refresh": "^0.11.0",
    "resolve": "^1.20.0",
    "resolve-url-loader": "^4.0.0",
    "sass-loader": "^12.3.0",
    "semver": "^7.3.5",
    "source-map-loader": "^3.0.0",
    "style-loader": "^3.3.1",
    "tailwindcss": "^3.0.2",
    "terser-webpack-plugin": "^5.2.5",
    "webpack": "^5.64.4",
    "webpack-dev-server": "^4.6.0",
    "webpack-manifest-plugin": "^4.0.2",
    "workbox-webpack-plugin": "^6.4.1"
  },
  "devDependencies": {
    "react": "^18.0.0",
    "react-dom": "^18.0.0"
  },
  "optionalDependencies": {
    "fsevents": "^2.3.2"
  },
  "peerDependencies": {
    "react": ">= 16",
    "typescript": "^3.2.1 || ^4"
  },
  "peerDependenciesMeta": {
    "typescript": {
      "optional": true
    }
  }
}

반대로, 사용자에게 최적의 번들을 제공하기 위한 전문 프론트엔드 개발자들은 이런 create-react-app의 거대한 라이브러리 목록을 줄이고자 직접 웹팩을 설치하여 하나씩 리액트와 그에 필요한 라이브러리 설정을 하기 시작했다. 자기에게 필요한 부분만 콕 집어서 개발하고자 했던 것이다.

리액트는 프론트엔드 라이브러리로서 최소한의 기능을 제공하고자 가볍게 만들어졌지만, 시간이 지나면서 아이러니하게도 개발자의 다양한 니즈를 충족시키기 위해 더 많은 라이브러리를 필수적으로 사용해야만 했고, 개발자가 필요한 이러저런 라이브러리를 골라서 번들링할 수 있는 웹팩이 필요하게 되었다.

리액트 개발에 꼭 필요한 라이브러리

react, react-dom

리액트 컴포넌트와 Hooks, 라이프 사이클에 대한 정보가 모두 들어있는 리액트와 이 리액트 코드를 브라우저에 보여줄 수 있는 react-dom은 꼭 필요하다.

babel

React를 학습하기 전, JSX부터 배워야 했다. 그런데, 브라우저에서 JavaScript는 읽을 수 있지만 JSX는 읽을 수 없다. 그렇다면 지금까지 React를 JSX로 작성해왔는데, 어떻게 브라우저에서 내가 만든 React 애플리케이션을 볼 수 있었을까? create-react-app에 포함되어 있는 balel이 jsx를 js로 변경해주어 번들링 해줬기 때문이다. 참고로 balbel은 JSX를 JavaScript로 변경하여 entry에서 불러올 수 있게 만들어줬기 때문에 로더의 일종으로 볼 수 있다.

css-loader

create-react-app으로 만들어진 애플리케이션을 보면 import 'aaa.css'와 같이 입력해도 css가 적용되던 것을 알 수 있다. 우리가 배웠던 css-loader가 필요하다는 것을 쉽게 알 수 있다.

리액트 개발에 도움이 되는 라이브러리

react-hot-reloader

react-hot-reloader는 webpack-dev-server처럼 저장할 때 마다 변경사항을 개발 환경에 적용해주는 라이브러리다. 추가적인 특징이 있다면 react-hot-reloader는 리액트 상태를 유지시켜준다.

eslint

eslint는 JavaScript로 개발 시 자주 접하는 에러를 방지하기 위한 린터다. eslint역시 많은 config와 plugin이 있는데, 이를 잘 조합하면 리액트에서 자주 접하는 에러를 미리 발견하는데 도움이 된다.

prettier

prettier는 JavaScript로 개발 시 통일성 있게 코드 형식을 맞출 수 있게 도와주는 툴이다. eslint와 조합해서 통일된 코드 형식까지 강요할 수도 있다

❗️웹팩을 통한 리액트 개발환경 구성 및 번들링

기본 환경 설정

// package.json 폴더 생성
npm init -y


// React 필수 라이브러리 설치
npm install react react-dom

// babel 설치
npm install -D @babel/core babel-loader @babel/preset-react @babel/preset-env 
- @babel/core : es6 -> es5
- @babel/preset-react : jsx -> javascript
- @babel/prest-env : es6 -> es5, 브라우저에 따라 알아서 컴파일
- babel-loader : html webpack plugin, js->es5(webpack, babel preset/plugin을 통해), jsx -> javascript

// 웹팩 설치 (실제 서비스에선 필요 없는 코드이므로 개발모드)
npm install -D webpack webpack-cli webpack-dev-server html-webpack-plugin
- webpack : 모든 리액트 파일을 웹팩을 통해 컴파일된 하나의 자바스크립트 파일에 넣는다.
- webpack-cli : build 스크립트를 통해 webpack 커맨드를 사용한다.
- html-webpack-plugin : html파일을 번들링 한다.

// 번들링시 자바스크립트파일에 css코드를 삽입하기 위한 로더 설치
npm i -D css-loader style-loader

// css최적화 플러그인 (나중에 정확히 알아보자)
npm install -D mini-css-extract-plugin css-minimizer-webpack-plugin


// ESLint 설치
npm install -D eslint
npm install -D -E prettier
npm install eslint-plugin-prettier eslint-config-prettier
- eslint-plugin-prettier
- ESLint와 Prettier이 충돌할 수 있는 설정들을 비활성화 시켜줍니다.
- eslint-config-prettier
- ESLint의 포맷 기능이 아닌 Prettier의 포맷 기능을 사용하게 만들어 줍니다.

eslintrc, Prettierrc 파일생성

// .eslintrc.js
module.exports = {
    root: true,
    env: {
        browser: true,
        es6: true,
    },
    extends: [
        "plugin:@typescript-eslint/recommended",
        // typescript 표준 규칙 모음
        "plugin:import/errors",
        "plugin:import/warnings",
        "plugin:import/typescript",
        // import 관련 규칙 모음

        "plugin:prettier/recommended",
        "prettier/@typescript-eslint",
        "prettier/react",
        // prettier 관련 규칙 모음
    ],
    parserOptions: {
        ecmaVersion: 2018,
        project: ["./tsconfig.json"],
        // tsconfig 파일의 경로를 참조 해줍니다.
        // 기준은 root 입니다.
    },
    rules: {
        // 추가하고 싶은 rule을 더 추가해줍니다.
    },
};
// .Prettierrc.js
module.exports = {
    endOfLine: "lf",
    tabWidth: 4,
    semi: true,
    singleQuote: false,
    trailingComma: "all",
    printWidth: 120,
};

webpack.config.js 파일 생성

// webpack.config.base.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");

module.exports = {
    target: ["web", "es5"],
    entry: "./src/index.js",
    output: {
        path: path.resolve(__dirname, "docs"),
        filename: "[fullhash].bundle.js",
        clean: true,
    },
    module: {
        rules: [
            {
                test: /\.css$/,
                use: [MiniCssExtractPlugin.loader, "css-loader"],
                exclude: /node_modules/,
            },
            {
                test: /\.m?js$/,
                exclude: /(node_modules)/,
                use: {
                    loader: "babel-loader",
                    options: {
                        presets: ["@babel/preset-env", ["@babel/preset-react", { runtime: "automatic" }]],
                    },
                },
            },
        ],
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: path.resolve(__dirname, "public", "index.html"),
        }),
        new MiniCssExtractPlugin(),
    ],
    optimization: {
        minimizer: [new CssMinimizerPlugin()],
    },
};
// webpack.config.dev.js
const { merge } = require("webpack-merge");
const baseConfig = require("./webpack.config.base");

module.exports = merge(baseConfig, {
    mode: "development",
    devServer: {
        port: 3000,
    },
});
// webpack.config.prod.js
const { merge } = require("webpack-merge");
const baseConfig = require("./webpack.config.base");

module.exports = merge(baseConfig, {
    mode: "production",
});

package.json 스크립트 추가

"scripts": {
    "build": "webpack --config webpack.config.prod.js",
    "dev": "webpack-dev-server --open --config webpack.config.dev.js"
},

웹팩 번들에 얼마나 많은 모듈이 포함되어 있는지 쉽게 분석할 수 있는 webpack-bundle-analyzer를 적용
https://www.npmjs.com/package/webpack-bundle-analyzer

0개의 댓글