Webpack

Webpack - 플러그인(Plugin)

shin96bc 2024. 3. 10. 21:30

❓플러그인의 역할은?

로더가 파일 단위로 처리하는 반면 플러그인은 번들된 결과물을 처리합니다.

 

번들된 Javascript를 난독화 한다거나 특정 텍스트를 추출하는 용도로 사용합니다.

 

 

⚒️ 커스텀 플러그인 만들기

webpack 문서의 Writing a plugin을 보면 Class로 플러그인을 정의 하도록 합니다.

 

문서의 예제 코드를 보고 나만의 플러그인을 만들어봅시다.

// my-webpack-plugin.js

class MyWebpackPlugin {
	apply(compiler) {
		compiler.hooks.done.tap('My Plugin', stats => {
			console.log('MyPlugin: done');
		});
	};
};

module.exports = MyWebpackPlugin;
  • 로더와 다르게 플러그인은 클래스로 제작합니다. 'apply' 함수를 구현하면 되는데 이 코드에서는 인자로 받은 'compiler'의 'tap()' 함수를 사용했습니다. 플러그인 작업이 완료되는(done) 시점에 로그를 찍는 코드입니다.

 

플러그인을 webpack 설정에 추가합니다.

const path = require('path');
const MyWebpackPlugin = require('./my-webpack-plugin');

module.exports = {
    mode: 'development',
    entry: {
        main: './src/app.js'
    },
    output: {
        path: path.resolve('./dist'),
        filename: '[name].js'
    },
    module: {
        ...
    },
		// 플러그인은 webpack 설정 객체의 plugins 배열에 추가합니다.
		// Class로 제공되는 플러그인의 생성자 함수를 실행해서 넘기는 방식입니다.
    plugins: [
        new MyWebpackPlugin(),
    ]
};

 

이제 webpack을 빌드해보면 아래와 같이 로그가 찍히는걸 볼 수 있습니다.

 

그런데 파일이 여러개인데 로그는 한번만 찍혔네요?? 여기서 우리는 'module'이 파일 하나 혹은 여러개에 대해서 동작하는 반면 'plugin'은 하나로 번들링된 결과물을 대상으로 동작한다는 사실을 알 수 있습니다.

 

위의 예제에서는 'math.js'로 결과물이 하나이기 때문에 플러그인이 한번만 동작한 것이라 추측할 수 있겠습니다.

 

그렇다면 어떻게 번들 결과에 접근할 수 있을까요? webpack 내장 플러그인 BannerPlugin 코드를 참고해서 알아봅시다.

 

'my-webpack-plugin.js' 예시 보기

class MyWebpackPlugin {
	apply(compiler) {
		compiler.hooks.done.tap('My Plugin', stats => {
			console.log('MyPlugin: done');
		});

		// compiler.plugin() 함수로 후처리를 합니다.
		// (webpack v4 사용법)
		compiler.plugin('emit', (compilation, callback) => {
			const source = compilation.assets['main.js'].source();
			console.log(source);
			callback();
		});

		// (webpack v5 사용법)
		compiler.hooks.emit.tapAsync('My Plugin', (compilation, callback) => {
			const source = compilation.assets['main.js'].source();
			console.log(source);
			callback();
		});
	};
}
  • 'compiler.plugin()' 함수의 두번째 인자 callback 함수는 emit 이벤트가 발생하면 실행되는 함수입니다. 번들된 결과가 'compilation' 객체에 들어있는데, 'compilation.assets[”main.js”].source()' 함수로 접근할 수 있습니다.

 

위의 예시에 있는 코드를 실행하면 터미널에 번들링된 결과를 확인할 수 있습니다.

 

이것을 이용해서 번들 결과 상단에 아래와 같은 배너를 추가하는 플러그인을 만들어 봅시다.

