본문으로 바로가기

Webpack Guide for beginner #3

category Web Tech/Webpack 2019. 11. 19. 10:50

Webpack 기본 개발 환경을 위한 설정

앞선 챕터에서 webpack 의 기본 주요 속성들에 대해 알아보았습니다.

이번 장에서는 자주 사용되는 기본 개발 환경을 설정하면서 기타 유용한 사항들에 대해 알아보도록 하겠습니다.

참고로 여기서는 SASS, webpack-dev-server 등을 사용해 보도록 하겠습니다.

 

webpack.config.js 개발환경 설정 사진

 

 

 

프로젝트 초기 생성

앞선 내용을 따라 왔다면 nodeJSwebpack global 이 설치되어 있을 것입니다. 만약 웹팩에 필요한 기본 설정이 되어 있지 않다면 이전 장을 참고하고, 여기서는 nodeJSwebpack global 이 설치되어 있음을 가정으로 진행합니다.

 

먼저 node, npm 버전을 확인 후 아래와 같이 디렉토리를 만들어 로컬에 webpackwebpack-cli(커맨드라인에서 웹팩을 실행하는 도구)를 설치 및 package.json을 생성하는 초기 프로젝트 단계을 진행합니다.

프로젝트 초기 단계 설정

# npm, node 설치 확인
$ node -v
$ npm -v
$ mkdir getting-started && cd getting-started
$ npm init -y
$ npm i webpack webpack-cli -D

CLI를 사용하지 않고 초기 디렉토리 생성은 사용자 편의대로 진행해도 무방합니다.

 

package.json 파일을 보면 아래와 같이 설치된 패키지를 확인하실 수 있습니다.

// package.json
{
  "name": "getting-started",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "webpack": "^4.36.1",
    "webpack-cli": "^3.3.6"
  }
}

 

Version 보기 & 특정버전 설치

다음과 같이 웹팩의 특정 버전을 설치할 필요가 있다면 사용하고자 하는 버전을 확인한 후에 특정 버전을 설치할 수 있습니다.

$ npm view webpack versions --json
$ npm install webpack@3.8.1 --save-dev

 

예를 들어 jQuery의 특정 버전을 설치하려고 하는 경우 특정 버전을 모를 때  npm view jquery versions --json 를 통해 버전을 확인하고, npm install jquery@2.2.4 --save 처럼 설치(jQuery 는 서비스시에 필요한 의존성이므로 --save 로 설치)할 수 있습니다.

CLI에서 아래와 같이 jQuery versions을 확인할 수 있습니다.

[
	"1.5.1",
	"1.6.2",
	"1.6.3",
	생략 ...
	"2.2.4",
	"3.0.0-alpha1",
	"3.0.0-beta1",
	"3.0.0-rc1",
	"3.0.0",
	생략...
	"3.4.0",
	"3.4.1"
]

 

webpack.config.js 작성

webpack.config.js 파일을 만들어 기본적인 웹팩에 필요한 초기 코드를 아래와 같이 작성해 봅니다.

const path = require('path');

module.exports = {
    entry: './src/assets/js/main.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.app.js'
    },
    mode: 'development'
};

기본적인 entry와, mode 그리고 앞선 장에서 알아본 path 모듈을 사용하여 output 을 사용하였습니다.

 

그리고 아래와 같이 js 폴더main.js, sass 폴더index.html 을 추가로 진행하였습니다.

getting-started
	│
	├─node_modules
	│  └─ ....
	│  └─ ....
	│  └─ 다수 모듈들
	│
	└─src
	│	│
	│	├─js
	│	│  └─main.js
	│	│
	│	└─sass
	│
	├─index.html
	├─package.json
	└─webpack.config.js

 

index.html 은 아래와 같이 기본 템플릿만 작성하였습니다.

html
<!DOCTYPE html>
<html lang="ko">
<head>
	<meta charset="UTF-8">
	<title>Webpack beginner</title>
</head>
<body>

</body>
</html>

 

main.js 는 단순히 로그만 작성해 봅니다.


// main.js
console.log( 'mainJS 테스트 실행' );

 

 

플러그인 html-webpack-plugin 설치

html-webpack-plugin은 번들 된 파일을 <script />로 로드한 html 파일을 자동으로 생성해 주는 plugin입니다.

앞서 진행했 듯이 기본적으로, 번들링한 css, js 파일들은 html 파일에 <script/><link rel="stylesheet" href="style.css"> 코드를 직접 추가 작성해야하는 번거로움이 있습니다.

이런 번거로운 작업을 html-webpack-plugin을 사용하면 이 과정을 자동화 할 수 있습니다.

Webpack의 성능을 향상시키고 개발 편리성 목적이 이 플러그인의 역할입니다.
다시 말해, 설정에 따라 새로운 html 파일을 생성할 수도, 기존의 html 에 번들 된 파일을 <script/>로 로드한 html 파일을 생성 할 수도 있습니다. 그리고 해시(hash)된 파일 이름을 사용하는 webpack 번들을 로드하는데 유용하게 사용됩니다.

 

html-webpack-plugin 플러그인 사용을 위해 아래와 같이 설치합니다.

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

 

설치가 완료되면 webpack.config.js 파일을 아래와 같이 수정합니다.

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin'); // html-webpack-plugin 불러옴

module.exports = {
    entry: './src/js/main.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.app.js'
    },
    // 플러그인 설정
    plugins: [
        new HtmlWebpackPlugin({
            template: 'index.html', // 빌드 전에 사용되는 템플릿
            filename: 'index.html' // 빌드 후에 생성될 파일명
        })
    ],
    mode: 'development'
};

 

