fix: 타이머 클린업 및 메모리 누수 방지 개선

비디오 플레이어 관련 컴포넌트들의 타이머와 이벤트 리스너를 체계적으로 정리하여 메모리 누수 방지:

## ProductVideo.v2.jsx
- autoPlay 타이머 정리 강화 (dependency 최적화)
- 전체화면 전환 시 타이머 정리 명시
- Optional chaining으로 null 안정성 향상
- Document 이벤트 리스너 정리 명확화

## MediaPanel.jsx
- onEnded 타이머를 useRef로 추적 및 정리
- 컴포넌트 언마운트 시 전체 cleanup 함수 추가
- 비디오 플레이어 강제 정지로 리소스 누수 방지
- Modal 스타일 설정 시 ResizeObserver 정리 준비

## MediaPlayer.v2.jsx
- proportionLoaded 업데이트 타이머 최적화 (비디오 재생 중일 때만)
- 컴포넌트 언마운트 시 모든 타이머 및 상태 정리 강화
- Optional chaining으로 안정성 향상
- hideControls 메서드 타이머 정리 의도 명확화

🎯 효과:
- 장시간 비디오 재생 시 메모리 누수 방지
- 여러 번 반복 재생/정지 시 타이머 누적 방지
- 전체화면 전환 시 리소스 누수 방지
- 컴포넌트 언마운트 시 완전한 정리

📝 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-12 19:35:13 +09:00
parent ec4b7736f8
commit 660abbf691
4 changed files with 487 additions and 20 deletions

View File

@@ -50,6 +50,7 @@ const MediaPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
const dispatch = useDispatch();
const videoPlayer = useRef(null);
const onEndedTimerRef = useRef(null); // ✅ onEnded 타이머 관리
const [modalStyle, setModalStyle] = React.useState({});
const [modalScale, setModalScale] = React.useState(1);
const [currentTime, setCurrentTime] = React.useState(0);
@@ -110,8 +111,10 @@ const MediaPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
// MediaPanel에서는 indicator 사용 안 함
}, []);
// modal 스타일 설정
// modal 스타일 설정
useEffect(() => {
let resizeObserver = null;
if (panelInfo.modal && panelInfo.modalContainerId) {
// modal 모드: modalContainerId 기반으로 위치와 크기 계산
const node = document.querySelector(`[data-spotlight-id="${panelInfo.modalContainerId}"]`);
@@ -158,6 +161,13 @@ const MediaPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
videoPlayer.current.showControls();
}
}
// ✅ cleanup: resize observer 정리
return () => {
if (resizeObserver) {
resizeObserver.disconnect();
}
};
}, [panelInfo, isOnTop]);
// 비디오 클릭 시 modal → fullscreen 전환 또는 controls 토글
@@ -287,10 +297,19 @@ const MediaPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
// continuousPlay는 MediaPlayer(VideoPlayer) 컴포넌트 내부에서 loop 속성으로 처리
// onEnded가 호출되면 loop=false 인 경우이므로 패널을 닫음
Spotlight.pause();
setTimeout(() => {
// ✅ 이전 타이머가 있으면 정리
if (onEndedTimerRef.current) {
clearTimeout(onEndedTimerRef.current);
}
// ✅ 새로운 타이머 저장 (cleanup 시 정리용)
onEndedTimerRef.current = setTimeout(() => {
Spotlight.resume();
dispatch(PanelActions.popPanel(panel_names.MEDIA_PANEL));
onEndedTimerRef.current = null;
}, 1500);
e?.stopPropagation();
e?.preventDefault();
},
@@ -309,6 +328,26 @@ const MediaPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
setVideoLoaded(false);
}, [currentPlayingUrl]);
// ✅ 컴포넌트 언마운트 시 모든 타이머 정리
useEffect(() => {
return () => {
// onEnded 타이머 정리
if (onEndedTimerRef.current) {
clearTimeout(onEndedTimerRef.current);
onEndedTimerRef.current = null;
}
// ✅ 비디오 플레이어 정지
if (videoPlayer.current) {
try {
videoPlayer.current.pause?.();
} catch (error) {
console.warn('[MediaPanel] 비디오 정지 실패:', error);
}
}
};
}, []);
// console.log('[MediaPanel] ========== Rendering ==========');
// console.log('[MediaPanel] isOnTop:', isOnTop);
// console.log('[MediaPanel] panelInfo.modal:', panelInfo.modal);