[Open WebUI] MessageInput 컴포넌트 메모리 누수 수정: 비동기 이벤트 리스너 생명주기 관리
PR 링크: open-webui/open-webui#21968 상태: Merged | 변경: +58 / -44
들어가며
Svelte에서 onMount가 async 함수일 때, 내부의 await 이후 코드는 비동기적으로 실행됩니다. 반면 onDestroy는 동기적으로 즉시 실행됩니다. 컴포넌트가 빠르게 마운트/언마운트되면, onDestroy가 먼저 호출된 후에 onMount의 await 이후 코드가 실행될 수 있습니다. 이 시점에서 등록된 이벤트 리스너는 해제 함수가 이미 호출된 후에 등록되므로, DOM 트리 전체가 메모리에 남아 페이지 크래시를 유발할 수 있습니다.
핵심 코드 분석
기존: onMount/onDestroy 분리 패턴
Before:
onMount(async () => {
// 동기 초기화...
window.addEventListener('keydown', handleKeyDown);
await tick();
const dropzoneElement = document.getElementById('chat-pane');
dropzoneElement?.addEventListener('dragover', onDragOver);
dropzoneElement?.addEventListener('drop', onDrop);
dropzoneElement?.addEventListener('dragleave', onDragLeave);
});
onDestroy(() => {
window.removeEventListener('keydown', handleKeyDown);
const dropzoneElement = document.getElementById('chat-pane');
if (dropzoneElement) {
dropzoneElement.removeEventListener('dragover', onDragOver);
dropzoneElement.removeEventListener('drop', onDrop);
dropzoneElement.removeEventListener('dragleave', onDragLeave);
}
});
변경: 클린업 반환 + isDestroyed 가드
After:
onMount(() => {
// 동기 초기화...
window.addEventListener('keydown', onKeyDown);
let isDestroyed = false;
let dropzoneElement: HTMLElement | null = null;
const initialize = async () => {
await tick();
if (isDestroyed) return; // 이미 파괴된 경우 리스너 등록 안 함
dropzoneElement = document.getElementById('chat-pane');
if (dropzoneElement) {
dropzoneElement.addEventListener('dragover', onDragOver);
dropzoneElement.addEventListener('drop', onDrop);
dropzoneElement.addEventListener('dragleave', onDragLeave);
}
};
initialize();
return () => {
isDestroyed = true;
window.removeEventListener('keydown', onKeyDown);
if (dropzoneElement) {
dropzoneElement.removeEventListener('dragover', onDragOver);
dropzoneElement.removeEventListener('drop', onDrop);
dropzoneElement.removeEventListener('dragleave', onDragLeave);
}
};
});
왜 이게 좋은가
- 레이스 컨디션 제거:
isDestroyed플래그로 컴포넌트 파괴 후 리스너 등록을 방지합니다. - 클로저로 참조 보장:
dropzoneElement를 클로저 변수로 캡처하여, 클린업 시 정확히 같은 요소에서 리스너를 해제합니다. 기존 방식은document.getElementById로 다시 조회하므로 DOM이 변경되면 다른 요소를 참조할 위험이 있었습니다. - Svelte 권장 패턴:
onMount에서 클린업 함수를 반환하는 것이onDestroy를 별도로 사용하는 것보다 안전합니다. - 두 컴포넌트 동시 수정:
chat/MessageInput.svelte와channel/MessageInput.svelte모두에 동일한 패턴이 적용되었습니다.
참고 자료
관련 포스트
PR Analysis 의 다른글
- 이전글 [Open WebUI] Tooltip 컴포넌트의 tippy 인스턴스 메모리 누수 수정 및 타입 정의 개선
- 현재글 : [Open WebUI] MessageInput 컴포넌트 메모리 누수 수정: 비동기 이벤트 리스너 생명주기 관리
- 다음글 [Open WebUI] JSON.parse(JSON.stringify()) 대신 structuredClone으로 딥 카피 최적화
댓글