요즘 잘나가는 프론트엔드 개발 환경 만들기(2018): Webpack 4

최근에 새로운 프로젝트를 진행하게 되면서 기존에 간만 살짝 보고 있던 뷰(Vue, 이하 뷰)를 본격적으로 도입하게 되었다. 사실 팀에서는 리액트를 선호하고 있었으나 회사 내부에서 뷰의 사용이 빠른 속도로 증가하고 있었고 그에 따라 팀내에서도 뷰에 대한 전문성이 필요하게 되었다. 뷰와 리액트는 서로 영향을 주고받고 발전해나가는 도구들이라 비슷한 부분이 많아 큰 어려움 없이 적응할 수 있었다. 새로운 프로젝트에서는 뷰를 사용하는것 뿐 아니라 다른 개발 환경도 요즘 것들로 업그레이드 해봤다. 웹팩 4 사용하고 테스트 러너도 기존에 팀 내에서 표준처럼 사용하던 카르마(Karma)에서 요즘(이미) 잘나가는 제스트(Jest)로 새롭게 시도해봤다.

웹팩 4에 대한 내용을 시작으로 3회에 걸쳐 웹팩 4 + ES6 + 뷰 2 + 제스트 개발 환경에 대한 글을 공유할 예정이다. 내용은 기존에 웹팩을 사용했던 개발자와 처음 웹팩을 접하는 개발자 모두를 대상으로 작성했다.

웹팩 4 에서 달라진점

몇 달 전에 웹팩 4가 나왔다. 가장 큰 변화라고 할 수 있는 것은 개발 환경에 맞게 기본적으로 설정이 되어 있는 Development 모드와 프로덕션 환경에 맞게 설정이 되어 있는 Production 모드가 생긴 것이다. Parcel의 장점인 심플한 사용성을 수용한것으로 보인다. 그리고 모드에 따라 적용되는 옵션이 달라졌다. 변경된 내용을 조금 정리해봤다.

  • 빌드 속도가 빨라졌다. 개발팀이 강한 자신감을 보인 부분이기도 하다. 최대 98%까지 빨라질 수 있다고 한다. 멀티코어를 사용하게 된 것도 아닌데 이 정도면 놀라울 정도의 최적화가 이뤄진것 같다.
  • webpack 코어와 webpack-cli 가 분리 배포 된다.
  • 모드가 생겨 일정한 규칙만 지키면 설정 파일이 없이도 빌드가 가능하게 되었다. 상단에 서술한 Production, Development 모드를 말한다. 0CJS라고 표현해서 뭔가 봤더니 Zero configuration Javascript란다.
  • CommonsChunkPlugin이 deprecated되고 SplitChunksPlugin으로 내장되었으며 optimization.splitChunks라는 옵션이 생겼다.
  • 특별한 작업없이 WebAssembly 파일(wasm)을 직접 import해서 사용할 수 있다. 웹팩 4에서는 실험적인 수준이고 웹팩 5에서 안정적으로 지원한다고 한다.

기본 번들 환경

이번 프로젝트에서도 개발 서버를 띄워 작업을 할 수 있게 하고 단위 개발이 끝나면 프로덕션 빌드까지 만들 목적으로 웹팩을 사용한다. 우선 기본적인 번들 환경부터 시작해보자. 적당한 이름의 프로젝트 디렉터리를 만들고 노드 프로젝트를 생성한다.

npm init

npm init 에서는 늘 그렇듯 일단 엔터를 연타한다. 옵션을 사용하면 엔터 연타 없이 디폴트로 package.json 만드는 방법이 있었던것 같은데 개인적으로 엔터 연타를 선호한다. (-y 옵션이다.)

npm install --save-dev webpack webpack-cli

webpackwebpack-cli 를 설치한다. 번들링 환경은 글로벌에 설치하지 않도록 한다. mode를 이용한 0CJS 빌드를 테스트해보기 위해 간단한 코드를 작성한다.

//src/index.js
import sayHello from './sayHello';

console.log(sayHello());

//src/sayhello.js
export default function sayHello() {
  return 'HELLO WEBPACK4';
}

src 디렉터리를 만들고 그 안에 위의 파일을 각각 생성한다. 이제 설정 없이 각 환경에 맞는 빌드를 만들어 보자.

npx webpack --mode development

