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

View file

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

View file

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

View file

@ -14,7 +14,7 @@ const BirthdayCard = memo(function BirthdayCard({ schedule, showYear = false, on
return (
<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"
>
{/* 배경 장식 */}

View file

@ -26,7 +26,7 @@ const ScheduleCard = memo(function ScheduleCard({ schedule, onClick, className =
return (
<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}`}
>
{/* 날짜 영역 */}

View file

@ -17,7 +17,7 @@ const UndatedScheduleCard = memo(function UndatedScheduleCard({ schedule, onClic
return (
<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}`}
>
{/* 월 영역 (연한 카테고리 색) */}

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 { motion, AnimatePresence } from 'framer-motion';
import { ChevronLeft, ChevronRight, ChevronDown, Search, X, Calendar } from 'lucide-react';
@ -44,6 +44,11 @@ function MobileSchedule() {
);
const setSelectedDate = (date) => setStoredSelectedDate(date);
// ( React.memo )
const handleCardClick = useCallback((schedule) => {
navigate(`/schedule/${schedule.id}`);
}, [navigate]);
const [isSearchMode, setIsSearchMode] = useState(false);
const [searchInput, setSearchInput] = useState('');
const [searchTerm, setSearchTerm] = useState('');
@ -888,7 +893,7 @@ function MobileSchedule() {
key={schedule.id}
schedule={schedule}
delay={index * 0.05}
onClick={() => navigate(`/schedule/${schedule.id}`)}
onClick={handleCardClick}
/>
);
}
@ -908,7 +913,7 @@ function MobileSchedule() {
key={schedule.id}
schedule={schedule}
delay={index * 0.05}
onClick={() => navigate(`/schedule/${schedule.id}`)}
onClick={handleCardClick}
/>
);
})}
@ -926,7 +931,7 @@ function MobileSchedule() {
key={`undated-${schedule.id}`}
schedule={schedule}
delay={(selectedDateSchedules.length + index) * 0.05}
onClick={() => navigate(`/schedule/${schedule.id}`)}
onClick={handleCardClick}
/>
))}
</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) {
navigate(`/schedule/${schedule.id}`);
@ -309,7 +309,7 @@ function PCSchedule() {
} else {
navigate(`/schedule/${schedule.id}`);
}
};
}, [navigate]);
//
const toggleCategory = (categoryId) => {
@ -586,11 +586,11 @@ function PCSchedule() {
>
<div className={virtualItem.index < filteredSchedules.length - 1 ? 'pb-4' : ''}>
{schedule.is_birthday ? (
<BirthdayCard schedule={schedule} showYear onClick={() => handleScheduleClick(schedule)} />
<BirthdayCard schedule={schedule} showYear onClick={handleScheduleClick} />
) : schedule.is_debut || schedule.is_anniversary ? (
<DebutCard schedule={schedule} showYear />
) : (
<ScheduleCard schedule={schedule} showYear onClick={() => handleScheduleClick(schedule)} />
<ScheduleCard schedule={schedule} showYear onClick={handleScheduleClick} />
)}
</div>
</div>
@ -618,11 +618,11 @@ function PCSchedule() {
transition={{ delay: Math.min(index, 10) * 0.03 }}
>
{schedule.is_birthday ? (
<BirthdayCard schedule={schedule} onClick={() => handleScheduleClick(schedule)} />
<BirthdayCard schedule={schedule} onClick={handleScheduleClick} />
) : schedule.is_debut || schedule.is_anniversary ? (
<DebutCard schedule={schedule} />
) : (
<ScheduleCard schedule={schedule} onClick={() => handleScheduleClick(schedule)} />
<ScheduleCard schedule={schedule} onClick={handleScheduleClick} />
)}
</motion.div>
))}
@ -642,7 +642,7 @@ function PCSchedule() {
animate={{ opacity: 1 }}
transition={{ delay: Math.min(filteredSchedules.length + index, 10) * 0.03 }}
>
<UndatedScheduleCard schedule={schedule} onClick={() => handleScheduleClick(schedule)} />
<UndatedScheduleCard schedule={schedule} onClick={handleScheduleClick} />
</motion.div>
))}
</>