본문으로 건너뛰기

[Open WebUI] MentionList 컴포넌트 메모리 누수 수정

PR 링크: open-webui/open-webui#21965 상태: Merged | 변경: +13 / -12

들어가며

Open WebUI의 MentionList 컴포넌트에서 onMountasync로 선언되어 있고, 이벤트 리스너 해제는 별도의 onDestroy에서 수행되고 있었다. 비동기 onMount 내에서 이벤트 리스너를 등록하면, onDestroy가 먼저 호출된 후 비동기 onMount가 실행되는 경우가 발생할 수 있다. 이때 등록된 리스너는 영원히 해제되지 않아 전체 컴포넌트 DOM 트리가 메모리에 유지되는 심각한 누수가 발생했다.

핵심 코드 분석

Before: async onMount + 별도 onDestroy

const keydownListener = (e) => {
    if (e.key === 'Enter') {
        e.preventDefault();
        select(selectedIndex);
    }
};

onMount(async () => {
    window.addEventListener('keydown', keydownListener);
    // ...
    if (userSuggestions) {
        await getUserList();
    }
    // ...
});

onDestroy(() => {
    window.removeEventListener('keydown', keydownListener);
});

After: 동기 onMount + cleanup 반환

onMount(() => {
    const keydownListener = (e: KeyboardEvent) => {
        if (e.key === 'Enter') {
            e.preventDefault();
            select(selectedIndex);
        }
    };

    window.addEventListener('keydown', keydownListener);

    if (userSuggestions) {
        getUserList();  // await 제거
    }

    if (modelSuggestions) {
        _models = [...$models.map((m) => ({ type: 'model', id: m.id, label: m.name, data: m }))];
    }

    return () => {
        window.removeEventListener('keydown', keydownListener);
    };
});

왜 이게 좋은가

  1. 타이밍 문제 해결: onMount를 동기로 변경하여 onDestroy보다 먼저 실행되지 않는 경우를 원천 차단한다.
  2. 클로저 활용: keydownListeneronMount 내부에서 정의하여 cleanup 함수에서 정확히 같은 참조를 제거할 수 있다.
  3. Svelte 라이프사이클 패턴: onMount의 반환 함수를 cleanup으로 사용하면 onDestroy를 별도로 관리할 필요가 없어 코드가 단순해진다.
  4. getUserList의 await 제거: getUserList()를 fire-and-forget으로 변경하여 onMount 자체의 비동기 문제를 해결했다.

참고 자료

댓글

관련 포스트

PR Analysis 의 다른글