본문 바로가기

FrontEnd+

웹팩(Webpack) 밑바닥부터 설정하기

 

최근 웹팩을 설정해서 과제를 제출했다. 웹팩을 '활용'했다고 하기에는 사용한 기능이 빙산에 일각에 불과한데, 이마저도 대부분을 페어로부터 배웠다,,, 혼자 해보면 정말 못할 것 같아서, 다시 처음부터...! 정리를 해보려고 한다. 웹팩 설정 파일(webpack.config.js)을 차근차근 완성해가며 웹팩의 주요 개념을 익혀보자.

 

0. 웹팩(webpack)이란?

웹팩 공식 Github 에서는 웹팩을 '모듈(module)'을 위한 '번들러(bundler)' 라고 소개한다. 웹팩에 대해 알아보려면 '모듈'과 '번들러'가 무엇인지부터 알아보아야겠다.

webpack is a bundler for modules. The main purpose is to bundle JavaScript files for usage in a browser, yet it is also capable of transforming, bundling, or packaging just about any resource or asset.

모듈은 무엇일까

모듈(module)은 재사용 가능한 코드조각이다. 아주 쉽게 말하면 .js 파일이다. 모듈은 자신만의 파일 스코프(모듈 스코프)를 갖고 export, import 할 수 있다. 보통 클래스 하나 또는 특정한 목적을 가진 복수의 함수로 구성된 라이브러리 하나로 구성된다.

초기 자바스크립트는 크기도 작고 기능도 단순해서 '모듈'에 관한 표준 문법이 필요하지 않았다. 시간이 흘러 웹에 요구되는 기능이 점점 복잡해지면서, 코드의 크기가 점점 커지기 시작했다. 이에 따라 자바스크립트 커뮤니티는 필요한 모듈을 언제든지 불러올 수 있게 해 주거나, 코드를 모듈 단위로 구성해 주는 방법 등 다양한 라이브러리를 만들었다. 'AMD', 'CommonJS', 'UMD'와 같은 모듈 시스템이 그 예이다. '모듈시스템'에서는 전역에 선언된 var의 사용으로 발생하는 부작용을 예방할 수 있기도 하다.

'모듈 시스템'에 대한 니즈가 커지면서 2015년에는 표준 문법으로 등재되었다. 이제는 대부분의 주요 브라우저와 Node.js가 '모듈 시스템'을 지원한다. 브라우저 환경에서는 '모듈'을 단독으로 사용하기보다 번들링 해서 배포서버에 올리는 방식을 주로 사용한다. 

일반 스크립트와 구별되는 모듈의 특징은 다음과 같다.

1. 모듈은 항상 defer 속성을 붙인 것처럼 지연 실행된다. (굳이 body 끄트머리에 적지 않아도 된다.)
2. 모듈은 strict mode 로 실행된다.
3. 모듈은 자신만의 스코프를 갖는다. (파일 스코프)
4. 모듈은 단 한 번만 평가(실행)되고 필요한 곳에서 공유된다.
5. 모듈 최상위 레벨 this는 undefined이다.
6. import.meta 객체로 정보를 얻을 수 있다. (e.g. import.meta.url)

번들러는 무엇일까

JS, CSS, 이미지 등의 파일을 묶어주는 작업을 '번들링(Bundling)'이라고 하고, 작업의 결과물은 '번들(Bundle)'이라고 한다. 웹팩 자체는 묶어주는 역할을 하기 때문에 '번들러(Bundler)'라고 한다,

번들링 과정이 끝나면 기존 스크립트에서 import/export가 사라지기 때문에 type="module"이 필요 없어진다. 따라서 번들링 과정을 거친 스크립트는 일반 스크립트처럼 취급한다.

그래서 웹팩은 왜 쓰는 걸까

2012년 처음 등장한 웹팩은 2020년 version 5를 발표하기까지 꾸준한 성장세를 그리고 있다. 웹팩이 이렇게까지 사랑받는 이유는 무엇일까? 더 많은 장점이 있겠지만 내가 이해할 수 있는 수준의 장점은 다음과 같다.