그리고 터미널(CLI)에서 webpack 명령어를 실행한 후 dist/index.html이 생성되고 아래와 같이 임포트된 script를 확인할 수 있습니다.

html
<!DOCTYPE html>
<html lang="ko">
<head>
	<meta charset="UTF-8">
	<title>Webpack beginner</title>
</head>
<body>

<script type="text/javascript" src="bundle.app.js"></script></body>
</html>

그리고 main.js에서 작성한 로그는 브라우저 개발자 도구의 console 패널에서 확인하실 수 있습니다.

 

html-webpack-plugin에 대한 자세한 사용법은 다음의 링크에서 확인해 보실 수 있습니다.

 

 

Custom JS & library 사용하기

이제 사용자 스크립트와 기타 라이브러리를 사용하면서 번들링해 보도록 하겠습니다.

여기서는 jQuerymoment를 간단히 사용하면서 JS 파일을 번들링해 봅니다.

 

jQuerymoment 를 사용을 위해 다음과 같이 라이브러리를 다운로드 받습니다.

$ npm i -S jquery moment

 

라이브러리 설치가 완료되었다면 jquery, moment 를 사용을 위해 다음과 같이 index.html을 작성하고 main.jsmodule01.js, module02.js를 추가 생성하여 다음과 같이 작성해 봅니다.

html
<!DOCTYPE html>
<html lang="ko">
<head>
	<meta charset="UTF-8">
	<title>webpack beginner</title>
</head>
<body>

	<header>
		<h3>Libraries Code Splitting</h3>
	</header>
	<div>
		<label><strong>Moment JS : </strong></label>
		<p class="main-moment">
			not yet Moment loaded
		</p>
		<br>
		<label><strong>jQuery : module1.js</strong></label>
		<p class="jQ">jQuery 사용전</p>
		<br>
		<label><strong>Moment JS : module01.js</strong></label>
		<p class="module01">module1 에서 사용</p>
		<br>
		<label><strong>jQuery : module02.js</strong></label>
		<p class="module02">module2 사용</p>
	</div>

</body>
</html>

 

main.js에서는 moment 라이브러리를 사용.

// main.js

import moment from 'moment';
const ele = document.querySelector('.main-moment');

document.addEventListener("DOMContentLoaded", function(event) {
    ele.innerText = moment().format();
});

 

module01.js에서는 jQuerymoment 라이브러리를 동시 사용.

// module01.js

import $ from 'jquery';
const moment = require('moment');
// require(), import '' 등의 모듈 로딩시에 어느 폴더를 기준할 것인지 정하는 옵션

$(function() {
    $('.jQ').text('module01.js 에서 jQuery 를 사용하고 있습니다!!!');
});
$('.module01').text( moment().format());

 

module02.js 에서는 jQuery 라이브러리만 사용.

import $ from 'jquery';

$(function() {
    $('.module02').text('module-02.js 에서 jQuery 를 사용중입니다.');
});

 

마지막으로 webpack.config.js를 업데이트하도록 합니다.

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin'); // html-webpack-plugin 불러옴

module.exports = {
    entry: {
        'index': ['./src/js/main.js'], // index.js 생성
        'module': ['./src/js/module01.js', './src/js/module02.js'] // module.js 생성
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].js'
    },
    // 플러그인 설정
    plugins: [
        new HtmlWebpackPlugin({
            template: 'index.html',
            filename: 'index.html'
        })
    ],
    mode: 'development'
};

 

위와 같이 작성 완료되었다다면 터미널에서 webpack을 실행해 보세요.

dist/index.html 에서 jQuerymoment 가 로드되어 사용되고 있는 것을 확인할 수 있습니다.

 

 

splitChunks

위에서 작성한 방법에는 문제점이 있습니다.

라이브러리는 잘 동작하지만 번들링된 index.jsmodule.js를 확인해 보면 라이브러리(jQuery, moment)가 각각 로드되어 각 파일에 포함되어 있습니다. 이는 당연히 사용자가 원치않는 구성일 뿐더러 일반적인 방법이 아닙니다.

이를 해결하기 위해서는 index.jsmodule.js에 중복된 라이브러리인 코드 분할이 필요하게 됩니다.

코드 분할(Code Splitting)은 웹팩이 제공하는 좋은 기술 중 하나로, 엔트리에서 사용하는 공통된 작은 번들로 나눌 수 있어서 애플리케이션을 로드하는 시간에 영향을 줄 수 있습니다.

위 코드 main.js, module01.js, module02.js 의 각 청크간에 중복되는 패키지(여기선 라이브러리)들이 존재하고 있으며, 이렇게 청크(chunk)간에 겹치는 패키지들을 별도의 파일로 추출하여 번들링할 수 있는데 이를 웹팩에서는 vendor 부르고 있습니다.

벤더를 만드는 이유는 한 파일이 (a, b, c) 패키지를 가지고 있고, 또 다른 파일이 (a, b, d) 패키지를 가지고 있다면, a와 b 패키지가 겹치기 때문에 두 번 로드하여 쓸모없는 용량을 차지하게 됩니다. 이런 것은 vendor~A~B (a, b)로 만들어주고, A 청크는 (c), B 청크는 (d)로 만들어 중복을 최소화주어야 하기 때문입니다.

 

이전 webpack v3에서는 직접 벤더를 지정해야 했으나, v4에서는 웹팩이 알아서 벤더를 생성하여 줍니다.

