Github https://github.com/webpack/webpack

공식 문서 https://webpack.js.org/

최신 버전 (2022.06.12 기준): v5.73.0

들어가기 전에…

성장하는 Front-End 개발자라면 공부할 수밖에 없는 Module Bundler에 대해 알아보는 시간을 가져보겠습니다! Module Bundler는 대표적으로 WebpackRollup, Parcel 등이 있는데요. 저는 그중 Webpack에 대해 알아보도록 하겠습니다.

React를 개발하는 Front-End라면 Webpack을 이미 사용하고 있다는 사실을 알고 계신가요? React 환경을 구축하기 위해 CRA (Create-React-App)를 사용한다면 기본적으로 Webpack과 Babel과 같은 설정들이 세팅되어져 있습니다.

저는 이번에 Webpack에 대해 공부해보며 CRA를 사용하지 않고 React를 개발할 수 있는 환경을 직접 구성해보도록 하겠습니다!

번들러란

Bundling?

웹 애플리케이션을 구성하는 여러개의 자원들을 하나로 병합 및 압축해주는 동작을 번들링(Bundling)이라하며,

Bundler?

여러 파일들을 번들링을 할 수 있게 합쳐주는 도구번들러(Bundler)라고 합니다.

  • 여러 js파일들을 하나의 js파일로,
  • 여러 css파일들을 하나의 css파일로,
  • 여러 png파일들을 하나의 png파일로,

이렇게 기능별로 모듈화 했던 파일들을 하나로 묶어주는 작업을 해줍니다.

번들러가 왜 필요했을까

인터넷이 등장하고 초기에는 웹 페이지와 서비스들의 규모가 그렇게 크지 않았기 때문에 이를 구성하는 파일(HTML, JS 등)의 크기도 상대적으로 작았으며 해당 서비스를 유지하는데 큰 무리가 없었습니다.

하지만 인터넷이 발달하면서 대규모 웹 서비스들이 생겨나기 시작했고 하나의 웹 서비스에서 수십 수백개의 JS 파일을 다루면서 문제들이 발생했습니다.

1. 중복된 이름으로 인한 에러

자바스크립트의 변수 유효 범위는 기본적으로 전역 범위를 갖습니다. 그렇기 때문에 어디에서도 접근할 수 있다는 편리함이 있습니다.

하지만 이러한 장점이 실제로 웹 애플리케이션을 개발할 때는 아래와 같은 문제점으로 발생됩니다.

// app.js

var num = 10;
function getNum() {
  console.log(num);
}
// main.js

var num = 20;
function getNum() {
  console.log(num);}

위와 같은 두 개의 코드를 아래의 index.html에서 로딩하여 사용한다고 했을 때, 아래와 같이 코드를 실행하면 어떤 결과가 나올까요?

// index.html

<html>
  <head>
  </head>
  <body>
    <script src="./app.js"></script>
    <script src="./main.js"></script>
    <script>
      getNum(); // 20
    </script>
  </body>
</html

결과는 20입니다. app.js에서 선언한 num 변수는 main.js에서 다시 선언하고 20을 할당했기 때문입니다.

이러한 문제점은 실제로 복잡한 애플리케이션을 개발할 때 동일한 변수명과 함수명이 중복 선언되면서 의도치 않은 값을 할당하는 등 큰 문제가 발생하게 됩니다.

위처럼 자바스크립트 파일이 적다면 변수명을 나누어 사용하는 등 문제를 사전에 방지할 수 있겠지만, 대규모 프로젝트에서 사전에 방지하는 것은 거의 불가능에 가까웠습니다.

그래서 파일 단위로 변수를 관리하기 위한 모듈이 필요하게 되었습니다.

2. 파일 전송 문제

사용자가 브라우저에서 URI를 입력하면 해당하는 파일을 서버로부터 가져옵니다. 여기서 웹 애플리케이션를 구성하는 파일의 양이 많다면, 사용자의 요청에 응답하는 시간이 길어지게 됩니다.

