perf(schedule): 카드 onClick 안정화로 React.memo 복구

카드가 onClick(schedule)을 호출하도록 변경(기존 호출부는 인자 무시라
호환), 페이지는 useCallback 안정 핸들러를 전달. 매 렌더 새 인라인
함수로 memo가 깨져 필터/스크롤마다 전체 카드가 리렌더되던 문제 해결.
검색 결과 가상화 카드는 범위에서 제외(이미 최적화됨).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
caadiq 2026-06-07 16:19:33 +09:00
parent 067618d792
commit 1b330872f5
8 changed files with 23 additions and 18 deletions

View file

@ -67,7 +67,7 @@ const BirthdayCard = memo(function BirthdayCard({ schedule, showYear = false, de
initial={{ opacity: 0, x: -10 }} initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }} animate={{ opacity: 1, x: 0 }}
transition={{ delay, type: 'spring', stiffness: 300, damping: 30 }} transition={{ delay, type: 'spring', stiffness: 300, damping: 30 }}
onClick={onClick} onClick={() => onClick?.(schedule)}
className="cursor-pointer" className="cursor-pointer"
> >
{CardContent} {CardContent}
@ -76,7 +76,7 @@ const BirthdayCard = memo(function BirthdayCard({ schedule, showYear = false, de
} }
return ( return (
<div onClick={onClick} className="cursor-pointer"> <div onClick={() => onClick?.(schedule)} className="cursor-pointer">
{CardContent} {CardContent}
</div> </div>
); );

View file

@ -24,7 +24,7 @@ const ScheduleListCard = memo(function ScheduleListCard({
initial={{ opacity: 0, x: -10 }} initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }} animate={{ opacity: 1, x: 0 }}
transition={{ delay, type: 'spring', stiffness: 300, damping: 30 }} transition={{ delay, type: 'spring', stiffness: 300, damping: 30 }}
onClick={onClick} onClick={() => onClick?.(schedule)}
className={`cursor-pointer ${className}`} className={`cursor-pointer ${className}`}
> >
{/* 카드 본체 (플랫 테두리) */} {/* 카드 본체 (플랫 테두리) */}

View file

@ -26,7 +26,7 @@ const UndatedScheduleListCard = memo(function UndatedScheduleListCard({
initial={{ opacity: 0, x: -10 }} initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }} animate={{ opacity: 1, x: 0 }}
transition={{ delay, type: 'spring', stiffness: 300, damping: 30 }} transition={{ delay, type: 'spring', stiffness: 300, damping: 30 }}
onClick={onClick} onClick={() => onClick?.(schedule)}
className={`cursor-pointer ${className}`} className={`cursor-pointer ${className}`}
> >
{/* 카드 본체 (점선 테두리) */} {/* 카드 본체 (점선 테두리) */}

View file

@ -14,7 +14,7 @@ const BirthdayCard = memo(function BirthdayCard({ schedule, showYear = false, on
return ( return (
<div <div
onClick={onClick} onClick={() => onClick?.(schedule)}
className="relative overflow-hidden bg-gradient-to-r from-pink-400 via-purple-400 to-indigo-400 rounded-2xl shadow-lg hover:shadow-xl transition-shadow cursor-pointer" className="relative overflow-hidden bg-gradient-to-r from-pink-400 via-purple-400 to-indigo-400 rounded-2xl shadow-lg hover:shadow-xl transition-shadow cursor-pointer"
> >
{/* 배경 장식 */} {/* 배경 장식 */}

View file

@ -26,7 +26,7 @@ const ScheduleCard = memo(function ScheduleCard({ schedule, onClick, className =
return ( return (
<div <div
onClick={onClick} onClick={() => onClick?.(schedule)}
className={`flex items-stretch bg-white rounded-2xl shadow-sm hover:shadow-md transition-shadow overflow-hidden cursor-pointer ${className}`} className={`flex items-stretch bg-white rounded-2xl shadow-sm hover:shadow-md transition-shadow overflow-hidden cursor-pointer ${className}`}
> >
{/* 날짜 영역 */} {/* 날짜 영역 */}

View file

@ -17,7 +17,7 @@ const UndatedScheduleCard = memo(function UndatedScheduleCard({ schedule, onClic
return ( return (
<div <div
onClick={onClick} onClick={() => onClick?.(schedule)}
className={`flex items-stretch bg-white rounded-2xl border-2 border-dashed border-gray-300 hover:border-gray-400 transition-colors overflow-hidden cursor-pointer ${className}`} className={`flex items-stretch bg-white rounded-2xl border-2 border-dashed border-gray-300 hover:border-gray-400 transition-colors overflow-hidden cursor-pointer ${className}`}
> >
{/* 월 영역 (연한 카테고리 색) */} {/* 월 영역 (연한 카테고리 색) */}

View file

@ -1,4 +1,4 @@
import { useState, useEffect, useMemo, useRef } from 'react'; import { useState, useEffect, useMemo, useRef, useCallback } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { ChevronLeft, ChevronRight, ChevronDown, Search, X, Calendar } from 'lucide-react'; import { ChevronLeft, ChevronRight, ChevronDown, Search, X, Calendar } from 'lucide-react';
@ -44,6 +44,11 @@ function MobileSchedule() {
); );
const setSelectedDate = (date) => setStoredSelectedDate(date); const setSelectedDate = (date) => setStoredSelectedDate(date);
// ( React.memo )
const handleCardClick = useCallback((schedule) => {
navigate(`/schedule/${schedule.id}`);
}, [navigate]);
const [isSearchMode, setIsSearchMode] = useState(false); const [isSearchMode, setIsSearchMode] = useState(false);
const [searchInput, setSearchInput] = useState(''); const [searchInput, setSearchInput] = useState('');
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
@ -888,7 +893,7 @@ function MobileSchedule() {
key={schedule.id} key={schedule.id}
schedule={schedule} schedule={schedule}
delay={index * 0.05} delay={index * 0.05}
onClick={() => navigate(`/schedule/${schedule.id}`)} onClick={handleCardClick}
/> />
); );
} }
@ -908,7 +913,7 @@ function MobileSchedule() {
key={schedule.id} key={schedule.id}
schedule={schedule} schedule={schedule}
delay={index * 0.05} delay={index * 0.05}
onClick={() => navigate(`/schedule/${schedule.id}`)} onClick={handleCardClick}
/> />
); );
})} })}
@ -926,7 +931,7 @@ function MobileSchedule() {
key={`undated-${schedule.id}`} key={`undated-${schedule.id}`}
schedule={schedule} schedule={schedule}
delay={(selectedDateSchedules.length + index) * 0.05} delay={(selectedDateSchedules.length + index) * 0.05}
onClick={() => navigate(`/schedule/${schedule.id}`)} onClick={handleCardClick}
/> />
))} ))}
</div> </div>

View file

@ -292,7 +292,7 @@ function PCSchedule() {
}); });
// //
const handleScheduleClick = (schedule) => { const handleScheduleClick = useCallback((schedule) => {
// , , // , ,
if (schedule.is_birthday || schedule.is_debut || schedule.is_anniversary) { if (schedule.is_birthday || schedule.is_debut || schedule.is_anniversary) {
navigate(`/schedule/${schedule.id}`); navigate(`/schedule/${schedule.id}`);
@ -309,7 +309,7 @@ function PCSchedule() {
} else { } else {
navigate(`/schedule/${schedule.id}`); navigate(`/schedule/${schedule.id}`);
} }
}; }, [navigate]);
// //
const toggleCategory = (categoryId) => { const toggleCategory = (categoryId) => {
@ -586,11 +586,11 @@ function PCSchedule() {
> >
<div className={virtualItem.index < filteredSchedules.length - 1 ? 'pb-4' : ''}> <div className={virtualItem.index < filteredSchedules.length - 1 ? 'pb-4' : ''}>
{schedule.is_birthday ? ( {schedule.is_birthday ? (
<BirthdayCard schedule={schedule} showYear onClick={() => handleScheduleClick(schedule)} /> <BirthdayCard schedule={schedule} showYear onClick={handleScheduleClick} />
) : schedule.is_debut || schedule.is_anniversary ? ( ) : schedule.is_debut || schedule.is_anniversary ? (
<DebutCard schedule={schedule} showYear /> <DebutCard schedule={schedule} showYear />
) : ( ) : (
<ScheduleCard schedule={schedule} showYear onClick={() => handleScheduleClick(schedule)} /> <ScheduleCard schedule={schedule} showYear onClick={handleScheduleClick} />
)} )}
</div> </div>
</div> </div>
@ -618,11 +618,11 @@ function PCSchedule() {
transition={{ delay: Math.min(index, 10) * 0.03 }} transition={{ delay: Math.min(index, 10) * 0.03 }}
> >
{schedule.is_birthday ? ( {schedule.is_birthday ? (
<BirthdayCard schedule={schedule} onClick={() => handleScheduleClick(schedule)} /> <BirthdayCard schedule={schedule} onClick={handleScheduleClick} />
) : schedule.is_debut || schedule.is_anniversary ? ( ) : schedule.is_debut || schedule.is_anniversary ? (
<DebutCard schedule={schedule} /> <DebutCard schedule={schedule} />
) : ( ) : (
<ScheduleCard schedule={schedule} onClick={() => handleScheduleClick(schedule)} /> <ScheduleCard schedule={schedule} onClick={handleScheduleClick} />
)} )}
</motion.div> </motion.div>
))} ))}
@ -642,7 +642,7 @@ function PCSchedule() {
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
transition={{ delay: Math.min(filteredSchedules.length + index, 10) * 0.03 }} transition={{ delay: Math.min(filteredSchedules.length + index, 10) * 0.03 }}
> >
<UndatedScheduleCard schedule={schedule} onClick={() => handleScheduleClick(schedule)} /> <UndatedScheduleCard schedule={schedule} onClick={handleScheduleClick} />
</motion.div> </motion.div>
))} ))}
</> </>