Webpack 직접 적용하며 이해하기

sy u·2022년 7월 20일
3

🤔 Webpack이란?

자바스크립트 어플리케이션을 위한 정적 모듈 번들러 이다.

At its core, webpack is a static module bundler for modern JavaScript applications. When webpack processes your application, it internally builds a dependency graph from one or more entry points and then combines every module your project needs into one or more bundles, which are static assets to serve your content from.
Webpack Concepts

html에 들어가는 다수의 js 파일들을 하나의 js 파일로 만들어 주는 것을 의미한다.

js 파일 뿐만 아니라 css, image, html 등을 모듈로 로드해서 사용할 수 있고
코드를 압축하는 기능을 제공하며 번들링 된 파일이 너무 무거워질 경우, 다시 여러 개의 파일로 코드 스플리팅(Code Spliting) 할 수 있다.

여기서 말하는 모듈(module)이란?

관련된 데이터와 함수들이 묶여 module을 형성하고 주로 파일 단위로 관리된다.
🧐 모달은 isShow라는 상태와 show, close 함수를 가진 하나의 모듈이라고 할 수 있다.

모듈들은 각각의 javascript 파일에서 관리된다.

여기서 말하는 번들러(bundler)란?

모듈별로 나누어진 파일을 연관되어 있는 단위로 묶어 하나의 파일로 만들어주는 역할을 한다. (모듈 또는 외부 라이브러리 간의 의존성을 쉽게 관리할 수 있다.)
🧐 alert, confirm, propmt라는 모듈이 있을 때, 이들을 대화상자라는 하나의 번들로 묶을 수 있다. alert이 a 라이브러리를 사용하고 있다면 a 라이브러리도 함께 번들링 된다.

웹 페이지에서 모듈을 사용하려면 해당 모듈과 모듈에서 사용하는 라이브러리를 로드해야 하는데 선후 관계를 따져 순서대로 로드해야 한다.
모듈의 수가 얼마 되지 않는다면 간단하게 로드할 수 있지만 수가 많아지면 매우 복잡해진다.
또한 이렇게 많은 양의 파일을 로드한다면 네트워크 병목현상이 발생할 수 있다.
하나의 자바스크립트 파일 안에 많은 양을 작성하면 해결할 수 있지만 이럴 경우 가독성도 떨어지고 유지보수도 힘들어진다.
이때, 번들링 된 파일을 로드하면 문제를 해결할 수 있다.
모듈을 번들링 하는 과정 중, 모듈이나 외부 라이브러리의 의존성들을 로드하기 때문에 선후 관계를 따질 필요가 없으며, 여러 파일로 작성된 모듈들을 연관된 특정 단위로 묶어서 하나의 파일로 만들기 때문에 병목현상도 예방할 수 있다.

웹팩은 프로젝트에 필요한 모든 모듈을 매핑하고 하나 이상의 번들을 생성하는 디펜던시 그래프를 생성한다.

주요 개념

// webpack.config.js

module.exports = {
  entry: './path/to/my/entry/file.js',
};
  • Entry
    엔트리 포인트는 웹팩이 내부의 디펜던시 그래프를 생성하기 위해 사용하는 모듈이다.
    즉, 엔트리 포인트는 웹팩이 번들링을 시작할 메인 파일이다.

    값으로 입력된 파일의 의존성을 찾아 나머지 파일을 알아낸다.

    엔트리 포인트는 꼭 하나가 아니라 여러 엔트리 포인트를 지정할 수 있는데 이때 이름(name)과 콘텐츠 해시(contentHash) 가진 여러 개의 번들로 만들 수 있다.

      // webpack.config.js
    
      module.exports = {
        entry: './path/to/my/entry/file.js',
      };
  • Output
    output 속성은 생성된 번들을 내보낼 위치와 파일의 이름을 지정하는 역할을 한다.

    // webpack.config.js
    module.exports = {
      output: {
        path: path.resolve(__dirname, 'dist'), // 내보낼 위치
        filename: 'my-first-webpack.bundle.js', // 생성된 번들 파일 이름
      },
    };

    🔖 publicPath 옵션
    다른 도메인이나 CDN에서 일부 또는 모든 출력 파일을 호스팅 하려는 경우 사용할 수 있으며 Webpack Dev Server에서는 출력 파일의 URL 경로로 사용된다.

  • Loaders
    웹팩은 자바스크립트와 JSON 파일만 이해한다. 만약 JSX 같은 코드를 이해하기 원한다면 로더를 사용하여 해당 코드들을 웹팩이 이해할 수 있도록 변환할 로더를 옵션으로 설정하고 그것들을 디펜던시 그래프에 추가할 수있는 유효한 모듈로 변환한다.

    // webpack.config.js
    module.exports = {
      module: {
        rules: [{ test: /\.?js$/, use: 'babel-loader' }],
      },
    };
    • test: 변환이 필요한 파일들을 식별한다.
    • use: 변환을 수행하는 데 사용되는 로더를 가리킨다.
      babel-loader를 사용하면 최신 문법으로 작성된 Javascript를 모든 브라우저에서 해석할 수 있도록 변환할 수 있다.
  • Plugins
    로더는 특정 유형의 모듈(JSX 파일 같은)을 변환하는 데 사용하지만 플러그인은 번들을 최적화하거나 asset을 관리하고 환경 변수 주입 등과 같은 작업을 수행한다.
    Webpack 패키지에 포함된 내장 플러그인 외에 추가로 필요한 기능은 외부 플러그인을 받아서 사용할 수 있다.

    // webpack.config.js
      module.exports = {
        plugins: [new HtmlWebpackPlugin({ template: './src/index.html' })],
      };

    HtmlWebpackPlugin은 HTML 파일을 생성하고 자동으로 생성된 모든 번들을 삽입한다.

  • Mode
    웹팩에 내장된 환경별 최적화를 활성화할 수 있다.
    모드 옵션의 값으로 development production none을 선택할 수 있다.