만약, 1개의 파일을 요청하고 응답하는데 1초가 걸린다고 가정해본다면 100개의 파일을 응답하는 데 100초, 1000개의 파일을 응답하는 데 1000초의 시간이 걸리는 끔찍한 사용자 경험을 선사하게 됩니다.

여기에 수많은 사용자가 사이트를 이용할 경우 응답을 제때 못하는 네트워크 병목현상을 초래할 수 있고, 또한 서비스를 제공하는 입장에서도 엄청난 양의 파일을 주고 받으면서 생기는 비용적인 문제가 발생할 것입니다.

이러한 문제로 파일을 나누어 요청을 받기보다 하나로 합쳐 전송하는 방법이 필요하게 되었습니다.

위의 문제들을 해결하기 위해 여러개의 파일을 하나의 파일로 묶어주는 번들러(Bundler)가 등장하게 되었습니다.

번들러의 기능

번들러는 애플리케이션에 필요한 모든 종류의 파일들을 모듈 단위로 나누어 최소한의 파일 묶음(번들)으로 만들어 냅니다. 뿐만 아니라 자바스크립트 파일을 외부에서 알아 보기 힘들게 코드를 변환하는 작업(Uglyfy: 난독화)을 한다거나, 최신 문법의 자바스크립트를 모든 웹 브라우저에서 작동할 수 있게 ES5문법으로 변환(Transpile)하는 등 다양한 기능을 지원 할 수 있습니다.

때문에 번들러를 사용하게 되면 다음과 같은 장점들이 있습니다.

  1. 네트워크 병목 현상 해결 – 여러 파일을 최적화 해서 하나의 파일로 묶기 때문에 주고 받는 파일의 크기를 줄여줍니다.
  2. 모듈 단위 코딩 – 유지 보수가 유리해지고 코드의 가독성이 향상 됩니다.
  3. 다양한 서드파티 기능 이용 – Webpack의 경우 Babel-loader과 같은 다양한 로더를 이용해서 모던 자바스크립트나 SASS를 사용할 수 있습니다.

Webpack(웹팩)이란

Webpack이란 최신 프론트엔드 프레임워크에서 가장 많이 사용되는 모듈 번들러(Module Bundler)입니다. 모듈 번들러란 웹 애플리케이션을 구성하는 자원(HTML, CSS, Javscript, Images 등)을 모두 각각의 모듈로 보고 이를 조합해서 병합된 하나의 결과물을 만드는 도구를 의미합니다.

Webpack에서의 모듈

Module(모듈)이란 프로그래밍 관점에서 특정 기능을 갖는 작은 코드 단위를 의미합니다. Webpack에서 지칭하는 Module이라는 개념은 자바스크립트 Module에만 국한되지 않고 웹 애플리케이션을 구성하는 모든 자원을 의미합니다. 웹 애플리케이션을 제작하려면 HTML, CSS, Javascript, Images, Font 등 많은 파일들이 필요로 하며 이 파일 하나하나가 모두 Module이 됩니다.

무엇을 해결하려고 했을까

웹팩이 등장한 이유는 크게 3가지입니다.

  1. 파일 단위의 자바스크립트 모듈 관리
  2. 웹 개발 작업 자동화
  3. 빠른 로딩 속도와 높은 성능

왜 Webpack일까

개발자들이 다른 모듈 번들러에 비해 왜 Webpack을 선호하는지 몇 가지 이유를 알아보겠습니다.

  • 다양한 플러그인 시스템과 로더를 통해서 훨씬 많은 것을 할 수 있고 강력한 기능들을 사용할 수 있습니다.
  • 오랫동안 사용되어지고 개발 래퍼런스가 다양한 가장 안정적인 번들러입니다.
  • 수정된 사항을 새로 고침 없이 실시간으로 반영해주는 HMR(Hot Module Replacement)이 적용된 Dev Server가 존재합니다.

