본문 바로가기

Study/Web

2. 프론트엔드 개발환경의 이해: 웹팩(기본II - 플러그인)

이곳의 3강 내용을 정리한다.

https://tacademy.skplanet.com/live/player/onlineLectureDetail.action?seq=174#sec1

 

프론트엔드 개발환경 이해 | T아카데미 온라인강의

1. NPM 프로젝트와 모듈 시스템을 이해하고, Webpack/Babel/Lint 등 개발 도구들의 역할에 대해 알아봅니다. 2. 각 개발도구들을 이용하여 React 개발환경을 ..

tacademy.skplanet.com

 

플러그인


개념

엔트리를 모아서 아웃풋으로 만든다. 모을때 import해서 모듈로 처리한다. 누가? 로더가.

아웃풋으로 만들기 전에 후처리를 해준다. 빈칸을 줄인다거나, 자바스크립트를 난독화한다거나 어쩌구저쩌구.

이런 후처리는 플러그인이 수행한다. 

로더는 파일단위 처리, 플러그인은 번들된 결과물 처리!

 

플러그인 만들기

웹팩 문서의  Writing a plugin을 참고해서 플러그인 예제를 만들어보자.

 

myplugin.js:

1
2
3
4
5
6
7
8
9
class MyPlugin {
  apply(compiler) {
    compiler.hooks.done.tap('My Plugin', stats => {
      console.log('MyPlugin: done');
    })
  }
}
 
module.exports = MyPlugin;

플러그인은 클래스 단위로 제작한다. 

 

webpack.config.js:

1
2
3
4
5
6
7
const MyPlugin = require('./myplugin');
 
module.exports = {
  plugins: [
    new MyPlugin(),    //클래스니까 생성자 쓴다
  ]
}

 

로그가 한번만 찍힌다

로그가 한번만 찍히는 이유는? 플러그인은 번들결과물에 적용되기 때문이다.

 

 

배너

코드 상단에 주석으로 누가 만들었고, 이건 무슨 코드고... 이런 설명을 배너라고 한다.

이 배너를 만드는 플러그인을 작성해보자.

 

myplugin.js: //번들 소스를 얻어오는 source() 함수를 재정의했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class MyPlugin {
    apply(compiler) {
        compiler.hooks.done.tap('My Plugin', stats => {
            //컴파일러 종료될때 실행
            console.log('MyPlugin: done');
        });
        compiler.plugin('emit', (compilation, callback) => { // compiler.plugin() 함수로 후처리한다
            const source = compilation.assets['main.js'].source();
            compilation.assets['main.js'].source = () => { //source에 접근한다
                const banner = [
                    '/**',
                    ' * 이것은 BannerPlugin이 처리한 결과입니다.',
                    ' * Build Date: 2019-10-10',
                    ' */'
                ].join('\n');
                return banner + '\n\n' + source;
            }
            callback();
        });
    }
}

/dist/main.js에서 맨 위에 주석이 달린것을 확인할수있다.

 

 

Banner Plugin

웹팩에서 제공하는 배너플러그인을 사용해보자.

1
2
3
4
5
6
7
8
const webpack = require('webpack');
 
module.exports = {
  plugins: [
    new webpack.BannerPlugin({
      banner: '이것은 배너 입니다',
    })
  ]

 

 

배너 정보가 많으면 따로 js를 뺄수도 있다.

 

webpack.config.js:

1
2
3
const banner = require('./banner.js');
 
new webpack.BannerPlugin(banner);

 

banner.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
const childProcess = require('child_process');
 
module.exports = function banner() {
  const commit = childProcess.execSync('git rev-parse --short HEAD');
  const user = childProcess.execSync('git config user.name');
  const date = new Date().toLocaleString();
  
  return (  //템플릿 리터럴 이용 (ES6)
    `commitVersion: ${commit}` +
    `Build Date: ${date}\n` +
    `Author: ${user}`
  );
}

 

DefinePlugin

빌드타임에 정해져야하는 것들은 플러그인으로 주입해주면 좋다.

 

webpack.config.js:

1
2
3
4
5
6
7
const webpack = require('webpack');
 
export default {
  plugins: [
    new webpack.DefinePlugin({}),
  ]
}

 

DefinePlugin({})에 빈 객체를 넣어줘도 기본변수가 들어가게 된다.

기본으로 들어가는 변수는 process.env.NODE_ENV다. 이 변수는 웹팩 mode에 설정한 값을 뜻한다.

development, production 이거.

 

 

컴파일때 정해져야하는 상수같은것도 어플리케이션에 주입할 수 있다.

1
2
3
new webpack.DefinePlugin({
  TWO: '1+1',
})

 

app.js : 

1
console.log(TWO); // 2

 

만약 코드가 아닌 값을 입력하려면 문자열로 바꿔서 넣으면 된다.

1
2
3
4
5
6
new webpack.DefinePlugin({
  VERSION: JSON.stringify('v.1.2.3'),
  PRODUCTION: JSON.stringify(false),
  MAX_COUNT: JSON.stringify(999),
  'api.domain': JSON.stringify('http://dev.api.domain.com'),
})

 

app.js:

1
2
3
4
console.log(VERSION) // 'v.1.2.3'
console.log(PRODUCTION) // true
console.log(MAX_COUNT) // 999
console.log(api.domain) // 'http://dev.api.domain.com'

 

 

HtmlWebpackPlugin

HtmlWebpackPlugin은 써드파티다. HtmlWebpackPlugin은 HTML 파일을 후처리하는데 사용한다. 빌드 타임의 값을 넣거나 코드를 압축할수 있다.

 

1
$ npm install -D html-webpack-plugin

 

index.html을 src폴더 안으로 옮기자. 이유는? 얘도 웹팩의 대상으로 만들기 위해서다.

<%= env %>는 ejs 템플릿 엔진 문법이다. HtmlWebpackPlugin 빌드타임에 결정되는 정보를 변수로 넣어준다.

로딩 스크립트도 지웠다. 폴더이름이 dist일수도있고 output일수도있고 그런데 웹팩이 알아서 넣어주라고 지웠다. HtmlWebpackPlugin이 웹팩으로 빌드된 결과물을 알아서 넣어준다.

 

 

src/index.html:

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html>
  <head>
    <title>타이틀<%= env %></title>
  </head>
  <body>
    <!-- 로딩 스크립트 제거 -->
    <!-- <script src="dist/main.js"></script> -->
  </body>
</html>

 

webpack.config.js:

1
2
3
4
5
6
7
8
9
10
11
12
const HtmlWebpackPlugin = require('html-webpack-plugin');
 
module.exports {
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html'// 템플릿 경로를 지정 
      templateParameters: { // 템플릿에 주입할 파라매터 변수 지정
        env: process.env.NODE_ENV === 'development' ? '(개발용)' : ''
      },
    })
  ]
}

 

