Skip to main content
Reference status

This page predates the current package split. Use the current package guides for checked installation, public imports, and onboarding examples. The detailed examples below have not all been revalidated.

Before Hooks 사용 사례 분석

개요​

Extension의 onBeforeTransaction, onBeforeSelectionChange 같은 Before hooks를 통해 transaction/selection/content 변경을 가로채서 수정하거나 취소할 수 있는 실제 사용 사례들을 정리합니다.


1. Read-Only 모드 강제​

문제​

  • 현재 editorEditable context로 keybinding만 비활성화됨
  • 하지만 editor.executeCommand()로 직접 호출하면 우회 가능
  • Transaction 자체를 막을 수 없음

해결: onBeforeTransaction​

class ReadOnlyExtension implements Extension {
onCreate(editor: Editor) {
editor.setContext('readOnly', true);
}

onBeforeTransaction(editor: Editor, transaction: Transaction): Transaction | null {
// Read-only 모드면 모든 transaction 취소
if (editor.getContext('readOnly')) {
return null; // 취소
}
return transaction; // 그대로 진행
}
}

효과​

  • Command 우회 불가능
  • 모든 transaction 차단
  • 일관된 read-only 보장

2. Content Sanitization (XSS 방지)​

문제​

  • 외부에서 paste된 HTML에 <script> 태그 포함 가능
  • 사용자 입력에 악성 코드 포함 가능

해결: onBeforeTransaction​

class SanitizeExtension implements Extension {
onBeforeTransaction(editor: Editor, transaction: Transaction): Transaction | null {
const ops = transaction.getOperations();

// insertText operation에서 악성 패턴 검사
for (const op of ops) {
if (op.type === 'insertText') {
const sanitized = this.sanitizeText(op.text);
if (sanitized !== op.text) {
// 수정된 operation으로 교체
return transaction.withOperation(
op.id,
{ ...op, text: sanitized }
);
}
}
}

return transaction;
}

private sanitizeText(text: string): string {
// <script> 태그 제거, HTML 엔티티 이스케이프 등
return text.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
}
}

효과​

  • 모든 텍스트 입력 자동 검사
  • 악성 코드 자동 제거
  • 안전한 content 보장

3. Auto-Formatting (자동 포맷팅)​

문제​

  • 사용자가 URL을 입력하면 자동으로 링크로 변환
  • 날짜 입력 시 자동 포맷팅
  • 전화번호 자동 포맷팅

해결: onBeforeTransaction​

class AutoFormatExtension implements Extension {
onBeforeTransaction(editor: Editor, transaction: Transaction): Transaction | null {
const ops = transaction.getOperations();
const newOps: Operation[] = [];

for (const op of ops) {
if (op.type === 'insertText') {
// URL 패턴 감지
const urlMatch = op.text.match(/https?:\/\/[^\s]+/);
if (urlMatch) {
// 텍스트 삽입 + 링크 마크 추가로 변환
newOps.push(op);
newOps.push({
type: 'addMark',
nodeId: op.nodeId,
mark: { type: 'link', attributes: { href: urlMatch[0] } },
range: [op.offset, op.offset + urlMatch[0].length]
});
continue;
}
}
newOps.push(op);
}

// operation이 변경되었으면 새 transaction 반환
if (newOps.length !== ops.length) {
return transaction.withOperations(newOps);
}

return transaction;
}
}

효과​

  • 사용자 입력 자동 변환
  • 일관된 포맷팅
  • 사용자 경험 향상

4. Change Tracking (변경 추적)​

문제​

  • 누가 무엇을 언제 변경했는지 추적 필요
  • 변경 이력에 메타데이터 추가 필요

해결: onBeforeTransaction​

class ChangeTrackingExtension implements Extension {
private userId: string;

onBeforeTransaction(editor: Editor, transaction: Transaction): Transaction | null {
// Transaction에 메타데이터 추가
return transaction.withMetadata({
userId: this.userId,
timestamp: Date.now(),
changeType: this.detectChangeType(transaction)
});
}

private detectChangeType(transaction: Transaction): string {
const ops = transaction.getOperations();
if (ops.some(op => op.type === 'insertText')) return 'insert';
if (ops.some(op => op.type === 'deleteText')) return 'delete';
if (ops.some(op => op.type === 'updateNode')) return 'format';
return 'unknown';
}
}