class MyWebpackPlugin {
	apply(compiler) {

		// (webpack v4 사용법)
		compiler.plugin('emit', (compilation, callback) => {
      const source = compilation.assets['main.js'].source();
			
			// 빌드된 결과물에 주석으로 빌드된 날짜를 추가
      compilation.assets['main.js'].source = () => {
        const banner = [
          '/**',
          ' * 이것은 BannerPlugin이 처리한 결과입니다.',
          ' * Build Date: 2023-06-17',
          ' */'
          ''
        ].join('\n');
        return banner + '\n\n' + source;
      };

      callback();
    });

		// (webpack v5 사용법)
    compiler.hooks.emit.tapAsync('My Plugin', (compilation, callback) => {
      const source = compilation.assets['main.js'].source();

      // 빌드된 결과물에 주석으로 빌드된 날짜를 추가
      compilation.assets['main.js'].source = () => {
          const banner = [
              '/**',
              ' * 이것은 BannerPlugin이 처리한 결과입니다.',
              ' * Build Date: 2023-06-17',
              ' */'
          ].join('\n');
          return banner + '\n\n' + source;
      };

      callback();
    });
	};
};

module.exports = MyWebpackPlugin;

 

 

이렇게 하면 아래와 같이 주석이 추가된 것을 볼 수 있습니다.

 

하지만 한가지 문제가 발생했습니다. webpack v5의 경우에는 버전업 되면서 사용하는 방법이 변경된건지 'source()'함수에 새로운 함수를 할당하고 'console.log()'를 사용해서 출력하면 주석이 추가되어진 것을 확인할 수 있었는데 실제 번들링된 'main.js'에는 추가한 내용이 존재하지 않았습니다. 추후에 이 문제에 대해서 더 알아볼 예정입니다.

 

 

🤗 자주 사용하는 플러그인

개발하면서 플러그인을 직접 작성할 일은 거의 없습니다. 보통은 webpack에서 직접 제공하는 플러그인을 사용하거나 써드파티 라이브러리를 찾아서 사용하게 되는데, 자주 사용하는 플러그인에 대해서 알아봅시다.

 

BannerPlugin

우리가 만들었던 MyWebpackPlugin과 비슷한 것이 BannerPlugin입니다. 결과물에 빌드 정보나 커밋 버전같은 것을 추가할 수 있습니다.

 

'webpack.config.js' 예시 보기

const webpack = require('webpack');

module.exports = {
	...
	plugins: [
		new webpack.BannerPlugin({
			banner: '이것은 배너 입니다.',
		})
	]
};
  • 이렇게 하면 번들링된 'main.js' 파일에 배너가 추가됩니다.

 

생성자 함수에 전달하는 옵션 객체의 'banner' 속성에 문자열을 전달합니다. webpack 컴파일 타임에 얻을 수 있는 정보, 가령 빌드 시간이나, 커밋정보를 전달하기 위해 함수로 전달할 수도 있습니다.

// webpack.config.js

const webpack = require('webpack');
// node module 중에서 터미널 명령어를 실행시켜주는 child_process를 가져옵니다.
// child_process의 execSync는 문자열을 받아서 터미널에 실행시켜줍니다.
// 우리는 git의 커밋 해시와 유저명을 넣어봅시다.
const childProcess = require('child_process');

module.exports = {
	plugins: [
		new webpack.BannerPlugin({
			banner: () => `
				Build Date: ${new Date().toLocaleString()}
        Commit Version: ${childProcess.execSync('git rev-parse --short HEAD')}
        Author: ${childProcess.execSync('git config user.name')}
			`,
		})
	]
};

 

 

배너 정보가 많다면 별도의 파일로 분리할 수 도 있겠습니다.

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 (
        `
            Build Date: ${date}
            Commit Version: ${commit}
            Author: ${user}
        `
    );
};
// webpack.config.js
const webpack = require('webpack');
const banner = require('./banner.js');

module.exports = {
	plugins: [
		new webpack.BannerPlugin(banner)
	]
};

 

 