우선, 웹팩을 여러 개의 파일을 하나로 번들링하기 때문에 HTTP 요청 횟수를 줄일 수 있다. 이는 빠른 서비스 제공에 도움이 된다. (브라우저마다 HTTP 요청 횟수에 제한을 두고 있기도 하다.) 또한, 자바스크립트 외의 리소스 포맷의 모듈도 사용할 수 있게 해 준다. CSS든, 이미지든 사용하려는 곳에 해당 리소스를 import 해주기만 하면 웹팩이 알아서 빌드해준다! 웹팩이 알아서 자동화해주는 덕분에, 코드를 수정했을 때 다시 빌드하고 새로고침 하지 않아도 바로바로 빌드 결과를 확인할 수 있다. 또, 코드 스플리팅으로 원하는 Require.js와 같은 라이브러리 없이도 통째로 로딩하지 않고 필요한 순간에 원하는 모듈을 불러올 수 있다고 하는데(Dynamic Loading, Lazy Loading), 요 장점은 아직 체감하지는 못했다.

NPM Trends에서 조회한 webpack 사용량 (2021. 03. 기준)

 

 

 

이제 실전이다!

웹팩 공식 홈페이지의 Getting Started를 참고해서 webpack.config.js를 완성시켜 보자. 참고로 웹팩 version 4부터는 별도의 config 파일 없이도 번들링을 할 수는 있다. 하지만 config 파일을 활용하는 것이 사용자의 필요에 따라 설정을 변경하기에 훨씬 용이하다.

1. webpack 설치 및 실행

1) 웹팩 설치하기

우선 로컬에 웹팩을 설치한다. 터미널에서 명령어로 실행하기 위해 webpack-cli도 함께 설치하자. 웹팩은 테스트 도구인 cypress와 마찬가지로, 배포할 때 필요한 패키지가 아니라 개발할 때 필요한 패키지이니까 -D 또는 --save-dev 옵션을 준다. 그러면 package.json에 dependencies가 아닌 devDependencies에 추가가 된 것을 확인할 수 있다. 

// 터미널 명령어
npm i -D webpack webpack-cli
// package.json

"devDependencies": {
  "webpack": "^5.4.0",
  "webpack-cli": "^4.2.0"
}

2) 웹팩 실행하기

설치를 마쳤으면 아래 명령어를 참고해서 웹팩을 실행해보자.

실행 시 필요에 따라 --mode 옵션을 줄 수 있다. 'development', 'production', 'none' 세 가지 값을 정할 수 있다.  환경변수에 있는 DefinePlugin을 'development' 또는 'production'으로 바꾸고 웹팩이 각 모드에 맞는 빌트인 최적화를 할 수 있도록 한다. 'none'으로 지정하면 최적화를 허용하지 않는다. 별도로 지정하지 않으면 기본값인 'production' 모드로 실행된다.

webpack.config.js 파일에서 mode: 'development'로 지정해둘 수도 있는데, 개발과 배포 버전을 번갈아가면서 실행하기 위해서는 CLI에서 옵션을 주는 방법이 낫겠다. 

// 터미널 명령어
npx webpack --mode development

// 또는
node_modules/.bin/webpack --mode development

실행 성공을 알리는 초록색 메세지 ✅

 

 

2. Entry, Output, Loader, Plugin 설정

다음 해야 할 일은 웹팩의 4가지 핵심 개념을 이해하는 것이다. 실제로 웹팩 공식 홈페이지에서 Entry, Output, Loader, Plugin을 Core Concepts라고 소개한다.

1) Entry 설정

webpack은 번들링 과정에서 '디펜던시 그래프(dependency graph)'를 그린다. 특정 지점에서 출발해서, 애플리케이션에 필요한 모든 모듈을 포함하는 그래프를 재귀적으로 완성해 나간다. 한 파일이 다른 파일을 필요로 하면 이를 '디펜던시(dependency)'가 있다고 해석하는데, 이 방식으로 웹팩은 이미지 또는 웹 글꼴과 같이 코드가 아닌 리소스도 디펜던시로 관리할 수 있게 된다. 그래프를 모두 그리고 나면 이 모든 모듈을 소수의 번들로 묶어서 (보통 하나의 번들로 묶는다) 브라우저에 로드될 준비를 마친다.

이때 우리는 webpack이 어디를 출발지점으로 해서 그려나가면 좋을지 알려주어야 한다. config파일에서 entry 속성을 설정해서 웹팩이 어떤 모듈로부터 시작해서 디펜던시 그래프를 그려나갈지 명시해줄 수 있다. 'entry' 속성의 기본값은 './src/index.js'이지만 다른 Entry Point를 지정할 수도 있다. (여러 개도 지정 가능)

const config = {
  entry: './src/js/index.js',
};

2) Output 설정

