본문으로 건너뛰기

[Open WebUI] PanZoom 인스턴스 메모리 누수를 PanzoomContainer 컴포넌트로 통합 해결

PR 링크: open-webui/open-webui#23236 상태: Merged | 변경: +72 / -109

들어가며

Open WebUI에서 이미지 프리뷰, SVG, 오피스 슬라이드 등 여러 곳에서 panzoom 라이브러리를 사용합니다. 각 컴포넌트가 독립적으로 panzoom 인스턴스를 생성하고 onDestroy에서 dispose()를 호출하는 패턴이 반복되었는데, 일부 컴포넌트에서 dispose가 누락되거나 reactive 블록($:)에서 인스턴스를 재생성하면서 이전 인스턴스가 정리되지 않는 메모리 누수가 발생했습니다.

핵심 코드 분석

Before: 각 컴포넌트에서 개별 관리

<!-- ImagePreview.svelte -->
<script>
  let instance: PanZoom;
  $: if (sceneElement) {
    // reactive 블록에서 매번 새 인스턴스 생성 - 이전 것은 dispose 안 됨!
    instance = panzoom(sceneElement, { bounds: true, ... });
  }
  // onDestroy가 없거나 불완전
</script>
<div bind:this={sceneElement}>
  <img {src} {alt} />
</div>

After: PanzoomContainer로 통합

<!-- PanzoomContainer.svelte -->
<script>
  import { onMount } from 'svelte';
  import panzoom, { type PanZoom, type PanZoomOptions } from 'panzoom';

  let containerElement: HTMLElement;
  let instance: PanZoom | undefined;

  export const reset = () => {
    instance?.moveTo(0, 0);
    instance?.zoomAbs(0, 0, 1);
  };

  onMount(() => {
    const localInstance = panzoom(containerElement, { ...defaultOpts, ...options });
    instance = localInstance;
    return () => { localInstance.dispose(); };  // cleanup 보장
  });
</script>
<div bind:this={containerElement} class={className}>
  <slot />
</div>

<!-- 사용하는 쪽 -->
<PanzoomContainer bind:this={panzoomRef} className="...">
  <img {src} {alt} />
</PanzoomContainer>

왜 이게 좋은가

  1. dispose 보장: onMount의 반환 함수로 cleanup을 처리하므로 컴포넌트 해제 시 panzoom 인스턴스가 반드시 정리된다.
  2. 코드 중복 제거: FilePreview, FileItemModal, ImagePreview, SVGPanZoom 4개 컴포넌트의 반복 로직을 하나로 통합했다.
  3. reactive 블록 재생성 문제 해결: $: 블록 대신 onMount를 사용하여 인스턴스 중복 생성이 원천적으로 불가능하다.

참고 자료

댓글

관련 포스트

PR Analysis 의 다른글