효과​

  • 모든 변경에 메타데이터 자동 추가
  • 변경 이력 추적 가능
  • 협업 편집 지원

5. Collaborative Editing Protection​

문제​

  • 사용자가 입력 중인 노드를 외부 변경으로부터 보호 필요
  • AI나 다른 사용자의 변경이 사용자 입력을 방해

해결: onBeforeTransaction​

class CollaborativeProtectionExtension implements Extension {
private activeTextNodeId: string | null = null;

onCreate(editor: Editor) {
// 사용자 입력 시작 감지
editor.on('editor:content.change', () => {
this.activeTextNodeId = editor.selection?.startNodeId || null;
});
}

onBeforeTransaction(editor: Editor, transaction: Transaction): Transaction | null {
// 외부 변경인지 확인 (사용자 입력이 아닌 경우)
const isExternalChange = !transaction.metadata?.isUserInput;

if (isExternalChange && this.activeTextNodeId) {
// 활성 노드에 대한 변경인지 확인
const ops = transaction.getOperations();
const affectsActiveNode = ops.some(op =>
op.nodeId === this.activeTextNodeId
);

if (affectsActiveNode) {
// 사용자 입력 중인 노드는 보호
console.warn('[CollaborativeProtection] Blocked external change to active node');
return null; // 취소
}
}

return transaction;
}
}

효과​

  • 사용자 입력 보호
  • 외부 변경으로부터 안전
  • 협업 편집 안정성 향상

6. Selection Normalization (선택 영역 정규화)​

문제​

  • Selection이 특정 규칙을 위반 (예: 블록 노드 내부만 선택 가능)
  • Selection이 비정상적인 위치 (예: 데코레이터 영역)

해결: onBeforeSelectionChange​

class SelectionNormalizeExtension implements Extension {
onBeforeSelectionChange(
editor: Editor,
selection: ModelSelection
): ModelSelection | null {
// Selection이 블록 노드 경계를 넘지 않도록 정규화
const normalized = this.normalizeSelection(editor, selection);

if (normalized.startNodeId !== selection.startNodeId ||
normalized.startOffset !== selection.startOffset) {
// Selection이 변경되었으면 정규화된 selection 반환
return normalized;
}

return selection; // 그대로 진행
}

private normalizeSelection(
editor: Editor,
selection: ModelSelection
): ModelSelection {
// 블록 노드 경계 확인 및 조정 로직
// ...
return selection;
}
}

효과​

  • 일관된 selection 보장
  • 비정상적인 selection 방지
  • 사용자 경험 향상

7. Transaction Filtering (트랜잭션 필터링)​

문제​

  • 특정 조건의 transaction만 허용
  • 예: 특정 노드 타입만 수정 가능

해결: onBeforeTransaction​

class TransactionFilterExtension implements Extension {
private allowedNodeTypes: string[] = ['paragraph', 'heading'];

onBeforeTransaction(editor: Editor, transaction: Transaction): Transaction | null {
const ops = transaction.getOperations();

// 허용되지 않은 노드 타입 수정 시도 차단
for (const op of ops) {
if (op.type === 'updateNode' || op.type === 'createNode') {
const nodeType = op.stype || op.attributes?.stype;
if (nodeType && !this.allowedNodeTypes.includes(nodeType)) {
console.warn(`[TransactionFilter] Blocked operation on ${nodeType}`);
return null; // 취소
}
}
}

return transaction;
}
}

효과​

  • 세밀한 권한 제어
  • 특정 노드 타입만 수정 가능
  • 보안 강화

8. Content Validation (컨텐츠 검증)​

문제​

  • Schema 검증을 통과했지만 비즈니스 로직 검증 필요
  • 예: 최대 글자 수, 특정 패턴 필수

해결: onBeforeTransaction​

class ContentValidationExtension implements Extension {
private maxLength: number = 1000;

onBeforeTransaction(editor: Editor, transaction: Transaction): Transaction | null {
const ops = transaction.getOperations();

for (const op of ops) {
if (op.type === 'insertText') {
const node = editor.dataStore.getNode(op.nodeId);
const currentLength = node?.text?.length || 0;
const newLength = currentLength + op.text.length;

if (newLength > this.maxLength) {
// 최대 길이 초과 시 취소
console.warn(`[ContentValidation] Text too long: ${newLength} > ${this.maxLength}`);
return null;
}
}
}

return transaction;
}
}