이렇게 하면 아래와 같이 배너가 추가되는걸 확인할 수 있습니다.

 

 

DefinePlugin

애플리케이션은 개발환경과 운영환경으로 나눠서 운영합니다. 가령 환경에 따라 API 서버 주소가 다를 수 있습니다. 같은 소스 코드를 두 환경애 배포하기 위해서는 이러한 환경 의존적인 정보를 소스가 아닌 곳에서 관리 하는 것이 좋습니다. 배포할 때마다 코드를 수정하는 것은 곤란하기 때문입니다.

 

webpack은 이러한 환경 정보를 제공하기 위해 DefinePlugin을 제공합니다.

 

'webpack.config.js'에 설정을 추가합니다.

const webpack = require('webpack');

module.exports = {
	...
	plugins: [
		new webpack.DefinePlugin({}),
	]
};

// export default {
//	 plugins: [new webpack.DefinePlugin({})],
// }

 

DefinePlugin에 빈 객체를 전달해도 기본적으로 넣어주는 값이 있습니다. Node 환경정보인 'process.env.NODE_ENV'인데 webpack 설정의 mode에 설정한 값이 여기에 들어갑니다. 우리는 현재 'development'로 설정했기 때문에 'development'라는 값을 얻을 수 있습니다.

 

'app.js' 예시 보기

console.log(process.env.NODE_ENV);

 

'console.log()'로 찍어보면 콘솔창에 'development'라고 출력되는 것을 확인할 수 있습니다.

 

이외에도 webpack 컴파일 시간에 결정되는 값을 전역 상수 문자열로 애플리케이션에 주입할 수 있습니다.

// webpack.config.js

const webpack = require('webpack');

module.exports = {
	...
	plugins: [
		new webpack.DefinePlugin({
			TWO: '1+1',
		}),
	]
};

// app.js
console.log(TWO);

 

 

'TWO'라는 전역 변수에 '1+1'이라는 코드 조각을 넣었습니다. 실제 애플리케이션 코드에서 이것을 출력해보면 2가 나올 것입니다.

 

코드가 아닌 값을 입력하려면 문자열화 한 다음 넘겨주면 됩니다.

// webpack.config.js

const webpack = require('webpack');