output 은 웹팩이 번들을 꾸리고 나서 결과물을 어디로 내보낼지 지정하는 속성이다. 기본값으로 메인 결과물인 main.js 파일은 ./dist/main.js에, 그 외 파일은 ./dist 폴더에 내보내 진다. 파일 이름(filename), 경로(path)를 별도로 지정할 수 있고, clean을 true로 설정하면 지정한 결과물이 내보내지는 디렉토리(본 예제에서는 dist)안에 사용하지 않는 파일을 알아서 정리해준다. 이외에도 수많은 커스텀 옵션을 설정할 수 있다.

import path from 'path';
import { fileURLToPath } from 'url';

const dirname = path.dirname(fileURLToPath(import.meta.url));

const config = {
  output: {
    filename: 'main.js',
    path: path.resolve(dirname, 'dist'),
    clean: true
  },
};

3) Loader 설정하기

이제까지 자바스크립트 외의 리소스도 번들링할 수 있다고 했지만, 사실 웹팩은 기본적으로 JavaScript와 JSON 파일만 이해할 수 있다. 이 때 필요한 것이 Loader이다. 사용하려는 포맷에 대응하는 Loader를 설정해주면 다른 포맷의 리소스도 디펜던시 그래프에 추가할 수있게 된다. 

Loader를 설정하려면 'test'와 'use' 두 가지 필수 속성을 적어주어야 한다. 'test'는 어떤 파일을 변환할지 지정하는 속성으로, 보통 /\.txt$/과 같이 정규표현식으로 작성한다. 이 때, /\.txt$/ 과 같이 따옴표 없이 작성해야한다. '/\.txt$/' 또는 "/\.txt$/"와 같이 따옴표를 넣으면 빌드가 제대로 안될 것이다,,, 'use'는 파일을 변환할 때 어떤 로더를 사용해야하는지 명시하는 속성이다.이는 웹팩 컴파일러에게 다음과 같이 말하는 것과 같다.

웹팩 컴파일러야!
디펜던시 그래프를 그리다가 'test'에 지정된 파일형식을 발견하잖아?
그러면 번들에 넣기 전에 내가 'use'에 지정한 로더로 꼭 변환해줘야해~!

이렇게 Loader를 설정해주면 포맷에 얽매이지 않은 자유로운 import가 가능하다. 예를 들어 js파일에서 import '../css/index.css';과 같이 해당 모듈에서 필요한 css파일을 import해올 수 있다. 웹팩을 사용하기 전에는 상상할 수 없었던 일이다! 이 기능은 다른 번들러에서는 지원되지 않을 수도 있다고 한다. 

주의할 점은 config에 바로 rules 속성을 쓰는게 아니라 반드시 module.rules에 정의해 주어야 한다는 것이다. (틀리면 웹팩이 알아서 경고해주기는 한다.)

 

다음은 바벨 설정, HTML, CSS, 이미지 설정을 위한 명령어와 코드이다. 

- 바벨

바벨은 ES6+ 문법으로 작성된 js파일을 ES5문법으로 트랜스파일링 해준다.

// 터미널 명령어
npm i -D babel-loader @babel/core @babel/preset-env
npm i core-js regenerator-runtime
// webpack.config.js
const config = {
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /(node_modules|pages)/,
        use: {
          loader: 'babel-loader',
        },
      },
    ],
  },
};

- CSS

// 터미널 명령어
npm i -D css-loader
// webpack.config.js
const config = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          { loader: MiniCssExtractPlugin.loader },
          {
            loader: 'css-loader',
            options: { import: true },
          },
        ],
      },
    ],
  },
};

- 이미지 설정

const config = {
  module: {
    rules: [
      {
        test: /\.png$/,
        type: 'asset/resource',
      },
    ],
  },
};

 

4) Plugin 설정하기

바닐라 자바스크립트 프로젝트에서 꼭 필요한 두 가지 Plugin만 설정해보자. (주요 플러그인 리스트는 여기서 확인할 수 있다.)

html-webpack-plugin을 사용하면 dist의 main.js를 스크립트 파일로 포함하는 HTML 문서를 dist 디렉토리 내에 자동으로 생성해준다. template에 원본으로 사용할 HTML문서 경로를 넣어주면 된다. 이 플러그인을 사용하지 않고 빌드하면 dist 디렉토리에 .html 파일이 생성되지 않고, 따라서 dist 디렉토리 내의 빌드 결과물 만으로는 렌더할 수 없다.  