npx 는 현재 프로젝트에 설치된 디펜던시를 마치 Path로 잡아 놓은 것 마냥 바로 사용할 수 있게 해주는 명령이다. npm에 포함되어 있다. 모드 옵션을 development로 주었다.

img

실행한 결과로 dist/main.js 로 번들링된 파일이 만들어졌다. development 모드로 만들어진 번들 파일이라 압축은 되어 있지 않지만 소스맵이 포함되어 있다. 이쯤 되면 알 수 있지만 production 모드로 번들링을 하고자 할 때는 --mode 옵션의 값을 production 으로 주면 된다.

mode를 이용한 webpack.config.js

0CJS가 간편하긴 하지만 작은 프로젝트나 프로토타입용으로나 적합하지 실무에서는 세부 설정을 적용할 수밖에 없다. 기본적인 환경을 새 버전에 맞게 구성해보자. 웹팩 4에서는 mode 옵션이 필수 옵션이다. 즉 0CJS 사용할 때 뿐 아니라 webpack.config.js 으로 설정을 직접 구성해 사용할 때도 mode 옵션이 필요하다. 각 모드 별로 기본 디폴트 옵션값이 다르고 필요한 설정의 내용도 다르다. 모드별로 필요한 설정이 미리 잡혀있고 그것을 오버라이드하는 느낌으로 커스텀 설정을 한다. 우선 가장 기본적인 설정부터 한다.

const path = require('path');

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

설정이 없던 상태와 달라진 것은 번들링된 파일 이름이 app.bundle.js 로 달라진 것 뿐이다. 0CJS일 때의 번들 파일명이 main.js 인데 마음에 들지 않는다. 그리고 나중에 Chunk로 디펜던시 모듈들을 분리하려면 어차피 설정이 필요하다.

위 설정은 커맨드라인 명령에서 --mode 옵션을 반드시 넘겨줘야 한다. 웹팩 3을 사용하는 환경에서는 development와 production 빌드를 한개의 설정 파일을 공유하면서 커맨드라인에서 넘기는 옵션으로 분기를 태워서 개별적으로 적용하거나 설정 파일을 각각에 빌드에 맞게 따로 구성했다. 물론 웹팩 4에서도 가능한 일이다. 하지만 mode 옵션으로 인해 조금 더 나은 방법으로 바뀌었다. 우선 빌드에 따라 개별 파일 구성할 때는 mode 옵션이 설정에 추가되어야 한다. 개발 환경에서만 사용되는 설정 파일 webpack.config.dev.js 이 있다고 하면 파일 안의 설정에는 mode 옵션이 들어있어야 한다.

//webpack.config.dev.js

// ...
module.exports = {
  mode: 'development', // production 설정 파일에서는 'production'
// ...
}

이렇게 설정을 해두면 커맨드라인에서 --mode 옵션을 따로 주지 않아도 된다. 설정 파일에 mode 설정을 한 상태에서 커맨드라인에서 옵션을 주게 되면 설정 파일의 mode 옵션을 덮어쓰게 되니 주의해야 한다. 특정 설정 파일을 이용해 빌드할 때는 --config 옵션을 이용한다.

npx webpack --config webpack.config.dev.js

한 개의 설정을 공유하면서 커맨드라인에서 주어지는 옵션으로 각 빌드 별로 분기를 만들 때는 커맨드라인에서 주어지는 mode 옵션을 받아서 처리한다. 그러기 위해서는 우선 설정 파일의 설정이 객체의 형태가 아니라 객체를 반환하는 함수의 형태여야 한다. 일종의 콜백처럼 동작한다고 생각하면 된다.

module.exports = (env, options) => {
  const config = {
    entry: {
      app: ['./src/index.js']
    },
    output: {
      filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist')
    }
  }

  return config;
}

첫 번째 인자는 커맨드라인에서 전달해주는 --env 옵션들이 객체 형태로 전달 된다. webpack.EnvironmentPluginwebpack.DefinePlugin 를 이용하면 구현 코드에서도 해당 변수들을 전역에서 사용할 수 있게 해준다. 4 버전 이하에서는 --env 옵션을 이용해 어떤 빌드인지 구분했지만, 이제는 그럴 필요가 없어졌다. 두 번째 인자에는 커맨드라인에서 전달되는 모든 옵션이 객체 형태로 전달 된다. 여기에 mode 프로퍼티로 빌드 정보가 넘어온다.

