본문으로 바로가기

Webpack Guide for beginner #3

category Web Tech/Webpack 2019.08.07 15:40

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

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

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

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


 

 

프로젝트 초기 생성

앞선 포스팅을 따라 오셨다면 nodeJSwebpack global 이 설치되어 있을 것입니다. 만약 웹팩에 필요한 기본 설정이 되어 있지 않다면 필자의 이전 포스팅을 참고하시고, nodeJSwebpack global 이 설치되어 있음을 가정으로 진행합니다.

 

다음과 같이 디렉토리를 만들어, npm 을 초기화하고, 로컬에 webpackwebpack-cli(커맨드라인에서 웹팩을 실행하는 도구)를 설치하여 초기 프로젝트 단계을 진행합니다.

# 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/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 는 단순히 로그만 작성해 봅니다.

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 번들을 로드하는데 유용하게 사용됩니다.

 

해당 플러그인 사용을 위해 플러그인을 설치합니다.

$ 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 라이브러리를 사용.

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

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

 

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

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 명을 작성하면 됩니다.
그리고 하위 폴더를 구성하고자 하는 경우에는 entry 의 key 를 / 를 구분자로 사용할 수 있습니다.
예를 들어, 현재 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 구문 제거

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

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

 

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

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

 

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

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 폴더로 복사해서 사용하시기 바랍니다.

sass.zip


현재까지 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 구문을 이용하는 편이 나을 수 있습니다.

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', './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$/,
                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 or .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 을 실행하면 다음과 같이 컴파일될 것입니다.

.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 를 설치하여 사용하면 더 편리할 수 있습니다.

webpack-dev-server 는 webpack 자체에서 제공하는 개발 서버로서 로컬에서 사용할 개발용 서버를 제공하고 이 기능을 사용하면 소스 파일을 감시(지속적 관찰)하고 내용이 변경될 마다 번들을 다시 컴파일 합니다. 그리고 또한 소스가 변경될 때마다 수시로 새로고침을 하지 않아도 새로고침되는 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

 

 

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

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

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

여기서는 위에서 수행했던 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 은 여러 파일들을 하나 또는 특정 갯수의 번들된 파일로 묶다보니 error 와 warning 메시지를 통해 어느 파일의 어느 코드에서 문제가 발생했는지 정확히 추적하기가 어려워지게 됩니다. 이렇게 추적이 어려운 경우 Source map 을 사용하면 개발중인 원본 파일을 통해 코드를 보여주고 error 및 warning 메세지와 함께 정확한 파일명과 코드 위치를 알려주기 때문에 유용할 수 있습니다.

여기서는 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 #3  (0) 2019.08.07
Webpack Guide for beginner #2  (2) 2019.08.05
Webpack Guide for beginner #1  (0) 2019.08.05

댓글을 달아 주세요