이벤트 시스템 아키텍처

잘 설계된 이벤트 시스템은 플러그인이 에디터 상태 변경과 사용자 상호작용에 반응할 수 있게 합니다.

개요

이벤트 시스템은 에디터 코어와 플러그인 간의 통신 레이어입니다. 플러그인이 에디터 상태 변경, 사용자 상호작용, 생명주기 이벤트를 수신하고 반응할 수 있게 합니다.

잘 설계된 이벤트 시스템은 타입 안전성, 효율적인 이벤트 전파, 유연한 이벤트 처리 패턴을 제공합니다.

이벤트 버블링 및 캡처

버블링 구현

DOM 이벤트와 유사하게 이벤트 버블링을 구현하여 부모 핸들러가 자식보다 먼저 이벤트를 처리할 수 있게 합니다:

class EventEmitter {
  #listeners = new Map();
  
  on(event, handler, options) {
    if (!this.#listeners.has(event)) {
      this.#listeners.set(event, new Set());
    }
    this.#listeners.get(event).add(handler);
  }
  
  emit(event, data, bubbles = true) {
    const handlers = this.#listeners.get(event);
    if (handlers) {
      handlers.forEach(handler => {
        handler(data);
      });
    }
    
    // Bubble up to parent
    if (bubbles && this.#parent) {
      this.#parent.emit(event, data, true);
    }
  }
}

// Usage: Node-level events bubble to document
class DocumentNode {
  constructor(parent) {
    this.#parent = parent;
    this.#emitter = new EventEmitter();
  }
  
  emit(event, data) {
    // Emit on this node
    this.#emitter.emit(event, data, false);
    
    // Bubble to parent
    if (this.#parent) {
      this.#parent.emit(event, data);
    }
  }
}

// Handler on document receives all node events
document.on('textInsert', (data) => {
  console.log('Text inserted anywhere in document:', data);
});

캡처 단계

버블링 전에 처리되어야 하는 이벤트를 위한 캡처 단계를 지원합니다:

class EventEmitter {
  #captureListeners = new Map();
  #bubbleListeners = new Map();
  
  on(event, handler, options = {}) {
    const listeners = options.capture 
      ? this.#captureListeners 
      : this.#bubbleListeners;
    
    if (!listeners.has(event)) {
      listeners.set(event, new Set());
    }
    listeners.get(event).add(handler);
  }
  
  emit(event, data) {
    // Capture phase: parent to child
    this.#emitCapture(event, data);
    
    // Target phase: current node
    this.#emitTarget(event, data);
    
    // Bubble phase: child to parent
    this.#emitBubble(event, data);
  }
  
  #emitCapture(event, data) {
    // Emit to capture listeners
    const handlers = this.#captureListeners.get(event);
    if (handlers) {
      handlers.forEach(handler => handler(data));
    }
    
    // Continue to children
    this.#children.forEach(child => {
      child.#emitCapture(event, data);
    });
  }
  
  #emitBubble(event, data) {
    // Emit to bubble listeners
    const handlers = this.#bubbleListeners.get(event);
    if (handlers) {
      handlers.forEach(handler => handler(data));
    }
    
    // Continue to parent
    if (this.#parent) {
      this.#parent.#emitBubble(event, data);
    }
  }
}

이벤트 위임 패턴

위임의 이점

이벤트 위임은 메모리 사용을 줄이고 성능을 향상시킵니다:

  • 개별 노드에 많은 리스너 대신 컨테이너에 단일 리스너
  • 동적으로 추가된 노드에서도 작동
  • 이벤트 리스너 오버헤드 감소
  • 이벤트 관리 단순화

위임 구현

이벤트를 효율적으로 처리하기 위해 이벤트 위임을 사용합니다:

class Editor {
  constructor(container) {
    this.#container = container;
    
    // Single listener on container
    this.#container.addEventListener('click', (e) => {
      this.#handleClick(e);
    });
    
    this.#container.addEventListener('input', (e) => {
      this.#handleInput(e);
    });
  }
  
  #handleClick(e) {
    const target = e.target;
    
    // Find the model node for this DOM element
    const node = this.#findNodeForElement(target);
    if (!node) return;
    
    // Dispatch to appropriate handler based on node type
    switch (node.type) {
      case 'link':
        this.#handleLinkClick(node, e);
        break;
      case 'image':
        this.#handleImageClick(node, e);
        break;
    }
  }
  
  #handleInput(e) {
    // Convert DOM input to model operation
    const operation = this.#domToOperation(e);
    this.applyOperation(operation);
  }
  
  #findNodeForElement(element) {
    // Walk up DOM tree to find node marker
    let current = element;
    while (current && current !== this.#container) {
      const nodeId = current.getAttribute('data-node-id');
      if (nodeId) {
        return this.#getNodeById(nodeId);
      }
      current = current.parentElement;
    }
    return null;
  }
}

커스텀 이벤트 시스템

타입화된 이벤트

타입화된 이벤트로 커스텀 이벤트 시스템을 구현합니다:

// Define event types
const EditorEvents = {
  OPERATION: 'operation',
  SELECTION_CHANGE: 'selectionChange',
  DOCUMENT_CHANGE: 'documentChange',
  ERROR: 'error'
};

// Typed event emitter
class TypedEventEmitter {
  #listeners = new Map();
  
  on(event, handler) {
    if (!this.#listeners.has(event)) {
      this.#listeners.set(event, new Set());
    }
    this.#listeners.get(event).add(handler);
  }
  
  off(event, handler) {
    const handlers = this.#listeners.get(event);
    if (handlers) {
      handlers.delete(handler);
    }
  }
  
  emit(event, data) {
    const handlers = this.#listeners.get(event);
    if (handlers) {
      handlers.forEach(handler => handler(data));
    }
  }
}

// Usage
const editor = new TypedEventEmitter();

editor.on(EditorEvents.OPERATION, (data) => {
  console.log('Operation:', data.operation);
});

editor.on(EditorEvents.ERROR, (data) => {
  console.error('Error:', data.error);
});

editor.emit(EditorEvents.OPERATION, { 
  operation: { type: 'insertText', text: 'Hello' } 
});

이벤트 우선순위

순서가 있는 이벤트 처리를 위한 이벤트 우선순위를 지원합니다:

class PriorityEventEmitter {
  #listeners = new Map();
  
  on(event, handler, priority = 0) {
    if (!this.#listeners.has(event)) {
      this.#listeners.set(event, []);
    }
    
    const listeners = this.#listeners.get(event);
    listeners.push({ handler, priority });
    
    // Sort by priority (higher priority first)
    listeners.sort((a, b) => b.priority - a.priority);
  }
  
  emit(event, data) {
    const listeners = this.#listeners.get(event);
    if (listeners) {
      listeners.forEach(({ handler }) => {
        handler(data);
      });
    }
  }
}

// Usage
const emitter = new PriorityEventEmitter();

// High priority handler (runs first)
emitter.on('operation', (data) => {
  console.log('High priority:', data);
}, 100);

// Normal priority handler
emitter.on('operation', (data) => {
  console.log('Normal priority:', data);
}, 0);

// Low priority handler (runs last)
emitter.on('operation', (data) => {
  console.log('Low priority:', data);
}, -100);

이벤트 생명주기

완전한 이벤트 생명주기 관리:

class EventLifecycle {
  #preHandlers = new Map();
  #handlers = new Map();
  #postHandlers = new Map();
  
  on(event, handler, phase = 'main') {
    const map = phase === 'pre' 
      ? this.#preHandlers 
      : phase === 'post' 
        ? this.#postHandlers 
        : this.#handlers;
    
    if (!map.has(event)) {
      map.set(event, new Set());
    }
    map.get(event).add(handler);
  }
  
  emit(event, data) {
    // Pre-handlers
    this.#callHandlers(this.#preHandlers, event, data);
    
    // Main handlers
    const result = this.#callHandlers(this.#handlers, event, data);
    
    // Post-handlers
    this.#callHandlers(this.#postHandlers, event, data);
    
    return result;
  }
  
  #callHandlers(map, event, data) {
    const handlers = map.get(event);
    if (handlers) {
      handlers.forEach(handler => handler(data));
    }
  }
  
  // Prevent default behavior
  preventDefault(event) {
    event.defaultPrevented = true;
  }
  
  // Stop propagation
  stopPropagation(event) {
    event.propagationStopped = true;
  }
}