웹팩 적용 전후, 네트워크 요청 횟수 비교

크롬 개발자 모드에서 Network 탭을 들어가게 되면 웹팩을 적용하기 전과 후의 어떤 변화가 있는지 확인할 수 있습니다. 가장 눈에 띄는 변화는 네트워크 요청 횟수로, 웹팩은 여러 자원을 하나의 파일로 합쳐 전송하기 때문에 네트워크 요청 횟수를 줄일 수 있습니다.

웹팩을 적용하기 전, 3개의 파일이( hello.js, world.js, test.js) 따로따로 요청됩니다.

웹팩을 적용한 후, 따로 요청되던 파일들이 하나로 합쳐진 main.js 파일로 전송되었습니다.

Webpack Dev Server

Webpack에서는 변경된 모듈을 실시간으로 번들 파일에 적용하고 업데이트해 주는 기능을 Dev Server를 통해 제공합니다. HMR(Hot Module Replacement)을 사용하면 파일을 수정할 때마다 페이지 리로드 없이 변경된 부분의 모듈만 업데이트되는 것을 확인할 수 있고 이로 인해 개발 생산성을 향상시킬 수 있습니다.

Dev Server를 실행하면 빌드한 결과물이 보이지 않는데요. 그 이유로는 Dev Server는 디스크가 아닌 메모리에 저장되어 실행되기 때문입니다. 파일 입출력보다 메모리의 입출력이 더 빠르기 때문에 컴파일 속도가 빨라지고 컴퓨터 자원이 덜 소모될 수 있습니다. 따라서 크롬 브라우저의 네트워크를 확인해보면 번들링된 파일을 요청하고 있지만 눈으로 확인할 수 없는 이유로 메모리에 저장되어 전송되고 있기 때문입니다.

Webpack Config 살펴보기

Bundler가 무엇이고 Webpack이 무엇인지 알았으니 Webpack을 어떻게 사용하는지, Webpack을 설정하는 속성들에 대해 알아보도록 하겠습니다. 속성들에 대한 예제 코드는 CRA로 구성된 React 프로젝트의 webpack.config.js 파일을 기준으로 살펴보도록 하겠습니다.

npm run eject

해당 명령어를 사용하여 숨겨져 있던 Webpack 설정 파일을 확인하고 커스터마이징 할 수 있습니다. 한번 실행시 되돌릴 수 없으며 자동으로 해줬던 설정들의 유지보수와 라이브러리 간의 의존성을 직접 신경써야 하는 단점이 있으니 신중해야 합니다.

entry

애플리케이션이 실행되며 Webpack이 번들링을 시작하는 곳입니다. Webpack은 entry point가 의존하는 다른 모듈과 라이브러리들을 찾아냅니다.

module.exports = {
	entry: paths.appIndexJs,

};

output

output 속성은 번들 결과물의 경로를 설정하고 이름을 지정하는 옵션 등을 추가할 수 있습니다.

  • path: 번들링된 결과물의 위치를 설정할 수 있습니다.
  • filename: 번들링된 결과물의 이름을 설정할 수 있습니다.
module.exports = {
	output: {
	  // The build folder.
	  path: paths.appBuild,
	  // Add /* filename */ comments to generated require()s in the output.
	  pathinfo: isEnvDevelopment,
	  // There will be one main bundle, and one file per asynchronous chunk.
	  // In development, it does not produce real files.
	  filename: isEnvProduction
	    ? 'static/js/[name].[contenthash:8].js'
	    : isEnvDevelopment && 'static/js/bundle.js',
	  // There are also additional JS chunk files if you use code splitting.
	  chunkFilename: isEnvProduction
	    ? 'static/js/[name].[contenthash:8].chunk.js'
	    : isEnvDevelopment && 'static/js/[name].chunk.js',
	  assetModuleFilename: 'static/media/[name].[hash][ext]',
	  // webpack uses `publicPath` to determine where the app is being served from.
	  // It requires a trailing slash, or the file assets will get an incorrect path.
	  // We inferred the "public path" (such as / or /my-project) from homepage.
	  publicPath: paths.publicUrlOrPath,
	  // Point sourcemap entries to original disk location (format as URL on Windows)
	  devtoolModuleFilenameTemplate: isEnvProduction
	    ? info =>
	        path
	          .relative(paths.appSrc, info.absoluteResourcePath)
	          .replace(/\\\\/g, '/')
	    : isEnvDevelopment &&
	      (info => path.resolve(info.absoluteResourcePath).replace(/\\\\/g, '/')),
	},

}

