훅 시스템 구현

훅 시스템은 플러그인이 생명주기의 특정 지점에서 에디터 기능을 확장할 수 있게 합니다. 이 패턴은 webpack, Vue 및 기타 확장 가능한 시스템에서 사용됩니다.

개요

훅은 플러그인이 에디터의 생명주기에 연결하고 특정 지점에서 동작을 수정할 수 있는 방법을 제공합니다. 핵심 코드를 수정하지 않고 기능을 확장할 수 있는 플러그인 아키텍처를 가능하게 합니다.

훅 시스템은 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);
    });
  }
}