module.exports = (env, options) => {
  const config = {
    entry: {
      app: ['./src/index.js']
    },
    output: {
      filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist')
    }
  }

  if(options.mode === 'development') {
    //... Development 설정
  } else {
    //... Production 설정
  }

  return config;
}

장단이 있기에 이번 프로젝트에서는 설정의 복잡도가 많이 높아지기전까지는 하나의 설정 파일을 공유하는것으로 결정했다.

Production 빌드 설정

프로젝트의 진행 내용상 현재로서는 프로덕션 빌드에서 고려해야 할 것은 코드 미니파이밖에 없다. 기존에는 UglifyWebpackPlugin 을 직접 설치한 뒤 미니 파이 설정을 직접 해줘야 했다. 하지만 웹팩 4에서는 UglifyWebpackPlugin 이 내장되어 따로 설치할 필요가 없어졌다. 내장되면서 webpack.optimize.UglifyJsPlugin 으로 직접 사용해 설정할 수 있는데 기본 설정만으로도 무리 없는 상태라 modeproduction 인것 만으로도 특별한 설정 없이 미니파이가 가능해졌다. 즉 추가 설치나 설정 없이 미니파이 가능하다. 그래서 따로 설정할 것은 없는데 빌드마다 기존의 dist 디렉터리를 지워주는 플러그인 정도만 사용했다.

플러그인을 설치하고

npm i --save-dev clean-webpack-plugin

간단한 설정을 해준다.

//...
const CleanWebpackPlugin = require('clean-webpack-plugin');

module.exports = (env, options) => {
  //...

  if(options.mode === 'development') {
    //...
  } else {
    // Production 설정
    config.plugins = [
      new CleanWebpackPlugin(['dist'])
    ];
  }

  return config;
}

이제 매 프로덕션 빌드마다 dist 디렉토리는 깔끔하게 지워진다.

Development 빌드 설정

Development 빌드를 따로 만들 일은 없으므로 Development 빌드 설정은 온전히 개발 서버 설정이다. webpack-dev-server 를 위한 설정을 하고 htmlWebpackPlugin 으로 서버를 띄울 때마다 임시 index.html 파일을 만들어 사용한다. 그리고 Hot Module Replacement(HMR)도 설정한다. HMR은 코드에 변경이 생겨 다시 빌드할 때 매번 브라우저를 리로드 할 필요 없이 변경된 모듈만 바로 교체하는 기능이다. 그래서 현재 테스트 중인 스테이트가 계속 유지된다는 장점도 있다. 그리고 빠질 수 없는 소스맵 설정도 들어간다.

우선 htmlWebpackPluginwebpack-dev-server 를 설치한다.

npm i --save-dev html-webpack-plugin webpack-dev-server

그리고 설정 파일에 필요한 설정을 추가한다.

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

module.exports = (env, options) => {
  //...

  if(options.mode === 'development') {
    config.plugins = [
      new webpack.HotModuleReplacementPlugin(),
      new HtmlWebpackPlugin({
        title: 'Development',
        showErrors: true // 에러 발생시 메세지가 브라우저 화면에 노출 된다.
      })
    ];

    config.devtool = 'inline-source-map';

    config.devServer = {
      hot: true, // 서버에서 HMR을 켠다.
      host: '0.0.0.0', // 디폴트로는 "localhost" 로 잡혀있다. 외부에서 개발 서버에 접속해서 테스트하기 위해서는 '0.0.0.0'으로 설정해야 한다.
      contentBase: './dist', // 개발서버의 루트 경로
      stats: {
        color: true
      }
    };
   } else {
     //...
  }

  return config;
}

splitChunks

기존에 CommonsChunkPlugin을 이용해 사용에 맞게 자동으로 번들 파일을 분리했던 기능을 splitChunk 옵션을 통해 할 수 있다. splitChunk를 이용하면 대형 프로젝트에서 거대한 번들 파일을 적절히 분리하고 나눌 수 있다. 파일 사이즈, 비동기 요청 횟수 등의 옵션에 따라 자동으로 분리할 수 있고 정규식에 따라서 특정 파일들만 분리할 수 있고 혹은 특정 엔트리 포인트를 분리할 수 있다. 번들 파일을 적절히 분리하면 브라우저 캐시를 전략적으로 활용할 수 있으며 초기 로딩속도를 최적화할 수도 있다. 물론 프로젝트의 필요에 따라 엔트리 포인트를 분리해서 여러 가지 번들 파일을 만들 때도 사용된다. splitChunks 에 대한 자세한 이야기는 여기 에서 확인할 수 있다.

