[Open WebUI] Chat.svelte 비동기 onMount 메모리 누수 수정
PR 링크: open-webui/open-webui#21962 상태: Merged | 변경: +69 / -66
들어가며
Open WebUI의 Chat.svelte에서 심각한 메모리 누수가 발생하고 있었습니다. 원인은 비동기 onMount 함수와 동기 onDestroy 함수 간의 타이밍 불일치입니다. 대화를 전환할 때 onDestroy가 먼저 호출되고, 이후 비동기 onMount의 이벤트 리스너 등록이 실행되면 더 이상 해제할 방법이 없어져 전체 DOM 트리가 메모리에 잔류하는 문제였습니다.
핵심 코드 분석
Before: 비동기 onMount + 별도 onDestroy
let pageSubscribe = null;
let showControlsSubscribe = null;
onMount(async () => {
window.addEventListener('message', onMessageHandler);
$socket?.on('events', chatEventHandler);
audioQueue.set(new AudioQueue(document.getElementById('audioElement')));
pageSubscribe = page.subscribe(async (p) => { /* ... */ });
showControlsSubscribe = showControls.subscribe(async (value) => { /* ... */ });
// ... 비동기 초기화 작업 ...
});
onDestroy(() => {
try {
pageSubscribe();
showControlsSubscribe();
window.removeEventListener('message', onMessageHandler);
$socket?.off('events', chatEventHandler);
$audioQueue?.destroy();
} catch (e) { console.error(e); }
});
문제: onMount가 async이므로 내부 코드가 비동기로 실행됩니다. 대화를 빠르게 전환하면 onDestroy가 onMount보다 먼저 실행될 수 있습니다. 이 경우 pageSubscribe는 아직 null이므로 구독 해제가 되지 않고, 이후 onMount에서 등록된 리스너도 영원히 해제되지 않습니다.
After: 동기 onMount + 반환 함수로 정리
onMount(() => {
window.addEventListener('message', onMessageHandler);
$socket?.on('events', chatEventHandler);
const audioQueueInstance = new AudioQueue(document.getElementById('audioElement'));
audioQueue.set(audioQueueInstance);
const pageSubscribe = page.subscribe(async (p) => { /* ... */ });
const showControlsSubscribe = showControls.subscribe(async (value) => { /* ... */ });
const selectedFolderSubscribe = selectedFolder.subscribe(async (folder) => { /* ... */ });
const init = async () => {
// 비동기 초기화 작업을 별도 함수로 분리
};
init();
return () => {
try {
pageSubscribe();
showControlsSubscribe();
selectedFolderSubscribe();
window.removeEventListener('message', onMessageHandler);
$socket?.off('events', chatEventHandler);
audioQueueInstance?.destroy();
audioQueue.set(null);
} catch (e) { console.error(e); }
};
});
핵심 변경:
onMount를 동기 함수로 변경하고, 비동기 로직은init()함수로 분리- 모든 구독/리스너를 로컬 변수로 캡처하여 반환 함수에서 해제
audioQueueInstance를 로컬 변수로 캡처하여 정확한 인스턴스를 파괴
왜 이게 좋은가
- 메모리 누수 완전 해결: 동기
onMount이므로 이벤트 리스너와 구독이 즉시 등록되고, 반환 함수가 정리를 보장합니다. 비동기/동기 타이밍 불일치가 사라집니다. - AudioQueue 누수 방지: 기존에는
$audioQueue(스토어 값)를 통해 파괴했는데, 스토어 값이 이미 변경된 상태에서 파괴하면 이전 인스턴스가 누수됩니다. 로컬 변수로 캡처하여 정확한 인스턴스를 파괴합니다. - Svelte 라이프사이클 패턴 준수:
onMount의 반환 함수는 Svelte가 자동으로 컴포넌트 파괴 시 호출하므로, 등록/해제의 대칭이 언어 차원에서 보장됩니다.
대화가 길어지면 페이지 크래시를 유발하던 심각한 버그를 해결한 중요한 PR입니다.
참고 자료
관련 포스트
PR Analysis 의 다른글
- 이전글 [Open WebUI] 사용자 메모리 컬렉션 쿼리에 소유권 검증 추가
- 현재글 : [Open WebUI] Chat.svelte 비동기 onMount 메모리 누수 수정
- 다음글 [Ray Serve] Controller 마이크로벤치마크 공식 추가
댓글