Webpack Plugin 개발

Webpack Plugin 개발

Plugin은 Webpack 엔진의 모든것을 개발자에게 공개합니다. 단계별 빌드 Callback을 사용하여 개발자는 Webpack 빌드 프로세스에 자신의 동작을 추가할 수 있습니다. Plugin 작성은 Loader를 빌드하는 것보다 조금더 진보된 것입니다. 왜냐하면 Webpack 하위 레벨 내부를 이해해야 할 필요가 있기 때문입니다.

Plugin 만들기

Webpack 용 Plugin은 다음과 같이 구성됩니다.

  • 이름이 부여된 JavaScript 함수.
  • 프로토 타입에 apply 메서드를 정의.
  • Webpack의 Event Hook을 특정하고 Attach.
  • Webpack 내부 인스턴스 특정 데이터를 조작.
  • 기능이 완료된 후 Webpack에서 Callback을 호출.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 이름이 부여된 JavaScript의 함수
function MyExampleWebpackPlugin() {
};
// prototype에 `apply` 메서드 정의
MyExampleWebpackPlugin.prototype.apply = function(compiler) {
// Webpack의 Event Hook을 지정하고 Attack
compiler.plugin('webpacksEventHook', function(compilation /* Webpack 내부 인스턴스 특정 데이터 조작. */, callback) {
console.log("This is an example plugin!!!");
// 기능이 완료된 후 Webpack에서 Callback을 호출.
callback();
});
};

Plugin을 개발할 때 가장 중요한 두가지 자원 compilercompilation 객체가 있습니다. 이 두 객체의 역할을 이해하는 것은 Webpack 엔진을 확장하는 중요한 첫단계입니다.

  • compiler 객체는 완전히 구성된 Webpack 환경을 나타냅니다. 이 객체는 Webpack이 시작할 때 한번 빌드되며 Option, Loader 및 Plugin을 포함한 모든 작업 설정으로 구성됩니다. Webpack 환경에 Plugin을 적용할 때 Plugin이 compiler에 대한 참조를 받습니다. compiler를 사용하여 기본 Webpack 환경을 액세스할 수 있습니다.

  • compilation 객체는 버전이 있는 Asset 하나의 빌드를 나타냅니다. Webpack 개발 미들웨어를 실행하는 동안 파일 변경이 감지될 때마다 새로운 compilation이 생성되어 새로운 컴파일된 Asset이 생성됩니다. compilation은 모듈 리소스, 컴파일된 Asset, 변경된 파일 감시
    및 종속성의 현재 상태에 대한 정보를 나타냅니다. compilation은 또한 Plugin이 사용자 정의 작업을 수행하도록 선택할 수 있는 많은 Callback 포인트를 제공합니다.

두 구성 요소는 모든 Webpack Plugin (특히 compilation)의 핵심 부분이므로 개발자는 다음 소스 파일을 알아두면 도움이됩니다.

Plugin 기본 구조

Plugin은 프로토타입에 apply 메서드가있는 인스턴스 객체입니다. 이 apply 메서드는 Plugin을 설치하는 동안 Webpack compiler에 의해 한번 호출됩니다. apply 메서드는 compiler Callback에 대한 액세스를 허용하는 기본 Webpack compiler에 대한 참조를 제공받습니다. 간단한 Plugin은 다음과 같이 구성됩니다.

1
2
3
4
5
6
7
8
9
10
11
function HelloWorldPlugin(options) {
// Option으로 Plubin 인스턴스 설정...
}
HelloWorldPlugin.prototype.apply = function(compiler) {
compiler.plugin('done', function() {
console.log('Hello World!');
});
};
module.exports = HelloWorldPlugin;

그런 다음 Plugin을 설치하려면 Webpack 설정파일의 plugins 배열에 인스턴스를 포함하면 됩니다.

1
2
3
4
5
6
7
8
var HelloWorldPlugin = require('hello-world');
var webpackConfig = {
// ... config settings here ...
plugins: [
new HelloWorldPlugin({options: true})
]
};

compilation 접근하기

compiler 객체를 사용하면 각각의 새로운 compilation에 대한 참조를 제공하는 Callback을 바인드할 수 있습니다. 이러한 compilation은 빌드 프로세스 내에서 수많은 단계를 Hook하기 위한 콜백을 제공합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function HelloCompilationPlugin(options) {}
HelloCompilationPlugin.prototype.apply = function(compiler) {
// compilation에 접근하기 위한 Callback 설정
compiler.plugin("compilation", function(compilation) {
// 이제 compilation 단계들에 접근하기 위한 Callback 설정
compilation.plugin("optimize", function() {
console.log("Assets are being optimized.");
});
});
};
module.exports = HelloCompilationPlugin;

compiler,compilation 및 다른 중요한 객체에서 사용할 수있는 Callback에 대한 자세한 내용은 plugins을 참조하십시오.

비동기 compilation Plugin

일부 compilation Plugin 단계는 비동기이며, Plugin 실행이 끝나면 호출되어야하는 Callback 함수를 전달합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function HelloAsyncPlugin(options) {}
HelloAsyncPlugin.prototype.apply = function(compiler) {
compiler.plugin("emit", function(compilation, callback) {
// Do something async...
setTimeout(function() {
console.log("Done with async work...");
callback();
}, 1000);
});
};
module.exports = HelloAsyncPlugin;

예제

일단 Webpack compiler와 개별 compilation에 접근하게 되면 엔진을 이용해할 수 있는 일이 많아집니다. 기존 파일을 다시 형식화하거나 파생 파일을 만들거나 완전히 새로운 Asset을 만들 수 있습니다.

filelist.md라는 새로운 빌드 파일을 생성하는 간단한 예제 Plugin을 작성해 보겠습니다. 이 파일의 내용에는 빌드의 모든 Asset 파일이 나열됩니다. 이 Plugin은 아래와 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function FileListPlugin(options) {}
FileListPlugin.prototype.apply = function(compiler) {
compiler.plugin('emit', function(compilation, callback) {
// Create a header string for the generated file:
var filelist = 'In this build:\n\n';
// Loop through all compiled assets,
// adding a new line item for each filename.
for (var filename in compilation.assets) {
filelist += ('- '+ filename +'\n');
}
// Insert this list into the webpack build as a new file asset:
compilation.assets['filelist.md'] = {
source: function() {
return filelist;
},
size: function() {
return filelist.length;
}
};
callback();
});
};
module.exports = FileListPlugin;

Plugin의 다른 형태

Plugin은 등록된 이벤트에 따라 여러 종류로 분류할 수 있습니다. 모든 Event Hook은 레지스트리에 Plugin을 적용하는 방법을 결정합니다.

  • synchronous Tapable 인스턴스는 다음을 사용하여 Plugin을 적용합니다.

    1
    applyPlugins(name: string, args: any...)
    1
    applyPluginsBailResult(name: string, args: any...)

즉, 각 Plugin 콜백은 특정 args를 사용하여 차례로 호출됩니다. 이것은 Plugin의 가장 간단한 형식입니다. "compile", "this-compilation"과 같은 많은 유용한 이벤트는 Plugin이 동기 실행할 것을 요구합니다.

  • waterfall Plugin을 사용하여 적용

    1
    applyPluginsWaterfall(name: string, init: any, args: any...)

여기서 각 Plugin은 이전 Plugin의 반환값에서 args를 사용하여 순차적으로 호출됩니다. Plugin은 실행 순서를 고려해야합니다. 실행된 이전 Plugin의 인수를 받아 들여야합니다. 첫 번째 Plugin의 값은 init입니다. 이 패턴은 ModuleTemplate, ChunkTemplate과 같은webpack 템플릿과 관련된 Tapable 인스턴스에서 사용됩니다.

  • asynchronous 모든 Plugin이 비동기적으로 적용될 때
    1
    applyPluginsAsync(name: string, args: any..., callback: (err?: Error) -> void)

Plugin 핸들러 함수는 모든 args와 (err ?: Error) -> void 시그니처를 가진 콜백 함수로 호출됩니다. 핸들러 함수는 등록된 순서로 호출되며 callback은 모든 핸들러가 호출된 후에 호출됩니다. 이것은 또한 "emit", "run"과 같은 Event에 일반적으로 사용되는 패턴입니다.

  • async waterfall Plugin은 waterfall 방식으로 비동기로 적용됩니다

    1
    applyPluginsAsyncWaterfall(name: string, init: any, callback: (err: Error, result: any) -> void)

Plugin 핸들러 함수는 현재 값과 (err : Error, nextValue : any) -> void 시그니처가 있는 콜백 함수로 호출됩니다. nextValue가 호출되면 다음 핸들러의 현재 값이 됩니다. 첫 번째 핸들러의 현재 값은 init입니다. 모든 핸들러가 적용된 후 마지막 값으로 callback이 호출됩니다. 어떤 핸들러가 err에 값을 전달하면, 이 에러와 함께 callback이 호출되고 핸들러는 더 이상 호출되지 않습니다. 이 Plugin 패턴은 "before-resolve""after-resolve"와 같은 Event에 필요합니다.

  • async series 비동기와 동일하지만 등록된 Plugin 중 하나라도 실패하면 Plugin이 더 이상 호출되지 않습니다.

    1
    applyPluginsAsyncSeries(name: string, args: any..., callback: (err: Error, result: any) -> void)
  • parallel

    1
    applyPluginsParallel(name: string, args: any..., callback: (err?: Error) -> void)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    applyPluginsParallelBailResult(name: string, args: any..., callback: (err: Error, result: any) -> void)
    ``
    ## 유용한 Plugin 패턴
    Plugin은 Webpack 빌드 시스템 내에서 사용자 정의를 수행할 수 있는 무제한의 기회를 부여합니다. 이를 통해 사용자 정의 Asset을 만들거나, 고유한 빌드 수정을 수행하거나, 미들웨어를 사용하면서 Webpack 런타임을 향상시킬 수 있습니다. 다음은 Plugin 작성 중에 매우 유용하게 사용되는 Webpack의 일부 기능입니다.
    ### Asset, Chunk, 모듈 및 종속성 탐색
    `compilation`이 완료된 후 `compilation` 내의 모든 구조를 탐색할 수 있습니다.
    ```javascript
    function MyPlugin() {}
    MyPlugin.prototype.apply = function(compiler) {
    compiler.plugin('emit', function(compilation, callback) {
    // 각 chunk를 탐색 (build output):
    compilation.chunks.forEach(function(chunk) {
    // chunk 내의 각 모듈 탐색 (built inputs):
    chunk.forEachModule(function(module) {
    // 모듈에 포함된 각 소스 파일 경로를 탐색합니다.:
    module.fileDependencies.forEach(function(filepath) {
    // 이제 소스 구조에 대해 많은 것을 알게 되었습니다.
    });
    });
    // chunk에 의해 생성된 각 asset 파일을 탐색합니다.
    chunk.files.forEach(function(filename) {
    // chunk에 의해 생성된 각 파일의 asset 소스 가져 오기
    var source = compilation.assets[filename].source();
    });
    });
    callback();
    });
    };
    module.exports = MyPlugin;
  • compilation.modules : compilation의 모듈 배열 (Built 입력). 각 모듈은 소스 라이브러리에서 Raw 파일의 빌드를 관리합니다.

  • module.fileDependencies : 모듈에 포함된 소스 파일 경로의 배열. 여기에는 소스 JavaScript 파일 자체 (예 : index.js) 및 필요한 모든 종속성 Asset 파일 (스타일 시트, 이미지 등)이 포함됩니다. 종속성 검토는 소스 파일이 모듈에 속하는지 확인하는데 유용합니다.
  • compilation.chunks : 컴파일에서 chunk 배열 (빌드 결과). 각 Chunk는 최종 렌더링 Asset의 구성을 관리합니다.
  • chunk.modules : Chunk에 포함되는 모듈의 배열. 확장에 따라 각 모듈의 종속성을 조사하여 청크에 공급된 Raw 소스 파일을 확인할 수 있습니다.
  • chunk.files : Chunk에 의해 생성된 결과 파일 이름들의 배열. compilation.assets 테이블에서 이러한 Asset 소스에 액세스할 수 있습니다.

Watch 그래프 모니터링

Webpack 미들웨어를 실행하는 동안 각 compilation에는 Watch 대상 파일인 fileDependencies 배열과 Watch된 파일 경로를 타임스탬프에 매핑하는 fileTimestamps가 포함됩니다. 이는 compilation 과정에서 변경된 파일을 탐지하는데 매우 유용합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function MyPlugin() {
this.startTime = Date.now();
this.prevTimestamps = {};
}
MyPlugin.prototype.apply = function(compiler) {
compiler.plugin('emit', function(compilation, callback) {
var changedFiles = Object.keys(compilation.fileTimestamps).filter(function(watchfile) {
return (this.prevTimestamps[watchfile] || this.startTime) < (compilation.fileTimestamps[watchfile] || Infinity);
}.bind(this));
this.prevTimestamps = compilation.fileTimestamps;
callback();
}.bind(this));
};
module.exports = MyPlugin;

새로운 파일 경로를 Watch 그래프에 제공하여 파일이 변경될 때 compilation 트리거를 수신할 수 있습니다. 유효한 파일 경로를 compilation.fileDependencies 배열에 넣어 Watch에 추가하면됩니다. 참고 : fileDependencies 배열은 각 compilation에서 다시 작성되므로 Plugin은 Watch 종속성을 각 compilation에 넣어 Watch를 계속 유지해야합니다.

변경된 Chunk

Watch 그래프와 마찬가지로 해시를 추적하여 compilation내에서 변경된 Chunk(또는 해당 모듈)를 모니터링하는 것도 가능합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function MyPlugin() {
this.chunkVersions = {};
}
MyPlugin.prototype.apply = function(compiler) {
compiler.plugin('emit', function(compilation, callback) {
var changedChunks = compilation.chunks.filter(function(chunk) {
var oldVersion = this.chunkVersions[chunk.name];
this.chunkVersions[chunk.name] = chunk.hash;
return chunk.hash !== oldVersion;
}.bind(this));
callback();
}.bind(this));
};
module.exports = MyPlugin;

이 내용은 나중에 참고하기 위해 제가 공부하며 정리한 내용입니다.
의역, 오역, 직역이 있을 수 있음을 알려드립니다.
This post is a translation of this original article [https://webpack.js.org/contribute/writing-a-plugin/]
참고 : https://github.com/webpack/docs/wiki/How-to-write-a-plugin

공유하기