vendor 를 사용하기 위해서는 webpack.config.jsoptimization 속성을 추가하고 다음과 같이 정의해 줄 수 있습니다.

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin'); // html-webpack-plugin 불러옴

module.exports = {
    entry: {
        'index': ['./src/js/main.js'],
        'module': ['./src/js/module01.js', './src/js/module02.js'],
        // vendor: ['lodash', 'jquery'], // webpack v4 이전 방식
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].js',
        // chunkFilename : '[name].js' // webpack v4 이전 방식
    },
    // 플러그인 설정
    plugins: [
        new HtmlWebpackPlugin({
            template: 'index.html',
            filename: 'index.html'
        })
    ],
    // optimization 로 중복된 모듈 없애기
    optimization: {
		// Splitting Duplicated Chunk
        // 전체 응용 프로그램의 vendors 모든 코드를 포함 하는 청크
		// 즉, 자주 사용되어 중복으로 import 된 모듈을 별도의 chunk 파일로 생성하기 위한 설정이다.
		// 번들 파일을 적절히 분리함으로써 브라우저 캐시를 전략적으로 활용할 수 있어 초기 로딩속도를 최적화할 수 있다.
        splitChunks: {
			// cacheGroups : 명시적으로 특정 파일들을 청크로 분리할 때 사용
            cacheGroups: {
                vendors: {
					// 대상이 되는 파일 지정(여기서는 node_modules 디렉터리에 있는 파일들이 대상)
                    test: /[\\/]node_modules[\\/]/,
					// 비동기 및 동기 모듈을 통한 최적화(test 조건에 포함되는 모든 것을 분리하겠다는 뜻)
                    chunks: 'all',
					// 청크로 분리할 때 이름으로 사용될 파일명
                    name: 'libs',
                }
            }
        }
    },
    mode: 'development'
};

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

 

chunks 옵션

  • all : test 조건에 해당하는 모든 모듈

    이봐, 웹팩!! 동적으로 가져온 모듈 또는 비동기적으로 가져온 모듈인지 상관하지 않고 이들 모두에 최적화를 적용해줘..

  • initial : 초기 로딩에 필요한 경우

    이봐, 웹팩! 동적으로 가져온 모듈에 관한 건 상관 없는데 그들 각각에 대해 별도의 파일을 가질 수 있게 해줘,
    대신 비동기적으로 가져온 모듈을 다른 모듈과 공유하고 청크할 준비가되어 있지만 비동기적으로 가져온 모듈을 하나의 번들로 모두 가져오려고 해..

  • async : import() 를 이용해 다이나믹하게 사용되는 경우

    이봐, 웹팩! 난 동적으로 가져온 모듈의 최적화에만 관심있으니 비동기적으로 가져온 모듈은 그대로 둬..

 

webpack.config.js를 위와 같이 업데이트했다면 다시 webpack을 실행해 보세요.

실행한 결과 dist/index.js, module.js, libs.js가 생성되어 있으며 libs.js 파일 내부에 jquery, moment 라이브러리가 함께 포함되어 있는 것을 확인하실 수 있습니다.

그리고 dist/index.html 을 확인해 보면 웹팩이 자동으로 스크립트 의존성에 맞게 다음과 같이 import하고 있는 것을 확인하실 수 있습니다.

html
<script type="text/javascript" src="libs.js"></script>
<script type="text/javascript" src="index.js"></script>
<script type="text/javascript" src="module.js"></script>

 

 

dist 내부 폴더 나누기 & 편하게 라이브러리 로드(ProvidePlugin)하여 사용하기

지금까지는 dist 폴더내에 자원들을 하위폴더로 분리하지 않고 사용하였으나 좀더 실무에 적합하도록 앞으로 사용할 cssjs 폴더를 나누기 위해서 하위 폴더를 구성해 보도록 하고, main.js, module01.js, module02.js에서 라이브러리를 import 하여 사용했던 구문,   예를 들어, jQuery 를 사용하고 있는 모든 모듈 JS 마다 import $ from 'jquery'; 불러온다면 꽤나 번거로운 일입니다.
이를 수정하여 좀더 편하게 라이브러리를 로드하여 사용하는 방법에 대해 알아보도록 하겠습니다.

 

webpack.config.js를 다음과 같이 수정하도록 합니다.

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

module.exports = {
    entry: {
        // 여기서는 output.path 의 dist 기준으로 폴더 생성
        'js/index': ['./src/js/main.js'],
        'js/module': ['./src/js/module01.js', './src/js/module02.js'],
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].js',
    },
    // 플러그인 설정
    plugins: [
        new HtmlWebpackPlugin({
            template: 'index.html',
            filename: 'index.html'
        }),

        // 모든 라이브러리를 불러올 때
        new webpack.ProvidePlugin({
            // 라이브러리 로딩
            $: 'jquery',
            jQ : 'jquery',
            moment : 'moment'
        })
    ],
    optimization: {
        splitChunks: {
            cacheGroups: {
                vendors: {
                    test: /[\\/]node_modules[\\/]/,
                    chunks: 'all',
                    // 여기서는 output.path dist 기준으로 폴더 생성
                    name: 'js/vendor/libs',
                }
            }
        }
    },
    mode: 'development'
};

