개요
훅은 플러그인이 에디터의 생명주기에 연결하고 특정 지점에서 동작을 수정할 수 있는 방법을 제공합니다. 핵심 코드를 수정하지 않고 기능을 확장할 수 있는 플러그인 아키텍처를 가능하게 합니다.
훅 시스템은 webpack의 tapable 시스템과 Vue의 플러그인 시스템에서 영감을 받았습니다. 동기 및 비동기 훅을 모두 제공하여 유연한 플러그인 통합을 가능하게 합니다.
동기 훅
기본 SyncHook
동기 훅은 콜백을 순차적으로 실행합니다. 각 콜백은 동일한 인수를 받습니다:
export class SyncHook {
#callbacks = [];
tap(fn) {
this.#callbacks.push(fn);
}
call(...args) {
this.#callbacks.forEach(callback => {
callback(...args);
});
}
untap(fn) {
const index = this.#callbacks.indexOf(fn);
if (index > -1) {
this.#callbacks.splice(index, 1);
}
}
}
// Usage
const hooks = {
beforeOperation: new SyncHook(),
afterOperation: new SyncHook(),
render: new SyncHook()
};
// Register callbacks
hooks.beforeOperation.tap((operation) => {
console.log('Before operation:', operation);
});
hooks.beforeOperation.tap((operation) => {
// Can modify operation
if (operation.type === 'insertText' && operation.text.length > 100) {
operation.text = operation.text.substring(0, 100);
}
});
// Call hook
hooks.beforeOperation.call(operation);워터폴 훅
워터폴 훅은 반환 값을 다음 콜백에 전달합니다:
export class SyncWaterfallHook {
#callbacks = [];
tap(fn) {
this.#callbacks.push(fn);
}
call(initialValue) {
return this.#callbacks.reduce((value, callback) => {
return callback(value);
}, initialValue);
}
}
// Usage: Transform document through multiple plugins
const transformHook = new SyncWaterfallHook();
transformHook.tap((doc) => {
// Plugin 1: Normalize whitespace
return normalizeWhitespace(doc);
});
transformHook.tap((doc) => {
// Plugin 2: Validate structure
return validateStructure(doc);
});
const transformed = transformHook.call(originalDocument);비동기 훅
병렬 훅
비동기 훅은 병렬 실행을 허용합니다:
export class AsyncParallelHook {
#callbacks = [];
tapPromise(fn) {
this.#callbacks.push(fn);
}
async promise() {
// Execute all callbacks in parallel
await Promise.all(this.#callbacks.map(callback => callback()));
}
}
// Usage
const hooks = {
initAsync: new AsyncParallelHook()
};
// Multiple plugins can initialize in parallel
hooks.initAsync.tapPromise(async () => {
await loadUserPreferences();
});
hooks.initAsync.tapPromise(async () => {
await loadDocumentHistory();
});
// Both run in parallel
await hooks.initAsync.promise();시리즈 훅
시리즈 훅은 순차적으로 실행됩니다:
export class AsyncSeriesHook {
#callbacks = [];
tapPromise(fn) {
this.#callbacks.push(fn);
}
async promise() {
// Execute callbacks sequentially
for (const callback of this.#callbacks) {
await callback();
}
}
}
// Usage
const hooks = {
saveAsync: new AsyncSeriesHook()
};
// Save operations must be sequential
hooks.saveAsync.tapPromise(async () => {
await saveToLocalStorage();
});
hooks.saveAsync.tapPromise(async () => {
await syncToServer();
});
// Run sequentially
await hooks.saveAsync.promise();훅 생명주기 관리
훅 정의
에디터 작업을 위한 완전한 훅 생명주기를 정의합니다:
// Editor hooks definition
const hooks = {
// Initialization
init: new SyncHook(),
initAsync: new AsyncParallelHook(),
ready: new SyncHook(),
// Operations
beforeOperation: new SyncHook(),
operation: new SyncHook(),
afterOperation: new SyncHook(),
// Rendering
beforeRender: new SyncHook(),
render: new SyncHook(),
afterRender: new SyncHook(),
// Selection
selectionChange: new SyncHook(),
// Cleanup
destroy: new SyncHook()
};
class Editor {
constructor() {
this.#hooks = hooks;
}
applyOperation(operation) {
// Before operation hooks
this.#hooks.beforeOperation.call(operation);
// Apply operation
this.#doApplyOperation(operation);
// Operation hooks
this.#hooks.operation.call(operation);
// After operation hooks
this.#hooks.afterOperation.call(operation);
// Trigger render
this.render();
}
render() {
const doc = this.getDocument();
this.#hooks.beforeRender.call(doc);
this.#doRender(doc);
this.#hooks.render.call(doc);
this.#hooks.afterRender.call(doc);
}
}훅 통합
플러그인은 훅과 통합할 수 있습니다:
class HistoryPlugin {
apply(editor) {
// Hook into operations
editor.hooks.beforeOperation.tap((operation) => {
// Save state before operation
this.#saveState(editor.getDocument());
});
editor.hooks.afterOperation.tap((operation) => {
// Update history after operation
this.#updateHistory(operation);
});
}
}
class ValidationPlugin {
apply(editor) {
// Hook into rendering
editor.hooks.beforeRender.tap((doc) => {
// Validate document before rendering
const errors = this.#validate(doc);
if (errors.length > 0) {
console.warn('Validation errors:', errors);
}
});
}
}고급 패턴
복잡한 시나리오를 위한 고급 훅 패턴:
// Conditional hook execution
class ConditionalHook extends SyncHook {
call(...args) {
this.#callbacks.forEach(callback => {
if (callback.condition(...args)) {
callback.fn(...args);
}
});
}
}
// Hook with priority
class PriorityHook extends SyncHook {
tap(fn, priority = 0) {
this.#callbacks.push({ fn, priority });
this.#callbacks.sort((a, b) => b.priority - a.priority);
}
call(...args) {
this.#callbacks.forEach(({ fn }) => {
fn(...args);
});
}
}
// Hook with context
class ContextHook extends SyncHook {
call(context, ...args) {
this.#callbacks.forEach(callback => {
callback.call(context, ...args);
});
}
}