🔦 Webpack이 필요한 이유

페이지마다 새로운 HTML을 요청해 뿌려주는 방식이었던 예전에는 괜찮았지만
SPA 즉, 하나의 HTML 페이지에서 여러 개의 js 파일을 포함하게 되면서 파일을 컴파일 할 때 다수의 js 파일을 불러오는데 시간이 오래 걸리게 된다.

그러한 부분을 해결하기 위해서 하나의 js 파일로 번들링 해주는 것이 필요하다. 생성된 번들 파일은 번들 파일의 용량을 최대한 줄이거나 분할하는 등 최적화되어야 하고 모든 브라우저에서 작동해야 한다.

🛠️ Webpack 직접 적용하며 이해하기

아래 예제들은 Babel 공식홈페이지를 참고한 코드입니다.

1. webpack과 webpack-cli 설치하기

webpack-cli를 사용하면 API로 webpack과 상호 작용할 수 있다.

npm install webpack webpack-cli --save-dev

2. bundle 생성하기

배포 코드와 소스 코드를 분리하기 위해 dist 폴더를 생성한다. 생성한 dist 폴더에 index.html을 수동으로 생성한다.
뭔가 이상하지만 이 부분은 뒤에서 다시 다룰 예정이다.

// dist/index.html 
 <html>
   <head>
     <meta charset="utf-8" />
   </head>
   <body>
    <script src="bundle.js"></script>
   </body>
 </html>

소스 코드가 존재할 src 폴더에 index.js 파일을 생성한다. 이 파일에서는 lodash에서 제공하는 기능을 사용하고 있기 때문에 명시적으로 lodash가 있어야 한다.

// src/index.js
import _ from 'lodash';

const App = () => {
  const container = document.createElement('div');
  container.id = 'root';

  // 이 라인이 동작하려면 현재 스크립트를 통해 포함된 Lodash가 필요합니다.
  container.innerHTML = _.join(['Hello', 'webpack'], ' ');

  return container;
}

document.body.appendChild(App());

아래 명령어를 사용하면 웹팩은 src/index.js를 엔트리 포인트로 사용하고 output으로 dist/bundle.js을 생성된다.

npx webpack

이 과정에서 src/index.js에 import를 사용하여 의존성을 명시한 것을 통해 웹팩은 이에 대한 디펜던시 그래프를 만들 수 있다. 디펜던시 그래프를 사용하여 모듈에 필요한 스크립트가 순서대로 실행되는 최적화된 번들을 생성한다.

3. webpack.config.js 생성하기

웹팩 4부터 설정 파일이 필요하지 않지만 webpack.config.js 설정 파일을 통해 기본 설정 이외의 설정을 할 수 있다.

const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
};

해당 설정 파일을 적용하여 빌드 하려면 아래와 같은 명령어를 사용할 수 있다.

npx webpack --config webpack.config.js

이보다 더 간단한 방법은 package.json에 스크립트로 추가하여 사용하는 것이다.

{
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack --config webpack.config.js"
  },
}