그런데 윈도우는 NODE_ENV제대로 적용되지 않는다. 'NODE_ENV'은(는) 내부 또는 외부 명령, 실행할 수 있는 프로그램, 또는 배치 파일이 아닙니다.' 에러가 뜬다. 해결방법은 여기여기를 참고한다.

 

첫번째 해결링크대로 하면 터미널에서는 ` NODE_ENV=development npm run build `가 잘 돌아가지만, vscode의 터미널에서는 또 'NODE_ENV=development' 용어가 cmdlet, 함수, 스크립트 파일 또는 실행할 수 있는 프로그램 이름으로 인식되지 않습니다. 이름이 정확한지 확인하고 경로가 포함된 경우 경로가 올바른지 검증한 다음 다시 시도
하십시오.'라면서 안된다. 

 

그래서 두번째 해결링크대로 했더니 vscode에서도 잘 돌아갔다. (윈도우인데도 $가 붙어있는 이유는 그냥 내가 터미널인거 알아보기 쉬우라고 넣어서 그렇다)

 

vscode의 terminal:

1
$ npm install --save-dev cross-env

 

이후 다시 build하면 된다.

1
$ cross-env NODE_ENV=development npm run build

 

build하면 index.html이 dist에 생성된것을 볼 수 있다. 그리고 빌드타임에 설정해준 내용도 들어가있다.

 

타이틀<% = env%> → 타이틀(개발용)

 

코드를 압축하는 minify옵션을 설정해보자. production으로 실행할때는 코드를 압축하도록 바꿨다.

1
2
3
4
5
6
new HtmlWebpackPlugin({
   minify: process.env.NODE_ENV === 'production' ? { 
        collapseWhitespace: true// 빈칸 제거 
        removeComments: true// 주석 제거 
      } : false}
)

 

NODE_ENV=development와 production으로 다르게 빌드해보면 dist/index.html이 차이나는것을 볼 수 있다.

NODE_ENV=development
NODE_ENV=production

 

 

CleanWebpackPlugin

이전 빌드 결과물을 청소해주는 플러그인.

1
$ npm install -D clean-webpack-plugin

webpack.config.js:

1
2
3
4
5
6
7
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
 
module.exports = {
  plugins: [
    new CleanWebpackPlugin(),
  ]
}

 

MiniCssExtractPlugin

CSS파일만 따로 뽑아서 로딩할때 쓰는 플러그인

js에 CSS를 그냥 냅다 추가하면 너무 크기가 커진다. 그래서 CSS만 따로 빼서 로딩할때 쓴다.

 

1
$ npm install -D mini-css-extract-plugin

 

webpack.config.js:

1
2
3
4
5
6
7
8
9
10
11
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
 
module.exports = {
  plugins: [
    ...(
      process.env.NODE_ENV === 'production' 
      ? [ new MiniCssExtractPlugin({filename: `[name].css`}) ]
      : []
    ),
  ],
}

 

그리고 production에서는 cssExtractPlugin도 쓰므로 새로운 로더가 필요하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
module.exports = {
  module: {
    rules: [{
      test: /\.css$/,
      use: [
        process.env.NODE_ENV === 'production' 
        ? MiniCssExtractPlugin.loader  // 프로덕션 환경
        : 'style-loader',  // 개발 환경
        'css-loader'
      ],
    }]
  }
}

 

 

FIXME: 

그런데 이렇게 만든 /dist/index.html을 보면 배경이미지가 제대로 로드되지 않는다. GET FILE 어쩌구저쩌구 :net::ERR_FILE_NOT_FOUND에러.

url-loader의 publicPath를 고치면 된다. html파일도 src로 옮기느라, 이미지파일과 html의 폴더가 같아져서 그렇다.

 

webpack.config.js:

1
2
3
4
5
6
7
8
9
10
11
module.exports = {
    module: {
            test: /\.(png|jpg|svg|gif)$/,
            loader: 'url-loader',
            options: {
                // publicPath: './dist/', // prefix를 아웃풋 경로로 지정:: 지운다
                name'[name].[ext]?[hash]'// 파일명 형식 
                limit: 5000 // 5kb 미만 파일만 data url로 처리 
            }
        }]
    },

 

 

참고