loader

웹 애플리케이션을 해석할 때 자바스크립트 파일이 아닌 HTML, CSS 등 웹 자원들을 변환할 수 있도록 도와줍니다. 자주 사용되는 로더로는 babel-loader, css-loader, style-loader, file-loader 등이 있습니다.

기본적으로 Webpack은 JavaScript 및 JSON 파일만 해석 가능합니다. 하지만 로더(loaders)를 사용하면 Webpack이 다른 포맷의 파일을 처리하고, 이를 앱에서 사용할 수 있는 모듈로 변환 할 수 있습니다.

  • test: loader를 적용시킬 파일 유형 명시 (정규 표현식 사용)
  • use: 해당 파일에 적용할 loader 명시
  • exclude: loader를 배제시킬 파일 명시
module: {
  strictExportPresence: true,
  rules: [
		{
		  test: /\\.svg$/,
		  use: [
		    {
		      loader: require.resolve('@svgr/webpack'),
		      options: {
		        prettier: false,
		        svgo: false,
		        svgoConfig: {
		          plugins: [{ removeViewBox: false }],
		        },
		        titleProp: true,
		        ref: true,
		      },
		    },
		    {
		      loader: require.resolve('file-loader'),
		      options: {
		        name: 'static/media/[name].[hash].[ext]',
		      },
		    },
		  ],
		  issuer: {
		    and: [/\\.(ts|tsx|js|jsx|md|mdx)$/],
		  },
		},
		// Process application JS with Babel.
		// The preset includes JSX, Flow, TypeScript, and some ESnext features.
		{
		  test: /\\.(js|mjs|jsx|ts|tsx)$/,
		  include: paths.appSrc,
		  loader: require.resolve('babel-loader'),
		  options: {
		    customize: require.resolve(
		      'babel-preset-react-app/webpack-overrides'
		    ),
		    presets: [
		      [
		        require.resolve('babel-preset-react-app'),
		        {
		          runtime: hasJsxRuntime ? 'automatic' : 'classic',
		        },
		      ],
		    ],
		    
		    plugins: [
		      isEnvDevelopment &&
		        shouldUseReactRefresh &&
		        require.resolve('react-refresh/babel'),
		    ].filter(Boolean),
		    // This is a feature of `babel-loader` for webpack (not Babel itself).
		    // It enables caching results in ./node_modules/.cache/babel-loader/
		    // directory for faster rebuilds.
		    cacheDirectory: true,
		    // See #6846 for context on why cacheCompression is disabled
		    cacheCompression: false,
		    compact: isEnvProduction,
		  },
		},
		{
      test: sassRegex,
      exclude: sassModuleRegex,
      use: getStyleLoaders(
        {
          importLoaders: 3,
          sourceMap: isEnvProduction
            ? shouldUseSourceMap
            : isEnvDevelopment,
          modules: {
            mode: 'icss',
          },
        },
        'sass-loader'
      ),
      // Don't consider CSS imports dead code even if the
      // containing package claims to have no side effects.
      // Remove this when webpack adds a warning or an error for this.
      // See <https://github.com/webpack/webpack/issues/6571>
      sideEffects: true,
    },
    // Adds support for CSS Modules, but using SASS
    // using the extension .module.scss or .module.sass
    {
      test: sassModuleRegex,
      use: getStyleLoaders(
        {
          importLoaders: 3,
          sourceMap: isEnvProduction
            ? shouldUseSourceMap
            : isEnvDevelopment,
          modules: {
            mode: 'local',
            getLocalIdent: getCSSModuleLocalIdent,
          },
        },
        'sass-loader'
      ),
    
},

plugin

Webpack으로 변환한 파일에 추가적인 기능을 제공할 수 있습니다. 플러그인은 해당 결과물의 형태를 바꿔주는 역할을 수행합니다. 예를 들어, 번들된 JS를 난독화 한다거나 특정 텍스트를 추출하는 용도로 사용할 수 있습니다. 사용가능한 플러그인에 대해 더 알아보고 싶으시다면 아래 링크를 참고하실 수 있습니다.

Plugins | webpack

plugins: [
  // Generates an `index.html` file with the <script> injected.
  new HtmlWebpackPlugin(
    Object.assign(
      {},
      {
        inject: true,
        template: paths.appHtml,
      },
      isEnvProduction
        ? {
            minify: {
              removeComments: true,
              collapseWhitespace: true,
              removeRedundantAttributes: true,
              useShortDoctype: true,
              removeEmptyAttributes: true,
              removeStyleLinkTypeAttributes: true,
              keepClosingSlash: true,
              minifyJS: true,
              minifyCSS: true,
              minifyURLs: true,
            },
          }
        : undefined
    )
  
),

CRA 없이 React 환경 구축하기 (Feat. TS)

React 프로젝트를 CRA 명령어를 통해 설치하면 쉽고 간편하게 React 개발 환경을 구축할 수 있습니다. 자동으로 다수의 패키지들이 설치되고 Webpack 및 Babel까지 설정해주는 친절한 도구입니다. 하지만 매번 CRA를 통한 환경 구축으로 Webpack과 같은 번들링 도구들이 어떻게 설정되어 있고 동작하는지에 대해 알 수 없었습니다.

Webpack을 공부하게 된 계기로 CRA 없이 TypeScript React 개발 환경을 직접 구축해보는 시간을 가져보았습니다.

1. package.json 생성

npm init -y

2. React 설치

npm install react react-dom
npm install -D @types/react @types/react-dom

3. Typescript 설치

npm install -D typescript ts-node

4. tsconfig.json 생성

npx tsc --init

5. Webpack 설치

npm i -D webpack webpack-cli webpack-dev-server
npm i -D file-loader url-loader ts-loader
npm i -D babel-loader @babel/core @babel/preset-env @babel/preset-react
npm i -D html-webpack-plugin clean-webpack-plugin

6. Webpack 설정 (wepack.config.js)

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");

module.exports = {
  // development(개발 모드) or production(배포 모드)
  mode: "development",

  // 번들링 할 파일 확장자
  resolve: {
    extensions: [".js", ".jsx", ".ts", ".tsx"],
  },

  // 번들링 진입점
  entry: "./src/index",

  // 번들링 결과물 설정
  output: {
    path: path.resolve(__dirname, "build"), // 빌드되는 파일들이 만들어지는 위치, __dirname: 현재 디렉토리
    filename: "bundle.js", // 번들파일 이름
  },

  // 로더 설정
  module: {
    rules: [
      {
        test: /\\.(ts|js)x?$/,    // loader를 적용시킬 파일 정규식 명시
        use: [    // 사용할 loader
          {
						// es6 > es5 변환 (transpiler)
            loader: "babel-loader",
          },
        ],
        exclude: /node_modules/, // loader를 배제시킬 파일 명시
      },
      {
        test: /\\.tsx?$/,
        use: ["ts-loader"],
        exclude: ["/node_modules/"],
      },
      {
        test: /\\.css$/,
				// css-loader: css를 js처럼 사용할 수 있도록 처리, style-loader : js로 처리된 스타일시트 코드를 html에 적용
        // use에 선언된 가장 오른쪽의 로더가 먼저 실행 (오른쪽에서 왼쪽 순으로)
				use: ["style-loader", "css-loader"],
      },
      {
        test: /\\.(png|jpe?g|gif|woff|woff2|ttf|svg|ico)$/i,
        use: [
          {
            loader: "file-loader",
          },
        ],
      },
    ],
  },

  // webpack 서버 설정
  devServer: {
    static: path.join(__dirname, "build"),
    port: 8088,
  },

  plugins: [
    // 분리된 css, js 파일들을 각각 html에 link 자동화
    new HtmlWebpackPlugin({
      template: `./public/index.html`,
    }),

    // output.path 디렉토리에 있는 이전에 빌드된 결과물 삭제
    new CleanWebpackPlugin(),
  ],

};

7. package.json

설정한 Webpack을 이용할 수 있도록 실행 명령어를 수정해줍니다.

"test": "echo \\"Error: no test specified\\" && exit 1",
"dev": "webpack-dev-server --open",
"build": "webpack --config webpack.config.js"

8. 초기 파일 구성

최상단에서 public과 src 폴더를 만들고 초기 파일을 생성해줍니다.

// 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>Document</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

// index.tsx

import * as React from "react";
import ReactDom from "react-dom";
import App from "./App";

ReactDom.render(<App />, document.querySelector("#root"));

// App.tsx

import React from "react";

const App = () => {
  return <div> Hello React!</div>;
};


export default App;

9. dev 서버 실행

npm run dev

10. 빌드

npm run build

build 폴더가 생성되며 번들링된 결과물을 확인할 수 있습니다.

마치며

이번엔 가장 많이 사용되는 대표적인 Bundler인 Webpack에 대해 알아보는 시간을 가졌습니다. 매번 CRA를 통해 환경 구축을 하던 저에게 Webpack이란 프론트엔드 개발자로서 알아야 될 필수 지식 중 하나였습니다. Webpack에 대해 공부하게 된다면 CRA 없이 직접 환경을 구축해봐야겠다! 했던 생각이 이번 기회를 통해 공부하게 되며 직접 실현해볼 수 있던 뜻깊은 배움의 시간이었습니다.

Webpack은 러닝커브가 높은 개념이지만 생태계가 넓고 잘 돼있는 만큼 공부하고 참고할 래퍼런스가 많아 접근하기 수월했습니다. 자체의 configulationloader, plugin에 추가하고 설정할 수 있는 옵션들이 많아 까다롭고 복잡하게 느껴지기도 했지만 그만큼 사용할 수 있는 기능이 많고 훌륭한 기술들이 다양히 존재하고 있어 큰 이점으로 다가왔습니다.

Webpack을 공부하며 알게 되는 부가적인 개념과 내용이 되게 많았습니다. 이 포스팅을 끝으로 마무리하는 게 아니라 자세히 설명하지 못한 babel과 plugin 등에 대해서도 점차 알아가도록 해야겠다는 생각이 들었습니다.

참고:

https://webpack.js.org/

https://joshua1988.github.io/webpack-guide/webpack/what-is-webpack.html#%EC%9B%B9%ED%8C%A9%EC%9D%B4%EB%9E%80

https://www.hyojae.info/db57bc7b-0fbd-46bc-a71b-e45bcbfad26e#9583c6db-bde0-499c-95e6-57b689cfb5cb

https://365kim.tistory.com/147#%F0%9F%99%8B%F0%9F%8F%BB%E2%80%8D%E2%99%80%EF%B8%8F-babel/preset-env-%EB%8D%94-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0

이정현
Developer

오픈소스컨설팅의 Front-End 개발자 이정현입니다. 한걸음 꾸준히 나아가며 한계를 뛰어넘기 위해 도전합니다.

Leave a Reply

Your email address will not be published. Required fields are marked *