이제 npm run build 스크립트를 통해 빌드 할 수 있다.

4. 로더를 사용하여 자바스크립트 파일에 css import 하기

자바스크립트 모듈 내에서 css 파일을 import 하여 사용하려면 style-loader css-loader를 설정해야 한다.

필요한 로더를 설치한다.

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

webpack.config.js 설정 파일에 설치된 로더를 적용한다.

// webpack.config.js

const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: ['style-loader', 'css-loader'],
      },
    ],
  },
};

위 추가된 설정 내용은 css 확장자를 가진 파일이 번들로 만들어지기 전에 style-loader와 css-loader를 사용하여 디펜던시 그래프에 유효한 모듈로 변환된다.

여러 개로 설정된 각 로더는 역순으로 실행된다. 처음 실행된 로더의 결과(변경된 모듈)는 다음 로더에게 전달된다. 따라서 로더를 설정하는 순서가 중요하다.
위 예제에서는 style-loader를 먼저 설정하고 css-loader를 그다음에 설정해야 한다.

src 폴더에 style.css 파일을 생성한다.

/* src/style.css */

#root {
  background-color: blue;
  color: white;
}

생성한 style.css 파일을 index.js 파일에 import 한다.

// src/index.js

import _ from 'lodash';
import './style.css';

const App = () => {
  const container = document.createElement('div');
  container.id = 'root';
  
  container.innerHTML = _.join(['Hello', 'webpack'], ' ');

  return container;
}

document.body.appendChild(App());

결과를 확인해 보면 css 파일이 적용된 것을 확인할 수 있다.

더 많은 정적 모듈 사용 방법은 asset-management에서 확인할 수 있다.

5. 다중 엔트리 사용하기

src 폴더에 새로운 자바스크립트 파일 src/print.js을 생성한다.

// src/print.js

export default print = () => {
  console.log('print 함수');
}

src/index.js 에서 생성한 print.js를 import하여 사용한다.

import _ from 'lodash';
import print from './print.js';
import './style.css';

const App = () => {
  const container = document.createElement('div');
  container.id = 'root';
  
  const btn = document.createElement('button');

  container.innerHTML = _.join(['Hello', 'webpack'], ' ');

  btn.innerHTML = '클릭!';
  btn.onclick = print;

  container.appendChild(btn);

  return container;
}

document.body.appendChild(App());

엔트리를 분리하기 위해 dist/index.html 파일도 수동으로 업데이트해준다.

 <html>
   <head>
     <meta charset="utf-8" />
     <script src="./print.bundle.js"></script>
   </head>
   <body>
    <div id="root"></div>
    <script src="./index.bundle.js"></script>
   </body>
 </html>

webpack.config.js 파일에 다중 엔트리 포인트를 설정한다.

// webpack.config.js

module.exports = {
  entry: { // string이었던 entry 값을 key value인 객체로 변환한다.
    index: './src/index.js',
    print: './src/print.js',
  },
  output: {
    // filename: 'bundle.js',
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: ['style-loader', 'css-loader'],
      },
    ],
  },
};

이때, [name]은 entry 객체의 key를 가져와서 할당된다.

빌드 후, 결과를 확인하면 dist 폴더에 index.bundle.js와 print.bundle.js 파일이 생성된 것을 확인할 수 있다.

6. HtmlWebpackPlugin 세팅하기

지금까지 생성된 번들이 변경될 경우, dist/index.js 파일을 수동으로 수정해 왔다.
한두 개의 번들 파일일 경우 이렇게 적용해도 문제 되지 않지만 번들이 많아지고 번들 이름이 변경될 일이 많을수록 힘들어진다.

이 문제를 해결하기 위해 HtmlWebpackPlugin을 사용하여 해결할 수 있다.
HtmlWebpackPlugin 메서드는 자체 index.html 파일을 생성한다. 번들을 출력할 폴더에 index.html 파일이 존재한다면 새로운 index.html 파일로 대체된다.

우선 HtmlWebpackPlugin을 설치한다.

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

webpack.config.js 파일에 플러그인으로 HtmlWebpackPlugin를 추가하고 엔트리의 print.js key를 이전과 다른 이름으로 수정한다.

// webpack.config.js

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

module.exports = {
  entry: {
    index: './src/index.js',
    printa: './src/print.js',
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: 'html webpack plugin',
    }),
  ],
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: ['style-loader', 'css-loader'],
      },
    ],
  },
};

빌드 후, 기존 index.html이 수정된 것과 printa.bundle.js를 확인할 수 있다.