import $ from 'jquery'; 와 같은 구문을 사용하지 않고 라이브러리를 불러오고자 할 경우에는 웹팩에서 제공하는 풀러그인 ProvidePlugin를 사용할 수 있습니다.   위와 같이 라이브러리를 담아올 별칭을 정하고 라이브러리의 node_module 이름을 작성하면 됩니다.
그리고 하위 폴더를 구성하고자 하는 경우에는 entrykey/ 를 구분자로 사용할 수 있습니다.
예를 들어, 현재 webpack.config.js 를 기준으로 진행할 경우 key 를 'js/common' 작성했다면 dist/js/common.js 가 번들링될 것입니다.
즉, 마지막 /(슬래시)의 다음 문자열이 output.filename'[name]' 으로 치환되게 됩니다.

webpack.config.js를 위와 같이 수정하였다면 기존에 작성했던 main.js, module01.js, module02.js 를 아래와 같이 수정해 보도록 합니다.

 

main.js 에서 import 구문 제거

// main.js

const ele = document.querySelector('.main-moment');

document.addEventListener("DOMContentLoaded", function(event) {
    ele.innerText = moment().format();
});

 

module01.js 에서 import, require 구문 제거

// module01.js

$(function() {
    $('.jQ').text('module01.js 에서 jQuery 를 사용하고 있습니다!!!');
});
$('.module01').text( moment().format());

 

module02.js 에서 import, require 구문 제거

// module02.js

jQ(function() {
    jQ('.module02').text('module-02.js 에서 jQ 로 치환하여 사용중입니다.');
});

모든 JS 파일 수정 후에 webpack을 실행하면 dist/index.html에서 이상없이 라이브러리가 잘 로드되어 동작하고 있을 것입니다.   

그리고 dist 하위 디렉토리 구성 또한 다음과 같이 구성되어 있을 것입니다.

dist
  │
  └─js
  │	├─vendor
  │	│  └─libs.js
  │	│
  │	├─index.js
  │	└─module.js
  │
  └─index.html

 

 

 

webpack sass 컴파일

이번에는 웹팩을 통해 sass를 컴파일하는 방법에 대해 살펴봅니다.
css만 번들링하는 것보다는 sass를 번들링해 보면서 기타 loader(로더)들의 쓰임새를 알아보는 것이 웹팩을 알아보는데 효과적일 것입니다.  

먼저 필요한 패키지를 다음과 같이 설치하도록 합니다.

$ npm install node-sass style-loader css-loader sass-loader --save-dev

node-sassnode.js 환경에서 사용할 수 있는 Sass 라이브러리로 실제로 Sass 를 css 로 컴파일하는 것은 node-sass 이기 때문에 node-sass 설치가 필요하며, style-loader, css-loader, sass-loader 는 Webpack 플러그인입니다.

그리고 앞선 내용에서 언급했던 mini-css-extract-plugin(컴파일된 css 를 별도의 css 파일로 분리)를 설치하도록 합니다.

$ npm install --save-dev mini-css-extract-plugin

 

Sass 컴파일을 위한 sass 테스트 파일은 아래의 압축파일을 받으신 후 src/sass 폴더로 복사해서 사용하시기 바랍니다.

현재까지 src 폴더 구성은 다음과 같습니다.

src
  ├─js
  │      main.js
  │      module01.js
  │      module02.js
  │
  └─sass
  	  ├─common
  	  │      _extend.scss
  	  │      _layout.scss
  	  │      _reset.scss
  	  │
  	  └─partials
  	  │		_extend.scss
  	  │		_mixins.scss
  	  │		_variables.scss
  	  │
  	  │  _common.scss
  	  │  _partials.scss
  	  │  pages.scss

 

이제 sass 컴파일을 위한 webpack.config.js 를 수정하도록 합니다.

const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); // mini-css-extract-plugin 로드

module.exports = {
    entry: {
        'js/index': ['./src/js/main.js', './src/sass/pages.scss'], // sass 파일 추가
        'js/module': ['./src/js/module01.js', './src/js/module02.js'],
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].js',
    },
    module: {
        rules: [
            {
                // 대상 파일 지정
                test: /\.s(a?c)ss$/, // /\.(sa|sc|c)ss$/, /\.s(a?c)ss$/,
                use: [
                    // 트랜스파일링이 된 것을 외부 css 파일로 추출하는 역할
                    {
                        loader: MiniCssExtractPlugin.loader,
                        options: {
							// publicPath 는 Webpack 이 번들을 (선택적으로)로드 할 곳입니다.
                        	// entry 가 현재 'js/index' 로 js 폴더 내에 생성하지 않고 상위폴더로 빼내기 위함
                            publicPath: '../'
                        }
                    },

                    // css-loader : css 를 CommonJS 방식의 js 로 트랜스파일링 하는 역할
                    'css-loader',

                    // sass-loader : 기본적으로 node-sass 를 사용하여 sass 를 css 로 컴파일하는 역할
                    {
                        loader: 'sass-loader',
                        options: {
                            outputStyle: 'expanded',
                            indentType: 'tab', // 정의되어 있지 않으면 기본값은 space
                            indentWidth: 1 // 기본값 2
                        }
                    }
                    // "sass-loader?outputStyle=expanded", // outputStyle=compressed
                ],
                exclude: /node_modules/
            }
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: 'index.html',
            filename: 'index.html'
        }),

        new webpack.ProvidePlugin({
            $: 'jquery',
            jQ : 'jquery',
            moment : 'moment'
        }),

        // 컴파일 + 번들링 CSS 파일이 저장될 경로와 이름 지정
        new MiniCssExtractPlugin({
            // MiniCssExtractPlugin.loader 의 publicPath: '../' 의 설정을 통해
            // js 폴더를 빠져나와 dist/css/style.css 를 생성하게 됨
            filename: 'css/style.css'
        })
    ],
    optimization: {
        splitChunks: {
            cacheGroups: {
                vendors: {
                    test: /[\\/]node_modules[\\/]/,
                    chunks: 'all',
                    name: 'js/vendor/libs',
                }
            }
        }
    },
    mode: 'development'
};