자주 사용되는 코드 분리는 npm으로 설치한 디펜던시 모듈과 실제 구현코드와 번들 파일을 분리하는 것이다. 설정에 아래와 같은 옵션을 추가한다.

//..
module.exports = (env, options) => {
  const config = {
    //..
    output: {
      filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist')
    },
    optimization: {
      splitChunks: {
        cacheGroups: {
          commons: {
            test: /[\\/]node_modules[\\/]/,
            name: 'vendors',
            chunks: 'all'
          }
        }
      }
    }
  }

  //..
}

cacheGroups 는 명시적으로 특정 파일들을 청크로 분리할 때 사용한다. 여기서는 common 이란 청크를 분리한다. 내용을 살펴보면 test 를 사용해 대상이 되는 파일을 정규식으로 잡는다. 여기서는 node_modules 디렉터리에 있는 파일들이다. name 은 청크로 분리할 때 이름으로 사용될 파일명이다. 우리의 설정에서는 output.filename 옵션에 [name] 에 대치될 내용이기도 하다. chunks 는 모듈의 종류에 따라 청크에 포함할지 말지를 결정하는 옵션이다 initialasync 그리고 all 이 있다. 여기서는 all 을 사용하는데 말 그대로 test 조건에 포함되는 모든 것을 분리하겠다는 뜻이다. initial 은 초기 로딩에 필요한 경우, asyncimport() 를 이용해 다이나믹하게 사용되는 경우에 분리한다.

분리된 파일들은 서버가 열리면 HtmlWebpackPlugin 이 알아서 index.html에 주입해준다. 물론 production 빌드를 하면 분리된 번들 파일 두개가 생성된다

package.json에 script 추가하기

자 이제 일반적인 webpack 설정은 모두 끝났다. package.json에 script 옵션을 추가해 쉽게 웹팩을 실행할 수 있게 하자.

//...
"mian": "index.js",
"scripts": {
  "build-dev": "webpack --mode development",
  "build": "webpack --mode production",
  "dev": "webpack-dev-server --open --mode development",
},
//...

npm run dev 로 개발 서버를 실행할 수 있고 --open 옵션으로 이제 서버가 뜨면 자동으로 브라우저가 열고 서버에 접속까지 해준다. npm run build 로 production 빌드를 할수 있다. 필요에 따라 npm run build-dev 로 development 빌드를 수행할 수도 있다.

최종 설정 파일

이렇게 만들어진 최종 설정 파일 webpack.config.js 는 아래와 같다.

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

module.exports = (env, options) => {
  const config = {
    entry: {
      app: ['./src/index.js']
    },
    output: {
      filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist')
    },
    optimization: {
      splitChunks: {
        cacheGroups: {
          commons: {
            test: /[\\/]node_modules[\\/]/,
            name: 'vendors',
            chunks: 'all'
          }
        }
      }
    }
  }

  if(options.mode === 'development') {
    config.plugins = [
      new webpack.HotModuleReplacementPlugin(),
      new HtmlWebpackPlugin({
        title: 'Development',
        showErrors: true
      })
    ];

    config.devtool = 'inline-source-map';

    config.devServer = {
      hot: true,
      host: '0.0.0.0',
      contentBase: path.resolve(__dirname, 'dist'),
      stats: {
        color: true
      }
    };
  } else {
    config.plugins = [
      new CleanWebpackPlugin(['dist'])
    ];
  }

  return config;
}

마무리

웹팩 4를 이용해 기본적인 번들부터 개발 서버 그리고 splitChunk까지 설정했다. 확실히 이번 버전부터는 설정이 간결해지고 사용이 쉬워진 것을 느낄 수 있었다. 웹팩 4가 나온 시점에는 플러그인들의 지원이 조금 아쉬운 편이었는데 이제는 자주 사용되는 플러그인이나 로더는 모두 무리 없이 사용할 수 있고 얼마 전엔 여러가지 문제를 개선한 4.6 버전이 배포되었다. 다음에는 ES6와 Vue 2의 개발 환경을 추가하고자 한다.