본문 바로가기

개발/개발 관련 Contents

Webpack에 대해 알아보자.

반응형

사실 아직은 개발을 하면서 Webpack이 무엇인지를 알지 제대로 사용해본 적이 없었습니다.

지금부터 Webpack에 대해서 제대로 알아봅시다!

Webpack을 알아보기 전에 Grunt와 Gulp를 알아보면서 공부를 하면 좋은데 webpack이 나오게 된 이유를 Grunt와 Gulp에 대한 이해에서 찾아볼 수 있습니다.

Grunt와 Gulp는 사람의 실수나 반복적인 작업을 줄이기 위한 자동화 툴입니다.

보통 CSS와 JavaScript 파일을 concat, minify, compress, uglify를 하는데 많이 사용합니다. 즉 사전에 필요한 반복적인 작업들을 간단한 작업만으로도 진행할 수 있습니다.

또한, 이들은 Nodo.js에서 사용할 수 있으며 여러 plugin을 활용할 수 있습니다.

Grunt vs Gulp

위처럼 둘은 같은 작업을 수행하지만, 수행하는 방법에서 차이가 있습니다.

// Grunt는 JSON 형태의 confing를 구성하는 방식으로 구현됩낟.
imagemin:{
    jpgs:{
        options:{
            progressive: true
        },
        files:[{
            expand:true,
            cwd: 'src/img',
            src: ['*.jpg'],
            dest: 'images/'
        }]
    }
}

설정 파일 기반의 Grunt와 달리 Gulp는 JavaScript 코드를 다룰 줄 아는 사람이면 쉽게 다룰 수 있습니다.

// Gulp는 Javascript를 사용합니다.

