Skip to content

Commit

Permalink
Merge pull request #511 from woowacourse-teams/feat/475-organize-prio…
Browse files Browse the repository at this point in the history
…rity-of-image-fetch

불필요한 이미지 fetch 최소화 및 이미지 fetch 우선순위 조정
  • Loading branch information
solo5star authored Sep 29, 2023
2 parents 30ffc8f + 3bf205b commit b124eb8
Show file tree
Hide file tree
Showing 2 changed files with 60 additions and 27 deletions.
27 changes: 10 additions & 17 deletions client/src/components/CafeCard.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useEffect, useRef, useState } from 'react';
import type { UIEventHandler } from 'react';
import { useCallback, useState } from 'react';
import { styled } from 'styled-components';
import type { Cafe } from '../types';
import Resource from '../utils/Resource';
Expand All @@ -16,21 +17,12 @@ const CafeCard = (props: CardProps) => {
const [isShowDetail, setIsShowDetail] = useState(false);
const [currentImageIndex, setCurrentImageIndex] = useState(0);

const ref = useRef<HTMLDivElement>(null);

useEffect(() => {
const handleScroll = () => {
if (ref.current) {
const { scrollLeft, clientWidth } = ref.current;
const index = Math.round(scrollLeft / clientWidth);
setCurrentImageIndex(index);
}
};

ref.current?.addEventListener('scroll', handleScroll);
return () => {
ref.current?.removeEventListener('scroll', handleScroll);
};
const handleScroll: UIEventHandler = useCallback((event) => {
if (!(event.target instanceof HTMLDivElement)) return;

const { scrollLeft, clientWidth } = event.target;
const index = Math.round(scrollLeft / clientWidth);
setCurrentImageIndex(index);
}, []);

return (
Expand All @@ -40,12 +32,13 @@ const CafeCard = (props: CardProps) => {
{`${currentImageIndex + 1}`}/{cafe.images.length}
</CardQuantityContents>
</CardQuantityContainer>
<CarouselImageList ref={ref}>
<CarouselImageList onScroll={handleScroll}>
{cafe.images.map((image, index) => (
<CarouselImage
key={index}
src={Resource.getImageUrl({ size: '500', filename: image })}
alt={`${cafe}의 이미지`}
loading={Math.abs(currentImageIndex - index) <= 1 ? 'eager' : 'lazy'}
/>
))}
</CarouselImageList>
Expand Down
60 changes: 50 additions & 10 deletions client/src/components/ScrollSnapContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type React from 'react';
import type { HTMLAttributes, MouseEventHandler, TouchEventHandler, WheelEventHandler } from 'react';
import type { HTMLAttributes, MouseEventHandler, PropsWithChildren, TouchEventHandler, WheelEventHandler } from 'react';
import { useEffect, useRef, useState } from 'react';
import styled from 'styled-components';

Expand Down Expand Up @@ -66,14 +66,38 @@ const SWIPE_FAST_SCROLL_DISTANCE_RATIO = 0.03;
// 0 = 다음 아이템으로 완전히 넘어가야 스와이프 판정 가능
const SWIPE_WHEEL_SCROLL_VALID_RATIO = 0.1;

type ScrollSnapVirtualItemProps = PropsWithChildren<{
// 이전 아이템인지, 현재 아이템인지, 이후 아이템인지 여부를 나타내는 숫자
offset: -1 | 0 | 1;
// 0.0 ~ 1.0
position: number;
}>;

const ScrollSnapVirtualItem = (props: ScrollSnapVirtualItemProps) => {
const { offset, position, children } = props;

return (
<div
style={{
height: '100%',
gridArea: '1 / 1 / 1 / 1',
transform: `translateY(${(offset + -position) * 100}%)`,
}}
>
{children}
</div>
);
};

type ScrollSnapVirtualItemsProps<Item> = {
scrollPosition: number;
items: Item[];
itemRenderer: (item: Item, index: number) => React.ReactNode;
enableRolling?: boolean;
};

const ScrollSnapVirtualItems = <Item,>(props: ScrollSnapVirtualItemsProps<Item>) => {
const { scrollPosition, items, itemRenderer } = props;
const { scrollPosition, items, itemRenderer, enableRolling } = props;

// position of item, which user sees
// always positive integer
Expand All @@ -83,22 +107,38 @@ const ScrollSnapVirtualItems = <Item,>(props: ScrollSnapVirtualItemsProps<Item>)
const indexedItems = items.map((item, index) => ({ index, item }));

// 현재 화면의 아이템 및 위, 아래의 아이템을 표시한다
const visibleItems = [
indexedItems[focusedIndex - 1] ?? indexedItems[indexedItems.length - 1],
indexedItems[focusedIndex],
indexedItems[focusedIndex + 1] ?? indexedItems[0],
];
const visibleItems = enableRolling
? [
indexedItems.at(focusedIndex - 1), // 현재에서 상단 (혹은 끝) 아이템
indexedItems.at(focusedIndex), // 현재 아이템
indexedItems.at((focusedIndex + 1) % indexedItems.length), // 현재에서 하단 (혹은 첫) 아이템
]
: [
indexedItems[focusedIndex - 1], // 현재에서 상단 아이템
indexedItems[focusedIndex], // 현재 아이템
indexedItems[focusedIndex + 1], // 현재에서 하단 아이템
];
const visiblePosition = mod(scrollPosition, 1);

return (
<div
style={{
width: '100%',
height: '100%',
transform: `translateY(${-100 + -visiblePosition * 100}%)`,
display: 'grid',
}}
>
{visibleItems.map(({ item, index }) => itemRenderer(item, index))}
{([0, 1, -1] as const)
.map((visibleIndex) => ({ ...visibleItems[1 + visibleIndex], visibleIndex }))
.map(({ item, index, visibleIndex }) =>
item && typeof index === 'number' ? (
<ScrollSnapVirtualItem key={index} offset={visibleIndex} position={visiblePosition}>
{itemRenderer(item, index)}
</ScrollSnapVirtualItem>
) : (
<ScrollSnapVirtualItem key={index} offset={visibleIndex} position={visiblePosition} />
),
)}
</div>
);
};
Expand Down Expand Up @@ -380,7 +420,7 @@ const ScrollSnapContainer = <Item,>(props: ScrollSnapContainerProps<Item>) => {
onMouseLeave={handleMouseLeave}
onWheel={handleWheel}
>
<ScrollSnapVirtualItems scrollPosition={scrollPosition} items={items} itemRenderer={itemRenderer} />
<ScrollSnapVirtualItems scrollPosition={scrollPosition} items={items} itemRenderer={itemRenderer} enableRolling />
</Container>
);
};
Expand Down

0 comments on commit b124eb8

Please sign in to comment.