Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FIX] 메인 페이지 네트워크 요청 최적화 #263

Merged
merged 7 commits into from
Dec 2, 2024
2 changes: 1 addition & 1 deletion frontend/src/apis/queries/main/useFetchMainLive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ export const useMainLive = () => {
return useSuspenseQuery<MainLive[], Error>({
queryKey: ['mainLive'],
queryFn: fetchMainLive,
refetchOnWindowFocus: false,
refetchOnWindowFocus: false
});
};
81 changes: 21 additions & 60 deletions frontend/src/components/main/LiveVideoCard.tsx
Original file line number Diff line number Diff line change
@@ -1,85 +1,45 @@
import { useNavigate } from 'react-router-dom';
import { useEffect, useState, useRef } from 'react';
import styled from 'styled-components';

import sampleProfile from '@assets/sample_profile.png';
import ShowInfoBadge from '@common/ShowInfoBadge';
import { ASSETS } from '@constants/assets';
import { RecentLive } from '@type/live';
import { LiveBadge, LiveViewCountBadge } from './ThumbnailBadge';
import usePlayer from '@hooks/usePlayer';
import { useVideoPreview } from '@hooks/useVideoPreview';

interface LiveVideoCardProps {
videoData: RecentLive;
}

const LiveVideoCard = ({ videoData }: LiveVideoCardProps) => {
const navigate = useNavigate();
const [isHovered, setIsHovered] = useState(false);
const hoverTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const thumbnailRef = useRef<HTMLDivElement>(null);

const { concurrentUserCount, category, channel, tags, defaultThumbnailImageUrl, liveId, liveImageUrl, liveTitle, streamUrl } =
videoData;

const videoRef = usePlayer(streamUrl);

useEffect(() => {
const video = videoRef.current;
if (!video) return;

const resetVideo = () => {
video.pause();
video.currentTime = 0;
};

const playVideo = () => {
video.currentTime = 0;
video.play();
};

const clearHoverTimeout = () => {
if (hoverTimeoutRef.current) {
clearTimeout(hoverTimeoutRef.current);
hoverTimeoutRef.current = null;
}
};

if (isHovered) {
hoverTimeoutRef.current = setTimeout(playVideo, 500);
return;
}

clearHoverTimeout();
resetVideo();

return clearHoverTimeout;
}, [isHovered]);

const handleLiveClick = () => {
navigate(`/live/${liveId}`);
};
const {
concurrentUserCount,
category,
channel,
tags,
defaultThumbnailImageUrl,
liveId,
liveImageUrl,
liveTitle,
streamUrl
} = videoData;

const handleMouseEnter = () => {
setIsHovered(true);
};
const { isHovered, isVideoLoaded, videoRef, handleMouseEnter, handleMouseLeave } = useVideoPreview(streamUrl);

const handleMouseLeave = () => {
setIsHovered(false);
const handleLiveClick = () => {
navigate(`/live/${liveId}`);
};

return (
<VideoCardContainer>
<ThumbnailContainer
ref={thumbnailRef}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onClick={handleLiveClick}
>
<VideoBox $isVisible={isHovered}>
<video ref={videoRef} muted playsInline />
<ThumbnailContainer onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} onClick={handleLiveClick}>
<VideoBox $isVisible={isHovered && isVideoLoaded}>
<video ref={videoRef} muted playsInline preload="none" />
</VideoBox>
<VideoCardThumbnail $isVideoVisible={isHovered}>
<VideoCardThumbnail $isVideoVisible={isHovered && isVideoLoaded}>
<VideoCardImage src={defaultThumbnailImageUrl ?? liveImageUrl} />
</VideoCardThumbnail>
<VideoCardDescription>
Expand Down Expand Up @@ -132,7 +92,7 @@ const VideoBox = styled.div<{ $isVisible: boolean }>`
width: 100%;
height: 100%;
opacity: ${(props) => (props.$isVisible ? 1 : 0)};
transition: opacity 0.3s ease-in-out;
transition: opacity 0.3s ease-in-out 0.6s;
z-index: 1;

video {
Expand Down Expand Up @@ -203,6 +163,7 @@ const VideoCardArea = styled.div`
${({ theme }) => theme.tokenTypographys['display-bold16']}
color: ${({ theme }) => theme.tokenColors['text-strong']};
margin-bottom: 8px;
cursor: pointer;
}
.video_card_name {
${({ theme }) => theme.tokenTypographys['display-medium14']}
Expand Down
52 changes: 19 additions & 33 deletions frontend/src/components/main/MiniPlayerItem.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { memo, useCallback, useMemo } from 'react';
import styled from 'styled-components';
import { MainLive } from '@type/live';

Expand All @@ -9,39 +8,26 @@ interface MiniPlayerItemProps {
isSelected: boolean;
}

const MiniPlayerItem = memo<MiniPlayerItemProps>(
({ item, onMouseEnter, index, isSelected }) => {
const handleMouseEnter = useCallback(() => {
onMouseEnter(index);
}, [onMouseEnter, index]);
const MiniPlayerItem = ({ item, onMouseEnter, index, isSelected }: MiniPlayerItemProps) => {
const thumbnailUrl = item.defaultThumbnailImageUrl ?? item.liveImageUrl;

const thumbnailUrl = useMemo(
() => item.defaultThumbnailImageUrl ?? item.liveImageUrl,
[item.defaultThumbnailImageUrl, item.liveImageUrl]
);

return (
<MiniPlayerItemStyled role="none" onMouseEnter={handleMouseEnter}>
<Thumbnail role="tab" aria-selected={isSelected}>
<ThumbnailWrapper $backgroundUrl={thumbnailUrl} />
</Thumbnail>
<TooltipContent>
<Title>{item.liveTitle}</Title>
<StreamerName>{item.channel.channelName}</StreamerName>
</TooltipContent>
</MiniPlayerItemStyled>
);
},
(prevProps, nextProps) => {
return (
prevProps.isSelected === nextProps.isSelected &&
prevProps.item.liveId === nextProps.item.liveId &&
prevProps.index === nextProps.index
);
}
);

MiniPlayerItem.displayName = 'MiniPlayerItem';
return (
<MiniPlayerItemStyled
role="none"
onMouseEnter={() => {
onMouseEnter(index);
}}
>
<Thumbnail role="tab" aria-selected={isSelected}>
<ThumbnailWrapper $backgroundUrl={thumbnailUrl} />
</Thumbnail>
<TooltipContent>
<Title>{item.liveTitle}</Title>
<StreamerName>{item.channel.channelName}</StreamerName>
</TooltipContent>
</MiniPlayerItemStyled>
);
};

export default MiniPlayerItem;

Expand Down
63 changes: 8 additions & 55 deletions frontend/src/components/main/RecommendLive.tsx
Original file line number Diff line number Diff line change
@@ -1,62 +1,20 @@
import { useEffect, useState, useCallback, useRef, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import styled from 'styled-components';

import AnimatedProfileSection from './AnimatedProfileSection';
import AnimatedLiveHeader from './AnimatedLiveHeader';
import RecommendList from './RecommendList';
import sampleProfile from '@assets/sample_profile.png';
import { RECOMMEND_LIVE } from '@constants/recommendLive';
import useRotatingPlayer from '@hooks/useRotatePlayer';
import { useMainLive } from '@queries/main/useFetchMainLive';
import useMainLiveRotation from '@hooks/useMainLiveRotation';

import AnimatedProfileSection from './AnimatedProfileSection';
import AnimatedLiveHeader from './AnimatedLiveHeader';
import RecommendList from './RecommendList';

const RecommendLive = () => {
const navigate = useNavigate();
const { videoRef, initPlayer } = useRotatingPlayer();
const { data: mainLiveData } = useMainLive();
const [currentUrlIndex, setCurrentUrlIndex] = useState(0);
const recommendListRef = useRef<HTMLDivElement>(null);
const [isTransitioning, setIsTransitioning] = useState(false);
const prevUrlIndexRef = useRef(currentUrlIndex);
const isInitialMount = useRef(true);

useEffect(() => {
if (!mainLiveData) return;
if (!mainLiveData[currentUrlIndex]) return;

const handleTransition = async () => {
const videoUrl = mainLiveData[currentUrlIndex].streamUrl;

if (isInitialMount.current) {
initPlayer(videoUrl);
setTimeout(() => {
isInitialMount.current = false;
}, 100);
return;
}

if (prevUrlIndexRef.current !== currentUrlIndex) {
setIsTransitioning(true);
await new Promise((resolve) => setTimeout(resolve, 200));

initPlayer(videoUrl);
prevUrlIndexRef.current = currentUrlIndex;

setTimeout(() => {
setIsTransitioning(false);
}, 100);
}
};

handleTransition();
}, [mainLiveData, currentUrlIndex, initPlayer]);

const onSelect = useCallback((index: number) => {
setCurrentUrlIndex(index);
}, []);

const currentLiveData = useMemo(() => mainLiveData?.[currentUrlIndex], [mainLiveData, currentUrlIndex]);

const { data: mainLiveData } = useMainLive();
const { currentLiveData, isTransitioning, videoRef, onSelect } = useMainLiveRotation(mainLiveData);
const { liveId, liveTitle, concurrentUserCount, channel, category } = currentLiveData;

return (
Expand All @@ -68,12 +26,7 @@ const RecommendLive = () => {
<AnimatedLiveHeader concurrentUserCount={concurrentUserCount} liveTitle={liveTitle} />
<RecommendLiveInformation>
<AnimatedProfileSection channel={channel} category={category} profileImage={sampleProfile} />
<RecommendList
ref={recommendListRef}
mainLiveData={mainLiveData}
onSelect={onSelect}
currentLiveId={liveId}
/>
<RecommendList mainLiveData={mainLiveData} onSelect={onSelect} currentLiveId={liveId} />
</RecommendLiveInformation>
</RecommendLiveWrapper>
</RecommendLiveContainer>
Expand Down
Loading
Loading