본문으로 건너뛰기

[Open WebUI] CodeEditor에서 EditorView 미해제로 인한 메모리 누수 수정

PR 링크: open-webui/open-webui#22110 상태: Merged | 변경: +36 / -28

들어가며

Open WebUI의 CodeEditor 컴포넌트에서 CodeMirror의 EditorView 인스턴스가 컴포넌트 해제 시 destroy()되지 않아 메모리 누수가 발생하고 있었습니다. EditorView는 내부적으로 DOMObserver를 유지하고 DOM 참조를 보유하므로, 명시적으로 해제하지 않으면 가비지 컬렉터가 수거할 수 없습니다. 또한 매 컴포넌트 인스턴스마다 languages 배열에 HCL, Elixir, Matlab 언어를 중복 등록하는 문제도 있었습니다.

핵심 코드 분석

Before: EditorView 미해제 + 인스턴스별 언어 등록

<script>
  let codeEditor;

  // 매 컴포넌트 인스턴스마다 languages 배열에 중복 추가
  languages.push(LanguageDescription.of({
    name: 'HCL', extensions: ['hcl', 'tf'],
    load() { return import('codemirror-lang-hcl').then((m) => m.hcl()); }
  }));

  onMount(() => {
    return () => {
      observer.disconnect();
      document.removeEventListener('keydown', keydownHandler);
      // EditorView.destroy() 호출 없음 - 메모리 누수!
    };
  });
</script>

After: EditorView destroy + 모듈 수준 언어 등록

// src/lib/utils/codemirror.ts - 모듈 수준에서 한 번만 실행
import { LanguageDescription } from '@codemirror/language';
import { languages } from '@codemirror/language-data';

languages.push(
  LanguageDescription.of({ name: 'HCL', ... })
);
languages.push(
  LanguageDescription.of({ name: 'Elixir', ... })
);
<script>
  import '$lib/utils/codemirror';

  let codeEditor: EditorView | null = null;

  onMount(() => {
    return () => {
      observer.disconnect();
      document.removeEventListener('keydown', keydownHandler);
      if (codeEditor) {
        codeEditor.destroy();
        codeEditor = null;
      }
    };
  });
</script>

왜 이게 좋은가

  • 메모리 누수 수정: EditorView.destroy()는 내부 DOMObserver를 해제하고 DOM 참조를 정리합니다. 채팅 메시지를 탐색하며 여러 코드 에디터를 마운트/언마운트할 때 메모리가 지속적으로 증가하던 문제가 해결됩니다.
  • 중복 등록 방지: 언어 등록을 별도 모듈(codemirror.ts)로 분리하여 ES 모듈 시스템이 자연스럽게 한 번만 실행되도록 합니다.
  • 타입 안전성: codeEditorEditorView | null 타입을 명시하고 옵셔널 체이닝(?.)을 사용합니다.

참고 자료

댓글

관련 포스트

PR Analysis 의 다른글