7. output.clean 옵션을 사용하여 번들 출력 폴더 정리하기

지금까지 생성된 번들 파일이 출력되는 dist 폴더는 더 이상 사용하지 않는 번들 파일을 손수 지워줘야 했다.
output.clean 옵션을 사용하면 빌드 전, dist 폴더를 정리해 준다.

webpack.config.js 파일 output에 output.clean 옵션을 추가한다.

// webpack.config.js

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

module.exports = {
  entry: {
    index: './src/index.js',
    printa: './src/print.js',
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: 'html webpack plugin',
    }),
  ],
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist'),
    clean: true,
  },
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: ['style-loader', 'css-loader'],
      },
    ],
  },
};

빌드 후, dist 폴더를 확인하면 사용하지 않는 번들 파일은 사라지고 빌드 시, 생성된 번들 파일만 존재한다.

🛠️ Webpack과 Babel을 이용한 React 개발 환경 세팅

1. react와 react-dome 설치하기

npm istall react react-dom

2. 바벨 설정하기

필요한 preset을 설치한다.

npm install --save-dev @babel/core @babel/preset-env @babel/preset-react

프로젝트 root에 babel.config.json을 생성한다.

{
  "presets": [
    "@babel/preset-env",
    "@babel/preset-react"
  ]
}

3. 웹팩 설정하기

웹팩 설정은 위에서 설정했던 내용들을 이어서 진행하겠습니다.

필요한 로더를 설치한다.

npm install --save-dev babel-loader html-loader

webpack.config.js 파일을 수정한다.

// webpack.config.js

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

module.exports = {
  mode: 'development',
  entry: {
    index: './src/index.js',
    app: './src/app.js',
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: 'index.html',
    }),
  ],
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist'),
    clean: true,
  },
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: ['style-loader', 'css-loader'],
      },
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
        },
      },
      {
        test: /\.html$/,
        use: {
            loader: 'html-loader',
        },
      },
    ],
  },
};

수정한 내용은 아래와 같다.

  • 설치한 로더를 설정
    babel-loader: 바벨을 웹팩에서 사용할 수 있게 해주는 로더이다.
    html-loader: HTML 파일을 디펜던시 그래프에 유효한 모듈로 변환하는 로더이다.
  • HtmlWebpackPlugintemplate 옵션 설정
    이전에는 HtmlWebpackPlugin가 새로운 index.html을 자동으로 생성해 다른 태그를 직접 넣을 수 없었다. template 옵션에 원하는 index.html을 설정하면 해당 파일을 템플릿으로 사용할 수 있다.

4. React 컴포넌트 생성하고 root 엘리먼트에 렌더하기

src 폴더에 app.js 파일을 생성한다.

// src/app.js

import React from 'react';
import print from './print';

const App = () => {
  return (
    <button onClick={() => print()}>클릭!</button>
  );
};

export default App;

src/index.js 파일에 ReactDOM.create()로 root 엘리먼트를 생성하고 생성된 root에 .render() 메서드를 사용하여 App 컴포넌트를 렌더 한다.

// src/index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import './style.css';
import App from './app.js';

const container = document.getElementById('root');
const root = ReactDOM.createRoot(container);
root.render(<App />);

root에 템플릿으로 사용할 index.html 파일을 생성한다.

<!-- index.html -->

<html>
  <head>
    <meta charset="UTF-8">
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

빌드 후, App 컴포넌트가 정상적으로 DOM에 렌더링 된 것을 확인할 수 있다.

🗃️ 참고 블로그

https://webpack.js.org/
https://dev.to/bnevilleoneill/versatile-webpack-configurations-for-your-react-application-3iif
https://dev.to/yadhus/what-is-entry-webpack-5-1cb5
https://dev.to/yadhus/introduction-to-webpack-5-5fhm
https://dev.to/nitsancohen770/what-is-webpack-in-simple-words-31fe
https://flatlogic.com/blog/what-is-webpack-flatlogic-glossary/
https://heodolf.tistory.com/151
https://proglish.tistory.com/209
https://berkbach.com/%EC%9B%B9%ED%8C%A9-webpack-%EA%B3%BC-%EB%B0%94%EB%B2%A8-babel-%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-react-%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-fb87d0027766

2개의 댓글

comment-user-thumbnail
2024년 6월 11일

좋은글 감사합니다. 그런데 CSS가 적용이 된 상태로 렌더링 되지 않았는데 혹시 이부분은 어떻게 하면 적용이 될까요 ! @geometry dash lite

1개의 답글