mini-css-extract-plugin를 사용하면 빌드 결과 JS파일에서 스타일시트를 분리해서 CSS 파일을 따로 만들어준다. 크기가 큰 하나의 파일을 받는 것보다 작은 여러 개의 파일을 다운로드 하는 것이 성능상 유리하기 때문에, 배포 시에는 분리하는 것이 좋다. 

// 터미널 명령어
npm i -D html-webpack-plugin mini-css-extract-plugin
// webpack.config.js
import HtmlWebpackPlugin from 'html-webpack-plugin';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';

const config = {
  plugins: [
    new HtmlWebpackPlugin({ template: './src/index.html' }), 
    new MiniCssExtractPlugin()
  ],
};

웹팩이 만들어준 main.js 를 포함하는 HTML파일이 dist 디렉토리 내 생성된다.

 

3. 웹팩 dev-server 설정

코드를 수정했을 때 다시 빌드하고 새로고침 하지 않아도 바로바로 빌드 결과를 확인할 수 있는 dev-server를 설정해보자.

// 터미널 명령어
npm i -D webpack-dev-server
const config = {
  devtool: 'eval-cheap-module-source-map',
  target: 'web',
  devServer: {
    contentBase: path.resolve(dirname, 'dist'),
    compress: true,
    hot: false,
    historyApiFallback: true,
    liveReload: true,
    open: true,
    port: 5500,
    watchContentBase: true,
    watchOptions: {
      poll: 1000,
      ignored: /node_modules/,
    },
  },
};

설정을 마치면 package.json에 script를 추가해보자. 참고로 serve는 키워드는 webpack-dev-server와 같다. (참고링크)

// package.json

"scripts": {
  "start": "webpack serve --mode=production",
  "start:dev": "webpack serve --mode=development",
  "build": "webpack --mode=production",
  "build:dev": "webpack --mode=production",
},

 

+ 전체 코드 보기

// webpack.config.js
import path from 'path';
import { fileURLToPath } from 'url';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';

const dirname = path.dirname(fileURLToPath(import.meta.url));

const config = {
  entry: './src/js/index.js',
  output: {
    filename: 'main.js',
    path: path.resolve(dirname, 'dist'),
    clean: true,
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /(node_modules|pages)/,
        use: {
          loader: 'babel-loader',
        },
      },
      {
        test: /\.css$/,
        use: [
          { loader: MiniCssExtractPlugin.loader },
          {
            loader: 'css-loader',
            options: { import: true },
          },
        ],
      },
      {
        test: /\.png$/,
        type: 'asset/resource',
      },
    ],
  },
  plugins: [new HtmlWebpackPlugin({ template: './src/index.html' }), new MiniCssExtractPlugin()],
  devtool: 'eval-cheap-module-source-map',
  target: 'web',
  devServer: {
    contentBase: path.resolve(dirname, 'dist'),
    compress: true,
    hot: false,
    historyApiFallback: true,
    liveReload: true,
    open: true,
    port: 5500,
    watchContentBase: true,
    watchOptions: {
      poll: 1000,
      ignored: /node_modules/,
    },
  },
};

export default config;
// babel.js
{
  "presets": [
    [
      "@babel/env",
      {
        "useBuiltIns": "usage",
        "corejs": "3.9"
      }
    ]
  ]
}
// package.json
{
  ...
  "main": "src/js/index.js",
  "license": "MIT",
  "type": "module",
  "engines": {
    "node": ">=14"
  },
  "scripts": {
    "start": "webpack serve --mode=production",
    "start:dev": "webpack serve --mode=development",
    "build": "webpack --mode=production",
    "build:dev": "webpack --mode=production",
  },
  "dependencies": {
    "core-js": "^3.9.1",
    "regenerator-runtime": "^0.13.7"
  },
  "devDependencies": {
    "@babel/core": "^7.13.10",
    "@babel/preset-env": "^7.13.10",
    "babel-loader": "^8.2.2",
    "css-loader": "^5.1.3",
    "cypress": "^6.7.1",
    "html-webpack-plugin": "^5.3.1",
    "mini-css-extract-plugin": "^1.3.9",
    "webpack": "^5.26.0",
    "webpack-cli": "^4.5.0",
    "webpack-dev-server": "^3.11.2"
  },
}

 

 

참고자료

webpack 공식홈페이지

프론트엔드 개발환경의 이해: 웹팩(기본)

웹팩 핸드북