작성이 끝난 후 webpack을 실행해 보면 dist/css/style.css 가 생성되어 있는 것을 확인하실 수 있습니다.

 

그리고 성격이 다른 모듈 분리 차원에서 다음과 같이 entry에 작성한 './src/sass/pages.scss' sass 를 삭제하고 main.js 내에 import 구문을
이용하는 편이 좋습니다.

sass 모듈 엔트리 포인트에서 제거 후 import 구문을 이용하여 로드하기

import '../sass/pages.scss'; // sass 로드
const ele = document.querySelector('.main-moment');

document.addEventListener("DOMContentLoaded", function(event) {
    ele.innerText = moment().format();
});

 

풍부한 CSS 환경을 위한 PostCSS

PostCSS 는 JS 플러그인을 사용하여 CSS 를 변환시키는 도구로서 변수, mixin 을 사용하거나, 인라인 이미지 또는 미래의 CSS 문법을 사용할 수 있습니다.  다시 말해, PostCSS 는 자바스크립트 기반의 플러그인을 사용하여 CSS 기능을 자동화하는 소프트웨어 개발 도구입니다.

PostCSS 플러그인에는 300여개에 달하는 플러그인들(PostCSS 플러그인 리스트)이 있으며 국내 환경에서 유용한 autoprefixer 도 사용할 수 있습니다.   여기서는 PostCSS 공식 홈페이지에서 소개하는 grid systemautoprefixer 를 간단히 사용해 보도록 하겠습니다.

 

먼저 PostCSS를 사용을 위해 패키지 postcss-loader, PostCSS의 플러그인 종류 중에 autoprefixer와 그리드 시스템 플러그인 lost를 설치하도록 합니다.

$ npm i -D postcss-loader autoprefixer lost

 

webpack.config.js를 업데이트를 위해 다음과 같이 작성합니다.

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

module.exports = {
    entry: {
        'js/index': ['./src/js/main.js'],
        'js/module': ['./src/js/module01.js', './src/js/module02.js'],
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].js',
    },
    module: {
        rules: [
            {
                test: /\.s(a?c)ss$/,
                use: [
                    {
                        loader: MiniCssExtractPlugin.loader,
                        options: {
                            publicPath: '../'
                        }
                    },
                    'css-loader',

                    // postcss-loader 설정
                    {
                        loader: 'postcss-loader',
                        options: {
                            ident: 'postcss',
                            plugins: [
                                require('lost'), // 그리드 시스템
                                require('autoprefixer'),
                                // require('autoprefixer')({
									//  browserslist를 package.json 또는 .browserslistrc file 로 사용을 권장함
									// 'browsers': ['> 1%', 'last 2 versions', 'not ie <=8']
									// 'browsers': ['cover 99.5%']
                                // }),
                            ]
                        }
                    },
                    {
                        loader: 'sass-loader',
                        options: {
                            outputStyle: 'expanded',
                            indentType: 'tab',
                            indentWidth: 1
                        }
                    }
                ],
                exclude: /node_modules/
            }
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: 'index.html',
            filename: 'index.html'
        }),
        new webpack.ProvidePlugin({
            $: 'jquery',
            jQ : 'jquery',
            moment : 'moment'
        }),
        new MiniCssExtractPlugin({
            filename: 'css/style.css'
        })
    ],
    optimization: {
        splitChunks: {
            cacheGroups: {
                vendors: {
                    test: /[\\/]node_modules[\\/]/,
                    chunks: 'all',
                    name: 'js/vendor/libs',
                }
            }
        }
    },
    mode: 'development'
};

위와 같이 module.rulespostcss-loader 를 작성하고 옵션으로 사용할 플러그인들을 추가하여 사용할 수 있습니다.

 

그리고 앞서 다운로드 받은 sass 파일 중 pages.scss 에 아래와 같은 코드를 확인하실 수 있습니다.

다음 코드는 PostCSS 공식 홈페이지에서 소개하는 그리드 시스템 문법(lost-column)으로 소개 그대로 컴파일이 되는지 확인해 봅니다.

.post-css {
	lost-column: 1/3;
}

 

이전까지 webpack 실행하면 위 코드 그대로 컴파일되었으나 이제 다시 webpack을 실행하면 다음과 같이 컴파일될 것입니다.

[컴파일 css 결과확인]

.post-css {
	width: calc(99.9% * 1/3 - (30px - 30px * 1/3));
}

.post-css:nth-child(1n) {
	float: left;
	margin-right: 30px;
	clear: none;
}

.post-css:last-child {
	margin-right: 0;
}

.post-css:nth-child(3n) {
	margin-right: 0;
	float: right;
}

.post-css:nth-child(3n + 1) {
	clear: both;
}

lost-column: 1/3; 이 공식 홈페이지에서 소개한 대로 잘 컴파일되고 있지만 한가지 이상한 점이 있습니다.

autoprefixer가 제대로 동작하지 않는 것으로 보여지고 있습니다. autoprefixer 문서를 확인해 보면 autoprefixerbrowserslist와 같이 사용된다고 안내하고 있습니다.

아래처럼 browserslist를 설치 후 위에서 주석처리된 부분을 제거하고 다시 실행해 보면 autoprefixer가 동작하는 것을 확인할 수 있습니다.

$ npm i -D browserslist