module.exports = {
    ...
    plugins: [
        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
console.log(VERSION);
console.log(PRODUCTION);
console.log(MAX_COUNT);
console.log(api.domain);

 

이렇게 'JSON.stringify'를 이용해서 문자열화 한 다음 'console.log()'로 찍어보면 아래와 같이 출력됩니다.

 

빌드 타임에 결정된 값을 애플리케이션에 전달할 때는 이 플러그인을 사용합시다.

 

 

HtmlWebpackPlugin

이번엔 써드 파티 패키지에 대하여 알아봅시다. HtmlTemplatePlugin은 HTML 파일을 후처리하는데 사용합니다. 빌드 타임의 값을 넣거나 코드를 압축할 수 있습니다. 이 플러그인을 사용하면 html 파일을 빌드 과정에 추가할 수 있습니다. html 파일을 빌드 과정에 추가하면 hmtl 파일을 동적으로 만들어 낼 수 있습니다.

 

먼저 패키지를 install 합니다.

npm install html-webpack-plugin

# 개발 환경에서만 사용하려면 -D 옵션을 추가해줍니다.
npm install -D html-webpack-plugin

 

 

이 플러그인으로 빌드하면 HTML파일로 아웃풋에 생성될 것입니다. 'index.html' 파일을 'src/index.html'로 옮긴뒤 다음과 같이 작성해봅시다.

 

'src/index.html'예시 보기

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <!-- <%= env %>는 ejs문법입니다. webpack 쪽에서 templateParameters로 넘겨준 변수를 사용할 수 있습니다. -->
    <title>Document<%= env %></title>
</head>
<body>
    <!-- html-webpack-plugin을 사용할 때는 로딩 스크립트를 제거합니다 -->
    <!-- <script src="dist/main.js"></script> -->
</body>
</html>
  • 타이틀 부분에 ejs 문법을 이용하는데 '<%= env %>'는 전달받은 'env' 변수 값을 출력합니다. HtmlWebpackPlugin은 이 변수에 데이터를 주입시켜 동적으로 HTML 코드를 생성해줍니다.
  • 또한 webpack으로 빌드한 결과물을 자동으로 로딩하는 코드를 주입해주기 때문에 스크립트 로딩 코드도 제거해줍니다.

 

그 다음 'webpack.config.js'에 설정을 추가합니다.

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

module.exports = {
    ...
    plugins: [
        new HtmlWebpackPlugin({
            template: './src/index.html',
            // template에 주입할 parameter 변수 지정
            // templateParameters는 ejs문법(<%= %>)에서 사용할 수 있는 값을 넘겨줄 수 있습니다.
            templateParameters: {
                env: process.env.NODE_ENV === 'development' ? '(개발용)' : ''
            }
        })
    ]
};
  • 환경변수에 따라서 타이틀명 뒤에 '(개발용)'이라는 문자열을 붙이거나 떼도록 했습니다.
  • 'NODE_ENV=development npm run build'으로 설정하고 빌드하면 '타이틀(개발용)'으로 표시됩니다.
  • 'NODE_ENV=production npm run build'으로 설정하고 빌드하면 '타이틀'이라고 표시됩니다.

 

이렇게 해주고 'NODE_ENV=development npm run build'명령어로 빌드하면 아래와 같이 번들링된 'dist/index.html'파일에 (개발용)이라는 문자열이 추가된 것을 확인할 수 있습니다.

  • 예상외로 주석으로 작성했던 부분까지 변경되어졌네요😲

 

추가적으로 개발환경과 달리 운영환경에서는 파일을 압축하고 불필요한 주석을 제거하는 것이 좋은데요. 불필요한 주석을 제거하는 방법을 알아봅시다.

 

'webpack.config.js' 예시 보기

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

module.exports = {
    ...
    plugins: [
        new HtmlWebpackPlugin({
            template: './src/index.html',
            templateParameters: {
                env: process.env.NODE_ENV === 'development' ? '(개발용)' : ''
            },
						// minify 부분에 빈칸 제거, 주석 제거 등을 설정할 수 있습니다.
						// 이번에는 'production' 환경일 때만 빈칸과 주석을 제거하도록 설정해줍니다.
            minify: process.env.NODE_ENV === 'production' ? {
                // 빈칸 제거 여부
                collapseWhitespace: true,
                // 주석 제거 여부
                removeComments: true,
            } : false
        })
    ]
};

 

 

자 이제 'NODE_ENV=production npm run build'명령어로 빌드하면 아래와 같이 빈칸과 주석이 제거되는 것을 볼 수 있습니다.

 

그리고 정적 파일을 배포하면 즉각 브라우저에 반영되지 않는 경우가 있는데요. 그것은 브라우저 캐시가 원인일 수 있는데, 이를 예방할 수 있는 옵션도 존재합니다.

 

'webpack.config.js' 예시 보기

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

module.exports = {
    ...
    plugins: [
        new HtmlWebpackPlugin({
            template: './src/index.html',
            templateParameters: {
                env: process.env.NODE_ENV === 'development' ? '(개발용)' : ''
            },
            minify: process.env.NODE_ENV === 'production' ? {
                collapseWhitespace: true,
                removeComments: true,
            } : false,
						// 정적 파일을 불러올 때 쿼리 문자열에 webpack 해시값을 추가하는 옵션
						hash: true
        })
    ]
};

 

이렇게 'hash: true'옵션을 추가하면 빌드할 때 생성되는 해시값을 정적 파일 로딩 주소의 쿼리 문자열로 붙여서 HTML을 생성합니다.

 

 

CleanWebpackPlugin

CleanWebpackPlugin은 빌드 이전 결과물을 제거하는 플러그인 입니다. 빌드 결과물은 아웃풋 경로에 모이는데 과거 파일이 남아 있을 수 있습니다. 이전 빌드 내용이 덮어쓰기 된다면 상관없겠지만 그렇지 않다면 아웃풋 폴더에 여전히 남아있을 수 있습니다.

 

임의의 파일을 만들고 다시 빌드해봅시다. 그러면 아래처럼 파일이 여전히 남아있는 것을 확인할 수 있습니다.

 

이러한 현상을 CleanWebpackPlugin으로 해결해 봅시다.

 

먼저 패키지를 install 합니다.

npm install clean-webpack-plugin

# 개발 환경에서만 사용하려면 -D 옵션을 추가해줍니다.
npm install -D clean-webpack-plugin

 

그 다음에 'webpack.config.js'에 설정을 추가합니다.

const {CleanWebpackPlugin} = require('clean-webpack-plugin');

module.exports = {
    ...
    plugins: [
        new CleanWebpackPlugin()
    ]
};

 

그 다음에 다시 빌드 해보면 아래처럼 'dist' 폴더가 전부 삭제되었다가 다시 생성되는 것을 확인할 수 있습니다.

 

 

MiniCssExtractPlugin

스타일시트가 점점 많아지면 하나의 Javascript 결과물로 만드는 것이 부담일 수 있습니다. 번들 결과에서 스타일시트 코드만 뽑아서 별도의 CSS 로 만들어 역할에 따라 분리하는 것이 좋습니다. 브라우저에서 큰 파일 하나를 내려받는 것 보다, 여러개의 작은 파일을 동시에 다운로드 하는 것이 더 빠릅니다.

 

개발 환경에서는 CSS를 하나의 모듈로 처리해도 상관없지만 프로덕션 환경에서는 분리하는 것이 효과적입니다. MiniCssExtractPlugin은 CSS를 별도 파일로 뽑아내는 플러그인 입니다.

 

먼저 패키지를 install 합니다.

npm install mini-css-extract-plugin

# 개발 환경에서만 사용하려면 -D 옵션을 추가해줍니다.
npm install -D mini-css-extract-plugin

 

'webpack.config.js'에 설정을 추가합니다.

const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
    ...
    module: {
        rules: [
            {
                test: /\.css$/,
                use: [
                    // MiniCssExtractPlugin을 사용하려면 MiniCssExtractPlugin에서
                    // 제공해주는 로더를 사용해야 합니다. 그러므로 
                    // 개발환경에서는 style-loader, css-loader를 사용하도록 하고,
                    // 운영환경에서는 MiniCssExtractPlugin.loader를 사용하도록 설정합니다.
                    process.env.NODE_ENV === 'production'
                    ? MiniCssExtractPlugin.loader
                    : 'style-loader',
                    'css-loader'
                ]
            }
        ]
    },
    plugins: [
        // 개발환경에서는 CSS를 별도 파일로 뽑아내는 작업이 오히려 효율이 떨어지기 때문에 
        // 운영환경에서만 작동하도록 세팅해줍니다.
        ...(process.env.NODE_ENV === 'production'
          ? [new MiniCssExtractPlugin({filename: '[name].css'})]
          : []
      )
    ]
};

 

이렇게 설정하고 'NODE_ENV=production npm run build'명령어로 빌드하면 'main.css'라는 파일이 생성되고 'dist/index.html'파일에 'link'태그로 'main.css'파일을 가져오는 코드가 추가된 것을 확인할 수 있습니다.

  •  

'Webpack' 카테고리의 다른 글

Webpack - 로더(Loader)  (0) 2024.03.10
Webpack - 엔트리(Entry)/아웃풋(Output)  (0) 2024.03.10
Webpack - Webpack이란?  (0) 2024.03.10