perf(schedule): 카드 onClick 안정화로 React.memo 복구
카드가 onClick(schedule)을 호출하도록 변경(기존 호출부는 인자 무시라 호환), 페이지는 useCallback 안정 핸들러를 전달. 매 렌더 새 인라인 함수로 memo가 깨져 필터/스크롤마다 전체 카드가 리렌더되던 문제 해결. 검색 결과 가상화 카드는 범위에서 제외(이미 최적화됨). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
067618d792
commit
1b330872f5
8 changed files with 23 additions and 18 deletions
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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}`}
|
||||||
>
|
>
|
||||||
{/* 카드 본체 (플랫 테두리) */}
|
{/* 카드 본체 (플랫 테두리) */}
|
||||||
|
|
|
||||||
|
|
@ -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}`}
|
||||||
>
|
>
|
||||||
{/* 카드 본체 (점선 테두리) */}
|
{/* 카드 본체 (점선 테두리) */}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
>
|
>
|
||||||
{/* 배경 장식 */}
|
{/* 배경 장식 */}
|
||||||
|
|
|
||||||
|
|
@ -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}`}
|
||||||
>
|
>
|
||||||
{/* 날짜 영역 */}
|
{/* 날짜 영역 */}
|
||||||
|
|
|
||||||
|
|
@ -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}`}
|
||||||
>
|
>
|
||||||
{/* 월 영역 (연한 카테고리 색) */}
|
{/* 월 영역 (연한 카테고리 색) */}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue