Writing a Plugin
플러그인은 서드파티 개발자들에게 webpack 엔진의 모든 잠재력을 보여줍니다. 단계별로 빌드 callback을 사용하여 개발자들은 webpack의 빌드 프로세스에 자신만의 행동을 도입할 수 있습니다. 플러그인을 빌드하는 것은 로더를 빌딩 하는 것보다 조금 더 진보되었습니다. 왜냐하면, 플러그인은 몇몇 webpack의 low-level에 내장된 훅을 이해할 필요가 있기 때문입니다. 이제 소스 코드를 읽어볼 준비를 해봅시다!
Creating a Plugin
Webpack용 plugin은 다음과 같이 구성됩니다.
- JavaScript function 또는 JavaScript class로 이름을 붙입니다.
- 프로토타입에서
apply메소드를 정의합니다. - 특정한 이벤트 훅으로 지정합니다.
- webpack 내부에 있는 인스턴스의 특정한 데이터를 처리합니다.
- 기능적으로 완벽해지면 webpack에서 제공하는 callback을 호출합니다.
// A JavaScript class.
class MyExampleWebpackPlugin {
// `apply`를 compiler를 인자로 받는 프로토타입 메소드로 정의합니다.
apply(compiler) {
// attach할 이벤트 훅을 지정합니다.
compiler.hooks.thisCompilation.tap(
"MyExampleWebpackPlugin",
(compilation) => {
// Specify the asset processing hook
compilation.hooks.processAssets.tap(
{
name: "MyExampleWebpackPlugin",
stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS,
},
(assets) => {
console.log("This is an example plugin!");
console.log(
"Here’s the `compilation` object which represents a single build of assets:",
compilation,
);
},
);
},
);
}
}Basic plugin architecture
플러그인은 apply라는 프로토타입 메소드로 인스턴스화 된 객체입니다. 이러한 apply 메소드는 플러그인을 설치하는 동안 webpack 컴파일러에 의해 한번 호출됩니다. apply 메소드는 컴파일러 callback에 대해 접근 권한을 부여하는 기본 webpack 컴파일러에 의해 참조됩니다. 플러그인의 구조는 다음과 같습니다.
class HelloWorldPlugin {
apply(compiler) {
// 이 훅은 빌드 프로세스가 완전히 완료되었을 때 실행됩니다.
compiler.hooks.done.tap(
"Hello World Plugin",
(stats /* 훅의 탭이 끝나면 stats는 인자로 통과됩니다. */) => {
console.log("Hello World!");
},
);
}
}
export default HelloWorldPlugin;그런 다음에 플러그인을 사용하려면 webpack의 설정인 plugins 배열에 인스턴스를 포함해야합니다.
// webpack.config.js
import HelloWorldPlugin from "hello-world";
export default {
// ... 여기서 구성을 설정할 것 ...
plugins: [new HelloWorldPlugin({ options: true })],
};5.106 이전 버전의 webpack에서 플러그인 옵션의 유효성을 검사하는 일반적인 방법은 생성자에서 schema-utils를 사용하는 것입니다.
import { validate } from "schema-utils";
// options 객체에 대한 schema
const schema = {
type: "object",
properties: {
test: {
type: "string",
},
},
};
export default class HelloWorldPlugin {
constructor(options = {}) {
validate(schema, options, {
name: "Hello World Plugin",
baseDataPath: "options",
});
}
apply(compiler) {}
}webpack 5.106 버전부터는 compiler.hooks.validate를 활용하고 compiler.validate(...)를 호출함으로써 해당 유효성 검사를 apply() 함수 안으로 옮길 수 있습니다.
export default class HelloWorldPlugin {
constructor(options = {}) {
this.options = options;
}
apply(compiler) {
compiler.hooks.validate.tap("HelloWorldPlugin", () => {
compiler.validate(
() => require("./schema/hello-world-plugin.json"),
this.options,
);
});
}
}Compiler and Compilation
플러그인 개발 중 가장 중요한 두 개의 리소스는 compiler 와 compilation 객체입니다. 두 가지의 역할을 이해하는 것은 webpack 엔진을 확장하는 첫 번째 단계입니다.
class HelloCompilationPlugin {
apply(compiler) {
// callback 함수에 인자로 compilation을 제공하는 훅으로 compliation 훅을 탭 합니다.
compiler.hooks.compilation.tap("HelloCompilationPlugin", (compilation) => {
// 이제 컴파일을 통해 이용할 수 있는 다양한 훅들을 이용할 수 있습니다.
compilation.hooks.optimize.tap("HelloCompilationPlugin", () => {
console.log("Assets are being optimized.");
});
});
}
}
export default HelloCompilationPlugin;compiler, compilation 그리고 다른 중요한 객체에서 사용 가능한 훅의 목록은 플러그인 API 문서를 참고해 주세요.
Async event hooks
일부 플러그인 훅은 비동기입니다. 이를 탭 하려면, 동기로 동작하는 tap 메소드를 사용하거나 비동기 메소드인 tapAsync 또는 tapPromise 메소드를 사용할 수 있습니다.
tapAsync
tapAsync 메소드를 사용하여 플러그인을 탭해야 할 경우 함수에서 마지막 인자로 제공되는 callback 함수를 호출해야 할 필요가 있습니다.
class HelloAsyncPlugin {
apply(compiler) {
compiler.hooks.emit.tapAsync(
"HelloAsyncPlugin",
(compilation, callback) => {
// Do something async...
setTimeout(() => {
console.log("Done with async work...");
callback();
}, 1000);
},
);
}
}
export default HelloAsyncPlugin;tapPromise
플러그인에 탭 하기 위해 tapPromise를 사용할 때, 동기식 작업이 완료될 때의 해석 promise를 반드시 반환해야 할 필요가 있습니다.
class HelloAsyncPlugin {
apply(compiler) {
compiler.hooks.emit.tapPromise(
"HelloAsyncPlugin",
(compilation) =>
// 끝났을 때 해석을 반환해야 합니다..
new Promise((resolve, reject) => {
setTimeout(() => {
console.log("Done with async work...");
resolve();
}, 1000);
}),
);
}
}
export default HelloAsyncPlugin;Example
일단 webpack compiler와 각각의 개별적인 compilations를 이해 할 수 있게 되면, 엔진 자체로 할 수 있는 가능성이 무궁무진해집니다. 기존 파일을 다시 포맷하거나 파생 파일을 만들거나 완전히 새로운 애셋을 만들 수 있습니다.
새로운 빌드 파일인 assets.md를 생성하는 예제를 작성해봅시다. 내용은 빌드에 있는 모든 애셋 파일을 나열합니다. 이 플러그인은 다음과 같습니다.
class FileListPlugin {
static defaultOptions = {
outputFile: "assets.md",
};
// 어떤 옵션이 플러그인의 생성자에 전달될 때,
// (이는 플러그인의 public API입니다.)
constructor(options = {}) {
// 기본 옵션에 대해 사용자 지정 옵션을 적용하고
// 병합된 옵션을 플러그인 메소드에 추가로 사용할 수 있게 합니다.
// 여기서 모든 옵션을 확인해야 합니다.
this.options = { ...FileListPlugin.defaultOptions, ...options };
}
apply(compiler) {
const pluginName = FileListPlugin.name;
// webpack 모듈 인스턴스는 컴파일러 객체로부터 접근할 수 있으며,
// 이는 사용되는 모듈의 정확한 버전을 보장합니다.
// (webpack 또는 직접적으로 어떤 심볼을 require/import 하지 말아야 합니다.)
const { webpack } = compiler;
// Compilation 객체는 몇 가지 유용한 상수에 대한 참조를 제공합니다.
const { Compilation } = webpack;
// Rawsource는 compilation에서 애셋 소스를 나타내는데
// 사용해야 하는 "sources" 클래스 중 하나입니다.
const { RawSource } = webpack.sources;
// 이전 단계에서 compilation 프로세스를 추가로 탭하기 위해서는
// "thisCompilation" 훅을 탭해야합니다.
compiler.hooks.thisCompilation.tap(pluginName, (compilation) => {
// 특정 단계에서 애셋 처리 파이프라인으로 탭합니다.
compilation.hooks.processAssets.tap(
{
name: pluginName,
// 이후 애셋 처리 단계 중 하나를 사용하여
// 모든 애셋이 이미 다른 플러그인에 의해 compliation에 추가되었는지 확인합니다.
stage: Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE,
},
(assets) => {
// "assets"는 compilation의 모든 애셋을 포함하는 객체이고,
// 객체의 key는 애셋의 경로 이름입니다.
// 그리고 values 파일 소스입니다.
// 모든 애셋을 거쳐 반복하고
// 마크다운 파일로 내용을 생성합니다.
const content = `# In this build:\n\n${Object.keys(assets)
.map((filename) => `- ${filename}`)
.join("\n")}`;
// 출력 디렉터리 webpack에 의해 자동으로 생성되도록
// compilation에 새로운 애셋을 추가합니다.
compilation.emitAsset(
this.options.outputFile,
new RawSource(content),
);
},
);
});
}
}
export default FileListPlugin;webpack.config.js
import FileListPlugin from "./file-list-plugin.js";
// 플러그인에 webpack 설정을 사용합니다.
export default {
// …
plugins: [
// 기본 옵션을 사용하여 플러그인을 추가합니다.
new FileListPlugin(),
// 또는
// 지원되는 옵션을 전달하도록 선택할 수 있습니다.
new FileListPlugin({
outputFile: "my-assets.md",
}),
],
};이렇게 하면 다음과 같은 이름을 가진 마크다운 파일이 생성됩니다.
# In this build:
- main.css
- main.js
- index.htmlWatching for file changes
webpack이 watch 모드(webpack --watch 또는 webpack serve)로 실행될 때,
파일 변경으로 인해 재빌드가 발생할 때마다 새로운 컴파일을 생성합니다.
compiler.modifiedFiles 세트는 플러그인이 어떤 특정 파일이 재빌드를 트리거했는지 알 수 있도록 해주므로, 관련 없는 파일에 대한 비용이 많이 드는 작업을 건너뛸 수 있습니다.
이 기능은 특정 파일 변경 사항에만 반응해야 하는 플러그인에 유용합니다. (예: 에셋 재생성, 템플릿 재처리 또는 캐시 무효화).
class WatchNotifierPlugin {
apply(compiler) {
compiler.hooks.watchRun.tap("WatchNotifierPlugin", (compiler) => {
if (compiler.modifiedFiles) {
const changedFiles = [...compiler.modifiedFiles]
.map((file) => ` • ${file}`)
.join("\n");
console.log(`\nFiles changed:\n${changedFiles}`);
}
});
}
}
export default WatchNotifierPlugin;Note:
compiler.modifiedFiles는 배열이 아니라Set입니다.- 첫 번째(콜드) 빌드 시에는
undefined입니다.- 이 변수는 watch 재빌드 중에만 채워집니다.
Adding custom file dependencies
플러그인이 webpack이 기본적으로 추적하지 않는 외부 파일(설정 파일, 템플릿 등)을 읽는 경우, webpack이 해당 파일을 감시하도록 지정해야 합니다.
webpack에게 다양한 유형의 종속성을 감시하도록 지시할 수 있습니다.
-
compilation.fileDependencies는 플러그인이 의존하는 개별 파일을 추적하는 데 사용되므로, webpack은 해당 파일이 변경될 때 재빌드를 트리거할 수 있습니다. -
compilation.contextDependencies는 디렉터리를 감시하는 데 사용되므로 해당 디렉터리 내의 변경 사항이 발생하면 다시 빌드가 트리거됩니다. -
compilation.missingDependencies는 현재 누락된 파일을 추적하는 데 사용되므로 해당 파일이 생성되면 webpack이 다시 빌드를 트리거할 수 있습니다.
import path from "node:path";
class TemplateWatchPlugin {
apply(compiler) {
compiler.hooks.compilation.tap("TemplateWatchPlugin", (compilation) => {
const templatePath = path.resolve(__dirname, "my-template.html");
// webpack이 이 파일을 감시하도록 설정하세요
compilation.fileDependencies.add(templatePath);
// 디렉토리 감시(컨텍스트 종속성)
const templatesDir = path.resolve(__dirname, "templates");
compilation.contextDependencies.add(templatesDir);
// 예시: 누락된 종속성을 표시합니다.
const missingFile = path.resolve(__dirname, "missing-file.txt");
compilation.missingDependencies.add(missingFile);
});
}
}
export default TemplateWatchPlugin;fileDependencies.add()를 호출하지 않으면 플러그인이 해당 파일에 의존하더라도 파일이 변경될 때 webpack은 다시 빌드를 트리거하지 않습니다.
Different Plugin Shapes
플러그인은 탭 하는 이벤트 훅에 따라 타입으로 분류될 수 있습니다. 모든 이벤트 훅은 동기 또는 비동기 waterfall 또는 병렬 훅으로 사전에 정의되어 있고 훅은 내부적으로 call/callAsync 메소드를 사용하여 호출됩니다. 지원되거나 탭이 된 훅의 목록은 일반적으로 this.hooks 프로퍼티에 명시되어 있습니다.
예시는 다음과 같습니다.
this.hooks = {
shouldEmit: new SyncBailHook(["compilation"]),
};이것은 SyncBailHook 타입의 훅인 shouldEmit만이 지원되는 훅이고 shouldEmit 훅에 탭 되는 플러그인에 전달되는 유일한 파라미터는 compilation입니다.
지원되는 다양한 훅의 타입은 다음과 같습니다.
Synchronous Hooks
-
SyncHook
new SyncHook([params])로 정의됩니다.tap메소드를 사용하여 탭 됩니다.call(...params)메소드를 사용하여 호출됩니다.
-
Bail Hooks
SyncBailHook[params]로 정의됩니다.tap메소드를 사용하여 탭 됩니다.call(...params)메소드를 사용하여 호출됩니다.
Bail Hook 타입의 훅들은, 각각의 callback 플러그인이 특정한
args를 사용하여 차례로 호출됩니다. 만약 플러그인에 의해 정의되지 않은 값을 제외하고 값이 반환된다면 훅에 의해 값이 반환되고 추가 플러그인 callback은 호출되지 않습니다. 많은 유용한optimizeChunks,optimizeChunkModules와 같은 이벤트는 SyncBailHooks입니다. -
Waterfall Hooks
SyncWaterfallHook[params]로 정의됩니다.tap메소드를 사용하여 탭 됩니다.call(...params)메소드를 사용하여 호출됩니다.
여기서 각각 플러그인은 이전 플러그인의 반환 값으로부터 인자들을 차례로 호출합니다. 플러그인은 실행 순서를 반드시 고려해야합니다. 실행된 이전의 플러그인으로부터 인자들을 반드시 허용해야합니다. 첫 번째 플러그인의 값은
init입니다. 따라서 waterfall hooks에는 최소 1개의 parameter를 제공해야 합니다. 이 패턴은ModuleTemplate,ChunkTemplate등과 같은 webpack 템플릿과 관련된 탭 될 수 있는 인스턴스에서 사용됩니다.
Asynchronous Hooks
-
Async Series Hook
AsyncSeriesHook[params]로 정의됩니다.tap/tapAsync/tapPromise메소드를 사용하여 탭 됩니다.callAsync(...params)메소드를 사용하여 호출됩니다.
플러그인 핸들러 함수는
(err?: Error) -> void서명이 있는 callback 함수와 모든 인자들이 호출됩니다. 핸들러 함수는 등록된 순서대로 호출 됩니다.callback은 모든 핸들러가 호출되고 난 뒤 호출됩니다. Async Series Hook은 또한emit,run과 같은 이벤트에 흔히 사용되는 패턴입니다. -
Async waterfall 플러그인은 waterfall 방식으로 비동기식으로 적용될 것입니다.
AsyncWaterfallHook[params]로 정의됩니다.tap/tapAsync/tapPromise메소드를 사용하여 탭 됩니다.callAsync(...params)메소드를 사용하여 호출됩니다.
플러그인 핸들러 함수는
(err: Error, nextValue: any) -> void.서명이 있는 callback 함수와 현재 값으로 호출됩니다.nextValue가 호출되면 다음 핸들러의 현재 값이 됩니다. 첫 번째 핸들러의 현재 값은init입니다. 모든 핸들러가 적용된 후 callback은 마지막 값으로 호출됩니다. 만약 어떤 핸들러가err값을 전달하면 callback은 오류가 호출되고 더이상 핸들러는 호출되지 않습니다. 이 플러그인 패턴은before-resolve와after-resolve와 같은 이벤트에서 사용할 수 있습니다. -
Async Series Bail
AsyncSeriesBailHook[params]로 정의됩니다.tap/tapAsync/tapPromise메소드를 사용하여 탭 됩니다.callAsync(...params)메소드를 사용하여 호출됩니다.
-
Async Parallel
AsyncParallelHook[params]로 정의됩니다.tap/tapAsync/tapPromise메소드를 사용하여 탭 됩니다.callAsync(...params)메소드를 사용하여 호출됩니다.
Configuration defaults
플러그인의 기본값이 적용된 후 webpack은 설정의 기본값을 적용합니다. 이를 통해 플러그인은 고유의 기본값을 제공하고 사전에 플러그인의 설정을 만드는 방법을 제공합니다.
Example: AssetLoggerPlugin
다음 예시는 Webpack의 로거 인터페이스를 사용하여 생성된 모든 애셋을 로깅하는 최소한의 Webpack 플러그인을 보여줍니다.
class AssetLoggerPlugin {
apply(compiler) {
compiler.hooks.thisCompilation.tap("AssetLoggerPlugin", (compilation) => {
const logger = compilation.getLogger("AssetLoggerPlugin");
compilation.hooks.processAssets.tap(
{
name: "AssetLoggerPlugin",
stage: compilation.constructor.PROCESS_ASSETS_STAGE_SUMMARIZE,
},
(assets) => {
logger.info("Generated assets:");
for (const assetName of Object.keys(assets)) {
logger.info(assetName);
}
},
);
});
}
}
export default AssetLoggerPlugin;이 플러그인은 Webpack 컴파일러의 emit 훅을 활용하여 생성된 모든 애셋의 이름을 콘솔에 출력합니다. Webpack 훅을 사용하여 플러그인이 컴파일 프로세스와 상호 작용하는 방법을 보여주는 예시입니다.
예를 들어, Webpack 빌드를 실행하면 다음과 같은 출력이 나타날 수 있습니다.
Generated assets:
main.js
vendor.js
styles.css


