fix(music): setTimeout 정리 + useCallback 폴링 deps

- TrendsTab: useRef로 타이머 ID 추적 후 언마운트 시 clearTimeout 호출 (stale setState 방지)
- VideoProjectsTab: loadProjects를 useCallback으로 감싸고 폴링 useEffect deps에 추가

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-01 15:13:04 +09:00
parent 08981a292a
commit 8a7b5e8a38
2 changed files with 19 additions and 7 deletions

View File

@@ -1,5 +1,5 @@
// src/pages/music/components/TrendsTab.jsx // src/pages/music/components/TrendsTab.jsx
import { useState, useEffect } from 'react'; import { useState, useEffect, useRef } from 'react';
import { import {
getLatestTrendReport, getTrendReports, getLatestTrendReport, getTrendReports,
getMarketSuggestions, triggerYoutubeResearch, getMarketSuggestions, triggerYoutubeResearch,
@@ -28,6 +28,9 @@ export default function TrendsTab() {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [researchMsg, setResearchMsg] = useState(''); const [researchMsg, setResearchMsg] = useState('');
const researchTimerRef = useRef(null);
const copyTimerRef = useRef(null);
const loadAll = async () => { const loadAll = async () => {
setLoading(true); setLoading(true);
try { try {
@@ -48,12 +51,20 @@ export default function TrendsTab() {
useEffect(() => { loadAll(); }, []); useEffect(() => { loadAll(); }, []);
useEffect(() => {
return () => {
clearTimeout(researchTimerRef.current);
clearTimeout(copyTimerRef.current);
};
}, []);
const handleResearch = async () => { const handleResearch = async () => {
setResearching(true); setResearching(true);
try { try {
await triggerYoutubeResearch(); await triggerYoutubeResearch();
setResearchMsg('수집이 시작되었습니다. 잠시 후 새로고침하세요.'); setResearchMsg('수집이 시작되었습니다. 잠시 후 새로고침하세요.');
setTimeout(() => setResearchMsg(''), 4000); clearTimeout(researchTimerRef.current);
researchTimerRef.current = setTimeout(() => setResearchMsg(''), 4000);
} catch (e) { } catch (e) {
console.error('triggerYoutubeResearch:', e); console.error('triggerYoutubeResearch:', e);
} finally { } finally {
@@ -64,7 +75,8 @@ export default function TrendsTab() {
const handleCopy = (text, idx) => { const handleCopy = (text, idx) => {
navigator.clipboard.writeText(text).then(() => { navigator.clipboard.writeText(text).then(() => {
setCopiedIdx(idx); setCopiedIdx(idx);
setTimeout(() => setCopiedIdx(null), 2000); clearTimeout(copyTimerRef.current);
copyTimerRef.current = setTimeout(() => setCopiedIdx(null), 2000);
}); });
}; };

View File

@@ -1,5 +1,5 @@
// src/pages/music/components/VideoProjectsTab.jsx // src/pages/music/components/VideoProjectsTab.jsx
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef, useCallback } from 'react';
import { import {
createVideoProject, getVideoProjects, createVideoProject, getVideoProjects,
renderVideoProject, exportVideoProject, deleteVideoProject, renderVideoProject, exportVideoProject, deleteVideoProject,
@@ -26,14 +26,14 @@ export default function VideoProjectsTab({ library, initialTrackId, onClearIniti
} }
}, [initialTrackId]); }, [initialTrackId]);
const loadProjects = async () => { const loadProjects = useCallback(async () => {
try { try {
const data = await getVideoProjects(); const data = await getVideoProjects();
setProjects(Array.isArray(data) ? data : data.projects ?? []); setProjects(Array.isArray(data) ? data : data.projects ?? []);
} catch (e) { } catch (e) {
console.error('getVideoProjects:', e); console.error('getVideoProjects:', e);
} }
}; }, []);
useEffect(() => { loadProjects(); }, []); useEffect(() => { loadProjects(); }, []);
@@ -49,7 +49,7 @@ export default function VideoProjectsTab({ library, initialTrackId, onClearIniti
return () => { return () => {
if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; } if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; }
}; };
}, [projects]); }, [projects, loadProjects]);
const toggleCountry = (c) => { const toggleCountry = (c) => {
setCountries(prev => setCountries(prev =>