효과​

  • Schema 검증 이후 추가 검증
  • 비즈니스 로직 강제
  • 데이터 무결성 보장

9. Auto-Correction (자동 수정)​

문제​

  • 오타 자동 수정
  • 대소문자 자동 수정

해결: onBeforeTransaction​

class AutoCorrectExtension implements Extension {
private corrections: Map<string, string> = new Map([
['teh', 'the'],
['adn', 'and'],
// ...
]);

onBeforeTransaction(editor: Editor, transaction: Transaction): Transaction | null {
const ops = transaction.getOperations();
const newOps: Operation[] = [];

for (const op of ops) {
if (op.type === 'insertText') {
// 오타 수정
let correctedText = op.text;
for (const [wrong, correct] of this.corrections) {
correctedText = correctedText.replace(wrong, correct);
}

if (correctedText !== op.text) {
// 수정된 텍스트로 교체
newOps.push({ ...op, text: correctedText });
continue;
}
}
newOps.push(op);
}

if (newOps.length !== ops.length ||
newOps.some((op, i) => op !== ops[i])) {
return transaction.withOperations(newOps);
}

return transaction;
}
}

효과​

  • 실시간 오타 수정
  • 사용자 경험 향상
  • 일관된 텍스트 품질

10. Transaction Batching (트랜잭션 배칭)​

문제​

  • 빠른 연속 입력 시 transaction이 너무 많음
  • 성능 저하 및 이벤트 폭주

해결: onBeforeTransaction​

class TransactionBatchingExtension implements Extension {
private pendingOps: Operation[] = [];
private batchTimeout: number | null = null;

onBeforeTransaction(editor: Editor, transaction: Transaction): Transaction | null {
const ops = transaction.getOperations();

// 배치에 추가
this.pendingOps.push(...ops);

// 타임아웃 설정 (100ms 내 추가 transaction이 없으면 배치 실행)
if (this.batchTimeout) {
clearTimeout(this.batchTimeout);
}

this.batchTimeout = window.setTimeout(() => {
// 배치 실행
const batchedTransaction = transaction.withOperations(this.pendingOps);
this.pendingOps = [];
this.batchTimeout = null;

// 실제 transaction 실행 (재귀 방지를 위해 플래그 설정)
editor.executeTransaction(batchedTransaction);
}, 100);

// 현재 transaction은 취소 (배치에서 처리)
return null;
}
}

효과​

  • Transaction 수 감소
  • 성능 향상
  • 이벤트 폭주 방지

구현 방식 요약​

Before Hooks 시그니처​

interface Extension {
// Transaction 가로채기
onBeforeTransaction?(
editor: Editor,
transaction: Transaction
): Transaction | null | void;
// - Transaction 반환: 수정된 transaction 사용
// - null 반환: transaction 취소
// - void: 그대로 진행

// Selection 가로채기
onBeforeSelectionChange?(
editor: Editor,
selection: ModelSelection
): ModelSelection | null | void;
// - Selection 반환: 다른 selection으로 교체
// - null 반환: selection 변경 취소
// - void: 그대로 진행

// Content 가로채기
onBeforeContentChange?(
editor: Editor,
content: DocumentState
): DocumentState | null | void;
// - Content 반환: 다른 content로 교체
// - null 반환: content 변경 취소
// - void: 그대로 진행
}

실행 순서​

  1. Extension들을 priority 순으로 정렬
  2. 각 Extension의 onBeforeTransaction 호출
  3. 반환값이 null이면 즉시 취소
  4. 반환값이 Transaction이면 수정된 transaction 사용
  5. 모든 Extension 통과 후 실제 transaction 실행

결론​

Before hooks는 다음과 같은 상황에서 유용합니다:

  1. 보안: Read-only 강제, Content sanitization
  2. 자동화: Auto-formatting, Auto-correction
  3. 보호: Collaborative editing protection
  4. 검증: Content validation, Transaction filtering
  5. 최적화: Transaction batching
  6. 추적: Change tracking

이러한 기능들은 단순 이벤트 리스너로는 불가능하며, transaction/selection/content를 가로채서 수정하거나 취소할 수 있어야 구현 가능합니다.