자세한 browserslist 의 옵션값은 다음의 링크인 browserslist 에서 확인하시기 바랍니다.

 

 

 

webpack-dev-server 설치하기

지금까지는 학습 목적으로 매번 소스를 수정할 때 마다 webpack을 실행해 왔습니다. 사실 명령창에서 webpack -w를 실행하면 파일이 변경될 때마다 지속적 관찰을 하면서 컴파일이 진행됩니다. 옵션 플래그인 -wwatch의 약어로 지속적인 관찰을 의미합니다.

하지만 개발 단계에서는 일반적으로 로컬에서 개발용 서버를 통해서 프로젝트를 수행하기 때문에 webpack -w를 실행하기 보다는 webpack-dev-server를 설치하여 사용하면 더 편리할 수 있습니다.

웹팩 데브 서버는 웹 애플리케이션을 개발하는 과정에서 유용하게 쓰이는 도구로 웹팩의 빌드 대상 파일이 변경 되었을 때 매번 웹팩 명령어를 실행하지 않아도 코드만 변경하고 저장하면 웹팩으로 빌드한 후 브라우저를 새로고침 해줍니다. 즉, 이 기능을 사용하면 소스 파일을 감시(지속적 관찰)하고 내용이 변경될 마다 번들을 다시 컴파일하고 소스가 변경될 때마다 수시로 새로고침을 하지 않아도 새로고침되는 live reloading 기능 또한 제공해 주고 있어서 편리합니다.

그렇기 때문에 매번 명령어를 치는 시간과 브라우저를 새로 고침하는 시간 뿐만 아니라 웹팩 빌드 시간 또한 줄여주기 때문에 웹팩 기반의 웹 애플리케이션 개발에 필수로 사용됩니다.

 

webpack-dev-server를 설치하고 webpack.config.jsdevServer를 추가해 보도록 합니다.

$ npm i --save-dev webpack-dev-server
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
    entry: {
        'js/index': ['./src/js/main.js'],
        'js/module': ['./src/js/module01.js', './src/js/module02.js'],
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].js',
    },
    module: {
        rules: [
            {
                test: /\.s(a?c)ss$/,
                use: [
                    {
                        loader: MiniCssExtractPlugin.loader,
                        options: {
                            publicPath: '../'
                        }
                    },
                    'css-loader',
                    {
                        loader: 'sass-loader',
                        options: {
                            outputStyle: 'expanded',
                            indentType: 'tab',
                            indentWidth: 1
                        }
                    }
                ],
                exclude: /node_modules/
            }
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: 'index.html',
            filename: 'index.html'
        }),
        new webpack.ProvidePlugin({
            $: 'jquery',
            jQ : 'jquery',
            moment : 'moment'
        }),
        new MiniCssExtractPlugin({
            filename: 'css/style.css'
        })
    ],
    optimization: {
        splitChunks: {
            cacheGroups: {
                vendors: {
                    test: /[\\/]node_modules[\\/]/,
                    chunks: 'all',
                    name: 'js/vendor/libs',
                }
            }
        }
    },
    devServer: {

        // 서버 포트 설정
        port: 9000,

		// contentBase 는 static 파일은 번들링되기 전이라든지 번들링된 이후의 결과물들을 총제적으로 한 번에 로딩할 수 있는 경로들을 의미합니다.
        // contentBase 는 웹팩 output.path 의 'publicPath' 와 동일해야 하며,
        // 정적 파일을 제공할 디렉토리 설정, 소스 파일을 감시하고 변경 될 때마다 번들을 다시 컴파일합니다
        // 다시 말해, 콘텐츠를 제공할 경로지정(정적파일을 제공하려는 경우에만 필요)
		// 기본값은 사용자가 작업하는 working directory 가 되고 특별히 개발계와 배포계로 나누려고 하는 경우
		// 즉, 개발 자원과 배포 자원을 분리한다고 했을 때 예를 들어 배포하는 자원의 디렉토리가 public 이라고 한다면
		// contentBase 의 위치를 public 으로 잡아 설정할 수 있습니다.
		// 여기서 주의할 점은 절대 경로를 사용해야 한다는 점입니다.
        // contentBase: path.resolve(__dirname, 'public'), // 'dist',

        // dev server 구동 후 브라우저 열기
        open: true,

        // 에러가 날 경우 브라우저에 표시
        // 이 옵션을 사용하지 않아도 개발자 도구 콘솔에서 알려주므로 반드시 사용하지 않아도 되지만
        // 에러가 났을 경우 확실히 알려주기 때문에 유용할 수 있다.
        // overlay: true,

        // hot: HotModuleReplacementPlugin 을 사용해 HMR 기능을 이용하는 옵션
        // 소스가 변경되면 자동으로 빌드되어 반영된다. 파일이 수정될 경우 그 부분에 대해 리로드를 해주는 옵션
        // hot: true,

        // host: 기본적으로 애플리케이션은 localhost 에서 서빙되지만 이 옵션을 이용해 다른 host 를 지정해줄 수 있다.
        // 또한 이 옵션에 ‘0.0.0.0’을 주면 개발중인 localhost 를 외부에서 접근할 수 있다.
        // host: '0.0.0.0'

		// compress : gzip 압축방식을 이용하여 웹 자원의 사이즈를 줄이는 방법
		// 웹 성능 최적화에 관한 기법으로 gzip(https://ko.wikipedia.org/wiki/Gzip) 은 파일들의 본래 크기를
		// 줄이는 것(minification, concatenation, compression)이 아니라 서버와 클라이언트 간의 압축 방식을 의미합니다.
		// compress : true
    },
    mode: 'development'
};