gulp.task('jpgs', function(){
    return gulp.src('src/images/".jpg")
        .pipe(imagemin({ progressive: true}))
        .pipe(gulp.dest('optimized_images'));
});

속도

Gulp와 Grunt의 속도의 차이를 이해하기 위해서는 처리방식의 차이점을 알아야 합니다.

Gulp는 스트림을 기반으로 하는 빌드 시스템입니다. 스트림을 이용해서 데어터를 읽고 출력하며 작업들을 메모리에서 처리합니다. 즉 요청 후 한 번에 결과를 받는 것이 아니라 이벤트 중간중간 전달받아 작업을 하기 때문에 비교적 작업 속도가 빠릅니다.

 

스트림이란?
데이터 입, 출력시 비동기적으로 처리될 수 있는 데이터의 연속적 흐름으로써, Node.js에서는 이 스트림을 읽고 쓸 수 있습니다.

 

또는 Gulp는 동시에 여러 작업을 처리할 수 있지만 Grunt는 일반적으로 한 번에 하나의 작업만 처리합니다.

Grunt는 2011년 후반부터 개발이 시작됐는데 현재 5000여개의 플러그인이 존재할 정도로 JavaScript관련 테스크는 대부분 Grunt플러그인으로 만들어졌었습니다. JSON을 통해서 선언적인 설정을 통해 테스크를 정의하고 이를 간단한 명령어를 통해 실행합니다. 본격적으로 인기를 얻기 시작한 것은 0.4 버전이 릴리즈된 2013.2 이후입니다.

Gulp는 2013여름부터 개발이 시작되고 Grunt를 대체해갔습니다. 하는 일은 거의 갔지만 Node.js를 통해 테스크를 선언하는 메리트로 Grunt에서 Gulp로 갈아타는 개발자들이 많았습니다. 현재 2000여 개의 플러그인이 존재합니다.

하지만 많은 개발자들이 Gulp로 갈아타게 된 이유는 Grunt는 첫 번째 릴리즈가 너무 느립니다.

0.4.5버전이(2014.05) 릴리즈가 된 후 약 2년 이후에야 1.0.0 버전(2016.02)이 릴리즈 되었습니다. 그리고 1.3.0 버전이 가장 최근 릴리즈인데 2018.08 으로써 Grunt의 관리가 소홀해졌고 새로 Grunt의 개발을 도와줄 사람을 공식적으로 찾기 시작했습니다. 이는 Grunt의 개발팀이 없어졌거나 유지 보수할 능력이 없다는 것을 공식 인정하는 것이나 다름이 없습니다. 지금 당장 문제는 없을 수도 있지만 버그는 발생할 것이고 언제 없어질지 모르는 이 도구를 계속해서 사용해야 될지는 의문이 듭니다.

둘째 설정보다는 코드가 직관적이라는 부분에서는 동의하지 못했지만, 코드로 테스크를 작성할 수 있고 기본적으로 스트림을 이용해서 플러그인을 만들도록 하므로 플러그인을 연결할 수 있습니다. Grunt였다면 concat플러그인, uglify 플러그인을 설치해서 각각 설치한 뒤에 alias로 연결해야 했다면 gulp에서는 src().pipe(concat()).pipe(uglify()).dest() 식으로 이어 붙일 수 있습니다.

셋째 Gulp는 Node 스럽습니다. Node.js에서 항상 얘기하는 것이 "Write programs that do one thing and do it well"이라는 Unix철학인데 Grunt는 all-in-one 느낌이라면 Gulp는 Node처럼 작은 모듈들이 많이 있고 이를 조합해서 테스크를 만듭니다.

넷째 복잡해지면 더 읽기 어려워지는 특징이 있습니다. 간단한 코드는 어느 쪽이든 상관없지만 복잡해지면 설정을 찍기가 상당히 어려워지는 느낌이 있습니다.

예전에 fastbook 프로젝트를 하다 팀원이었던 승현이 형에게 이미지 최적화에 대해서 물어보았을 때 gulp를 사용하라고 했었던 기억이 난다. 그때 당시에는 sass를 css로 자동으로 컴파일해주는 build task용으로 사용했었다. 마찬가지로 nunjucks이란 템플릿 엔진을 자동화를 사용하기 위해 사용했다.

Webpack

Gulp와 Grunt와 같은 자동화 도구 덕분에 많은 작업의 양이 줄었는데도 불구하고 Webpack이 나왔습니다. 우선 Webpack은 모듈 번들러이고 Gulp와 Grunt는 task runners입니다.

결론부터 말하자면 Webpack이 이들을 대체된다기보다 이 기능이 포함되어 있고 더 많은 작업을 할 수 있기 때문에 이 도구를 사용하는 것입니다. Webpack은 Browserify와 같은 의존성 관리 기능까지 포함하고 있으니 "webpack = (Grunt||Gulp) + Browserify" 이게 되는 것입니다. 게다가 속도도 빠른 편입니다.

우선 Browserify?

Browserify는 Node.js기반 javascript code를 브라우저 환경에서도 실행 가능하도록 해줍니다.

var path = require('path');
var foo = require('./helpers/foo')

module.exports = function bar(){
    return 'bar'
};

즉, 브라우저와 Node.js의 코드를 동일하게 사용할 수 있습니다.

하지만 브라우저에서 작업 결과를 봐야 하기 때문에 매번 코드를 컴파일해야 하는데, 코드의 양이 많아지면 하나 수정하는데도 엄청 오래 걸릴 수 있다는 애기가 됩니다.

Webpack에 들어가기 전 Babel과 Polyfil 개념도 간단하게 정리해보면

Babel은 JavaScript 컴파일러로써 ES6 이후의 코드를 구형 브라우저 환경에 맞게 변환해주는 데 주로 사용을 합니다.

polyfill은 개발자가 특정 기능이 지원되지 않는 브라우저를 위해 사용할 수 있는 코드 조각이나 플러그인을 말한다. 폴리필은 HTML5 및 CSS3와 오래된 브라우저 사이의 간격을 메꾸는 역할을 담당한다.

Polyfill을 써야 하는 상황과 babel을 사용해야 하는 에러 상황 비교

  • ReferenceError() undefined : JavaScript가 알아듣긴 하나 없다 ⇒ polyfill
  • syntax error: JavaScript가 구문 자체를 못 알아들음 ⇒ babel

Babel 공식 document에 이런 문장이 있는데

@babel/polyfill
Babel includes a polyfill that includes a custom regenerator runtime 
and core-js.

This will emulate a full ES2015+ environment (no < Stage 4 proposals) 
and is intended to be used in an application rather than a library/tool.
(this polyfill is automatically loaded when using babel-node).

Babel은 regenerator runtime과 core-js를 포함하는 polyfill을 포함한다.라는 말이 있어 Babel이 polyfill보다 큰 개념으로 파악하고 넘어가겠습니다.

JavaScript 코드가 많아지면 하나의 파일로 관리하는데 한계가 있습니다. 그렇다고 해서 여러 개 파일을 브라우저에서 로딩하는 것은 네트워크 비용이 크고 서로의 스코프를 침범해 변수 충돌 위험성도 존재합니다. 함수 스코프를 사용하는 JavaScript는 IIFE를 사용해 모듈을 만들 수 있는데 CommonJS나 AMD 스타일의 모듈 시스템을 사용해 파일별로 모듈을 관리할 수도 있습니다.

그러나 여전히 브라우저에서는 파일 단위 모듈 시스템을 사용하는 것은 쉽지 않습니다. 모듈을 IIFE 스타일로 변경해 주는 과정뿐만 아니라 하나의 파일을 묶어 네트워크 비용을 최소화할 수 있는 방법이 웹 프론트 과정에서 필요합니다.

웹팩은 이러한 과정에서 이해할 수 있습니다. 기본적으로 모듈 번들러로 소개하고 있는 웹팩의 주요 네 가지 개념을 정리해 보겠습니다.

엔트리(entry)

웹팩에서 모든 것은 모듈입니다. JavaScript, 스타일 시트, 이미지 등 모든 것을 JavaScript 모듈로 로딩해서 사용하도록 합니다.

JavaSciprt가 로딩하는 모듈이 많아질수록 모듈 간의 의존성은 증가합니다. 의존성 그래프의 시작점을 웹팩에서는 엔트리(entry)라고 합니다.

웹팩은 엔트리를 통해서 필요한 모듈을 로딩하고 하나의 파일로 묶습니다.

// webpack.config.js : 
module.exports = {
    entry: {
        main: './src/main.js',
    }
}

우리가 사용할 html에서 사용할 JavaScript 시작점은 src/main.js 코드입니다. entry 키에 시작점 경로를 지정했습니다.

아웃풋

엔트리에 설정한 JavaScript파일을 시작으로 의존되어 있는 모든 모듈을 하나로 묶을 것이다. 번들된 결과물을 처리할 위치는 output에 기록합니다.

// webpack.config.js: 
module.exports = {
    output: {
        filename: 'bundle.js',
        path: './dist'
    }
}

dist 폴더의 bundle.js 파일로 결과를 저장할 것입니다.

html 파일에서는 번들링 된 이 파일을 로딩하게끔 합니다.

// index.html:
<body>
    <script src="./dist/bundle.js"></scirpt>
</body>

엔트리에 설정한 JavaScript는 Utils.js 모듈을 사용합니다.

// src/main.js
import Utils from './Utils'
Utils.log('Hello webpack')

Utils.js 코드는 다음과 같습니다.

// src/Utils.js:
export default class Utils{
    static log(msg) {console.log('[LOG] ' + msg)}
}

웹팩은 터미널에서 webpack 커맨드로 빌드할 수 있습니다.

여기까지가 간단히 웹팩으로 번들링 하는 과정입니다.

로더

웹팩은 모든 파일을 모듈로 관리한다고 했습니다. JavaScript 파일뿐만 아니라 이미지, 폰트, 스타일 시트도 전부 모듈로 관리합니다. 그러나 웹팩은 JavaScript밖에 밖에 모릅니다. 비 JavaScript 파일을 웹팩이 이해하게끔 변경해야 하는데 로더가 그런 역할을 합니다.

로더는 test와 use키로 구성된 객체로 설정할 수 있습니다.

  • test에 로딩할 파일을 지정하고
  • use에 적용할 로더를 설정한다.

babel-loader

가장 간간한 예가 Babel이다. ES6에서 ES5로 변환할 때 Babel을 사용할 수 있는데 test에 ES6로 작성한 JavaScript파일을 지정하고, use에 이를 변화할 Babel 로더를 설정합니다.

// webpack.config.js : 
module.exports = {
    module: {
        rules: [{
            test: /\.js$/,
            exclude: 'node_modules',
            use: {
                loader: 'babel-loader',
                options: {
                    presets: ['env']
                }
            }
        }]
    }
}

test에 JavaScript 확장자를 갖는 파일을 정규표현식으로 지정했습니다. node_modules 폴더는 패키지 폴더이므로 제외하기 위해서 exclude에 설정한다. use에 로더를 설정하는데 babel-loader를 추가했습니다.

위 과정을 거치면 bundle.js가 ES5문법으로 변경이 됩니다.

플러그인

웹팩에서 알아야 할 마지막 개념이 플러그인입니다. 로더가 파일 단위로 처리하는 반면 플러그인은 번들된 결과물을 처리합니다. 번들된 JavaScript를 난독화 한다거나 특정 텍스트를 추출하는 용도로 사용할 수 있습니다.

UglifyJsPlugin

UglifyJsPlugin은 로더로 처리된 JavaScript 결과물을 난독화 처리하는 플러그인입니다.

플러그인을 사용할 때는 웹팩 설정 객체의 plugins배열에 추가합니다.

const webpack = require('webpack')

module.exports = {
    plugins: [
        new webpack.optimize.UglifyJsPlugin(),
    ]
}

아래 번들링 된 결과를 보면 난독화 처리가 되었습니다.

 

정리

웹팩은 기본적으로 모듈 번들러입니다. 의존성 그래프에서 엔트리로 그래프의 시작점을 설정하면 웹팩은 모든 자원을 모듈로 로딩한 후 아웃풋으로 묶어줍니다. 로더로 각 모듈별로 바벨, 사스변환 드으이 처리하고 이 결과를 플러그인이 받아 난독화, 텍스트 추출 등의 추가 작업을 합니다.

출처

Grunt, Gulp, Webpack - https://fullest-sway.me/blog/2017/03/29/tool-each/

왜 Grunt에서 Gulp로 갈아탔는가? - https://blog.outsider.ne.kr/1181

Babel 공식 - https://babeljs.io/docs/en/

Polyfill - https://webdir.tistory.com/328

웹팩의 기본 개념 - http://blog.jeonghwan.net/js/2017/05/15/webpack.html

반응형