본문으로 건너뛰기

[Open WebUI] ChatItem 사이드바 메모리 누수 수정

PR 링크: open-webui/open-webui#23209 상태: Merged | 변경: +21 / -24

들어가며

Open WebUI의 사이드바에서 각 채팅 항목(ChatItem)마다 이벤트 리스너 정리가 제대로 되지 않아 메모리 누수가 발생하고 있었습니다. 또한 각 ChatItem 인스턴스마다 1x1 투명 드래그 이미지(Image 객체)를 개별 생성하고 있어 사이드바에 채팅이 많으면 불필요한 메모리를 사용했습니다.

핵심 코드 분석

Before: onMount/onDestroy 분리 + 개별 드래그 이미지

<script lang="ts">
    const dragImage = new Image();
    dragImage.src = 'data:image/png;base64,...';

    onMount(() => {
        if (itemElement) {
            document.addEventListener('click', onClickOutside, true);
            itemElement.addEventListener('dragstart', onDragStart);
            itemElement.addEventListener('drag', onDrag);
            itemElement.addEventListener('dragend', onDragEndHandler);
        }
    });

    onDestroy(() => {
        if (itemElement) {
            document.removeEventListener('click', onClickOutside, true);
            itemElement.removeEventListener('dragstart', onDragStart);
            // ...
        }
    });
</script>

두 가지 문제가 있습니다:

  1. onMount에서 등록한 리스너를 onDestroy에서 해제하지만, itemElement 참조가 달라질 수 있어 해제가 누락될 수 있습니다.
  2. 각 ChatItem 인스턴스마다 new Image()를 생성합니다.

After: onMount 반환 함수로 정리 + 모듈 레벨 공유 이미지

<script context="module" lang="ts">
    /** Shared 1x1 transparent drag preview */
    const invisibleDragImage = new Image();
    invisibleDragImage.src = 'data:image/png;base64,...';
</script>

<script lang="ts">
    onMount(() => {
        const el = itemElement;
        if (!el) return;

        document.addEventListener('click', onClickOutside, true);
        el.addEventListener('dragstart', onDragStart);
        el.addEventListener('drag', onDrag);
        el.addEventListener('dragend', onDragEndHandler);

        return () => {
            document.removeEventListener('click', onClickOutside, true);
            el.removeEventListener('dragstart', onDragStart);
            el.removeEventListener('drag', onDrag);
            el.removeEventListener('dragend', onDragEndHandler);
        };
    });
</script>

핵심 변경 사항:

  1. onMount에서 el을 로컬 변수로 캡처하고, 반환 함수에서 같은 참조로 리스너를 해제합니다.
  2. 드래그 이미지를 context="module" 블록으로 이동하여 모든 ChatItem 인스턴스가 하나의 Image를 공유합니다.

왜 이게 좋은가

  1. 메모리 누수 완전 해결: onMount의 반환 함수는 Svelte가 컴포넌트 파괴 시 자동으로 호출하므로, 등록과 해제의 대칭이 보장됩니다. 클로저가 동일한 el 참조를 캡처하므로 DOM 엘리먼트 불일치 문제도 없습니다.
  2. 메모리 사용량 감소: 사이드바에 100개의 채팅이 있으면 100개의 Image 객체 대신 1개만 생성됩니다.
  3. 코드 단순화: onDestroy import가 제거되고, 등록/해제 로직이 한 곳에 모여 유지보수가 쉬워집니다.

Svelte에서 onMount의 반환 함수를 정리(cleanup)에 사용하는 것은 React의 useEffect 정리 패턴과 유사하며, 컴포넌트 라이프사이클 관리의 모범 사례입니다.

참고 자료

댓글

관련 포스트

PR Analysis 의 다른글