위에서 추가한 속성 devServer 에서 포트를 9000 그리고 open: true로 설정되어 있습니다.

 

webpack-dev-server 는 다음과 같이 실행할 수 있습니다.

webpack-dev-server

webpack-dev-server 를 실행하면 브라우저창이 자동으로 열리면서 localhost:9000 으로 index.html 을 확인하실 수 있습니다.

브라우저가 자동으로 열리는 이유는 devServer 의 옵션값으로   open: true 로 설정되어 있기 때문입니다.

만약, open을 설정하지 않았다면 아래와 같이 실행해야 합니다.

webpack-dev-server --open

 

웹팩 데브 서버의 특징

웹팩 데브 서버는 일반 웹팩 빌드와 다른점이 있습니다.

웹팩 데브 서버를 실행하여 웹팩 빌드를 하는 경우에는 빌드한 결과물이 파일 탐색기나 프로젝트 폴더에서 보이지 않습니다. 좀 더 구체적으로 얘기하자면 웹팩 데브 서버로 빌드한 결과물은 메모리에 저장되고 파일로 생성하지는 않기 때문에 컴퓨터 내부적으로는 접근할 수 있지만 사람이 직접 눈으로 보고 파일을 조작할 순 없습니다.

따라서, 웹팩 데브 서버는 개발할 때만 사용하다가 개발이 완료되면 웹팩 명령어를 이용해 결과물을 파일로 생성해야 합니다.

 

컴퓨터 구조 관점에서 파일 입출력보다 메모리 입출력이 더 빠르고 컴퓨터 자원이 덜 소모됩니다.

 

HMR(Hot Module Replacement)

HMR은 브라우저를 새로 고치지 않아도 웹팩으로 빌드한 결과물이 웹 애플리케이션에 실시간으로 반영될 수 있게 도와주는 설정입니다.
브라우저 새로 고침을 위한 LiveReload 대신에 사용할 수 있으며 웹팩 데브 서버와 함께 사용할 수도 있습니다.

리액트, 앵귤러, 뷰와 같이 대부분의 프레임워크에서 이미 HMR을 사용할 수 있는 로더들을 지원하고 있지만 만약 개별적으로 설정하고 싶다면
아래와 같은 방식으로 설정할 수 있습니다.

HMR 설정하기

module.exports = {
  devServer: {
    hot: true
  }
}

데브 서버에 옵션으로 hot:true를 추가하고 자바스크립트나 CSS 스타일시트를 변경하면 해당 모듈이 바로 업데이트가 됩니다. 그리고 화면에서는 브라우저가 다시 로딩되지 않고도 변경된 내용을 확인할 수 있습니다.

 

 

태스크 실행 스크립트 추가하기

npm을 태스크 러너로 사용해 긴 명령어를 실행하는 것은 꽤나 번거로운 일일 수 있습니다.

위에서 실행한 webpack-dev-server에 옵션 플래그인 open 까지 명령어를 수행한다면 webpack-dev-server --open을 매번 타이핑해줘야 합니다. 하지만 반복적이고 자주 사용하는 명령어를 package.jsonscripts에 명령어를 등록해 놓는다면 좀더 수월하게 명령을 수행할 수 있습니다.

여기서는 위에서 수행했던 webpack-dev-serverscripts에 등록해 보도록 하겠습니다.

그리고 기존 devServer 에 작성한 open: true 도 제거한 후 진행해 주시기 바랍니다.

 

package.json 수정

"scripts": {
    "start": "webpack-dev-server --open",
},

 

package.json 을 수정하셨다면 아래와 같이 수행해 보시기 바랍니다.

$ npm start

위 명령어를 수행하면 webpack-dev-server 가 동작될 것입니다.

여기서는 start 를 타이핑했지만 package.json에 등록한 명령어 이름인 startserver로 변경한 후 npm server라고 실행하면 태스크가 수행되지 않습니다.

start, test 명령어 이름은 기본값으로 등록이 되어 있기 때문에 npm start로 수행이 가능하지만 "server": "webpack-dev-server --open" 라고 사용자 정의한다면 태스크 실행시에 run을 함께 아래와 같이 작성해 주어야 합니다.

"scripts": {
	"server": "webpack-dev-server --open",
},
$ npm run server

위와 같이 명령어를 사용자가 등록할 수 있기 때문에 지금까지 사용했던 webpack.config.js를 수행했던 webpack 명령어도 사용자 등록이 가능합니다.  예를 들어 webpack 구성파일을 개발 모드와 배포 모드로 구분해야 한다면 webpack.dev.js, webpack.prod.js 로 구성 파일을 나누어 사용할 수 있습니다.

개발계와 배포계를 나누었다고 가정할 경우 다음과 같이 scripts 를 등록하여 사용할 수 있습니다.

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

위와 같이 등록이 되어 있다면 개발시에는 npm run dev를 배포시에는 npm run build를 수행하실 수 있습니다.

다시 말해, npm run {지정한 이름} 과 같은 간단한 명령으로 대체할 수 있습니다.

 

 

 

개발 편의성을 위한 Source map

개발을 진행하다보면 점차 js 파일이 많아지게 되고 webpack 은 여러 파일들을 하나 또는 특정 갯수의 번들된 파일로 묶다보니 errorwarning 메시지를 통해 어느 파일의 어느 코드에서 문제가 발생했는지 정확히 추적하기가 어려워지게 됩니다.
소스 맵(Source Map)이란 배포용으로 빌드한 파일과 원본 파일을 서로 연결시켜주는 기능입니다.
보통 서버에 배포를 할 때 성능 최적화를 위해 HTML, CSS, JS와 같은 웹 자원들을 압축합니다. 그런데 만약 압축하여 배포한 파일에서 에러가 난다면 어떻게 디버깅을 할 수 있을까요? 이렇게 추적, 디버깅이 어려운 경우 Source map 을 이용해 배포용 파일의 특정 부분이 원본 소스의 어떤 부분인지 확인하는 것입니다.
즉, 원본 파일을 통해 코드를 보여주고 error 및 warning 메세지와 함께 정확한 파일명과 코드 위치를 알려주기 때문에 유용할 수 있습니다.

 

웹팩에서 소스 맵을 설정하는 방법은 아래와 같습니다.

// webpack.config.js
module.exports = {
  devtool: 'cheap-eval-source-map'
}

devtool 속성을 추가하고 소스 맵 설정 옵션 중 하나를 선택해 지정해주면 됩니다.

현재 진행하고 있는 예제에서는 webpack 공식 문서에서 권장하고 있는 cheap-eval-source-map을 추가하여 사용해 봅니다.

webpack.config.js에서 devtool 속성을 추가하도록 합니다.

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

module.exports = {
    entry: {
        'js/index': ['./src/js/main.js'],
        'js/module': ['./src/js/module01.js', './src/js/module02.js'],
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].js',
    },
    module: {
        rules: [
            {
                // 대상 파일 지정
                test: /\.s(a?c)ss$/,
                use: [
                    {
                        loader: MiniCssExtractPlugin.loader,
                        options: {
                            publicPath: '../'
                        }
                    },
                    'css-loader',
                    {
                        loader: 'sass-loader',
                        options: {
                            outputStyle: 'expanded',
                            indentType: 'tab',
                            indentWidth: 1
                        }
                    }
                ],
                exclude: /node_modules/
            }
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: 'index.html',
            filename: 'index.html'
        }),
        new webpack.ProvidePlugin({
            $: 'jquery',
            jQ : 'jquery',
            moment : 'moment'
        }),
        new MiniCssExtractPlugin({
            filename: 'css/style.css'
        })
    ],
    optimization: {
        splitChunks: {
            cacheGroups: {
                vendors: {
                    test: /[\\/]node_modules[\\/]/,
                    chunks: 'all',
                    name: 'js/vendor/libs',
                }
            }
        }
    },
    devServer: {
        port: 9000,
        open: true,
    },

    // source map 설정
    devtool: 'cheap-eval-source-map',
    mode: 'development'
};

소스맵을 추가하셨다면 webpack-dev-server를 실행해 보세요.
그리고 js 파일 중에 고의로 문법을 틀리게 작성하면 개발자 도구의 console 패널에서 에러가 발생한 파일명과 라인을 표시해 줄 것입니다.

 

소스맵 옵션에는 다양한 값들이 존재하는데 개발과 운영 모드에서 사용할 수 있는 옵션이 서로 다릅니다.

webpack 공식 문서에서는 개발 모드에서는 eval-source-map 또는 cheap-eval-source-map를, 운영 모드에서는 none(사용하지 않음) 이거나 hidden-source-map 정도를 권장하고 있습니다.

이 소스맵 옵션에 사용자 개발 환경이나 기호에 맞게 선택적으로 사용할 수 있습니다.

 

devtool options

  • source-map

    모든 기능이 포함된 완전한 소스맵을 별도의 파일로 생성
    이 옵션은 최고 품질의 소스맵을 생성하지만 빌드 프로세스가 느려지는 단점이 있음

  • cheap-module-source-map

    별도의 파일에 컬럼 매핑을 제외한 소스 맵을 생성
    컬럼 매핑을 생략하면 빌드 속도는 향상되지만 디버깅시 조금 불편할 수 있음
    브라우저 개발자 툴은 원래 소스 파일의 행만 가리킬 수 있으며, 특정 컬럼(또는 문자)을 가리킬 수 없다

  • eval-source-map

    eval을 사용해 동일한 파일 안에 전체 소스맵과 소스코드 모듈을 중첩해 번들로 생성
    이 옵션을 사용하면 빌드 시간에 대한 부담 없이 모든 기능이 포함된 소스맵을 생성할 수 있지만
    자바스크립트를 실행할 때 성능과 보안이 저하되는 단점이 있다
    즉, 개발 중에는 유용하지만 배포시에 빌드할 때는 사용하지 말아야 한다

  • cheap-module-eval-source-map

    빌드 중에 소스 맵을 생성하는 가장 빠른 방법
    생성되는 소스맵에는 번들 자바스크립트 파일이 칼럼 매핑을 제외하고 동일하게 인라인으로 포함된다
    이 옵션도 자바스크립트 성능을 저하시키기 때문에 production 에서는 적합하지 않다

 

 

 

Conclusion

지금까지 실무에서 사용해볼 만한 몇 가지 웹팩 환경설정(라이브러리 로드, sass 컴파일, webpack-dev-server, source map)등에 대해 알아보았습니다.  아마도 입문자의 경우에 녹록치 않았을 것으로 생각되지만 웹팩을 사용하기 위한 최소한의 도움과 변화하는 개발환경 트렌드에 적응하는데 필요한 시간이 되었으면 합니다.

 

 

Jaehee's WebClub

 

 

'Web Tech > Webpack' 카테고리의 다른 글

Webpack Guide for beginner #2  (5) 2019.11.19
Webpack Guide for beginner #1  (4) 2019.11.19