Skip to content

Commit 727dbcc

Browse files
authored
refactor: 식품 이미지 미처 적용 안됐던 lazy loading 적용 (#529)
* feat: FoodItem lazy loading 적용 * refactor: observerRef를 RefObject > MutableRefObject로 변경 * refactor: SuspendedImg의 lazy loading을 LazyImage를 활용하도록 변경 * refactor: LazyImage > LazyImg 네이밍 변경 * refactor: LazyImg 불필요한 loading 속성 제거
1 parent ef95a6f commit 727dbcc

File tree

5 files changed

+46
-52
lines changed

5 files changed

+46
-52
lines changed

frontend/src/components/@common/LazyImage/LazyImage.tsx

-27
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { ComponentPropsWithoutRef, ForwardedRef, forwardRef, memo, useCallback } from 'react';
2+
3+
import { useIntersectionObserver } from '@/hooks/@common/useIntersectionObserver';
4+
5+
const LazyImg = forwardRef(
6+
(props: ComponentPropsWithoutRef<'img'>, ref: ForwardedRef<HTMLImageElement>) => {
7+
const { src, ...restProps } = props;
8+
9+
const { targetRef, isIntersected } = useIntersectionObserver<HTMLImageElement>({
10+
observerOptions: { threshold: 0.1 },
11+
});
12+
13+
const callbackRef = useCallback((instance: HTMLImageElement | null) => {
14+
targetRef.current = instance;
15+
16+
if (!ref) return;
17+
18+
// eslint-disable-next-line no-param-reassign
19+
typeof ref === 'function' ? ref(instance) : (ref.current = instance);
20+
}, []);
21+
22+
if (isIntersected && targetRef.current && !targetRef.current.src && src) {
23+
targetRef.current.src = src;
24+
}
25+
26+
// eslint-disable-next-line jsx-a11y/alt-text
27+
return <img {...restProps} ref={callbackRef} />;
28+
},
29+
);
30+
31+
LazyImg.displayName = 'LazyImg';
32+
33+
export default memo(LazyImg);
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,43 @@
11
import { useQuery } from '@tanstack/react-query';
2-
import { ComponentPropsWithoutRef, ComponentPropsWithRef, useEffect } from 'react';
2+
import { ComponentPropsWithoutRef, memo } from 'react';
33

44
import { useIntersectionObserver } from '@/hooks/@common/useIntersectionObserver';
55

6+
import LazyImg from '../LazyImg/LazyImg';
7+
68
interface SuspendedImgProps extends ComponentPropsWithoutRef<'img'> {
79
staleTime?: number;
810
cacheTime?: number;
9-
enabled?: boolean;
1011
lazy?: boolean;
1112
}
1213

13-
// eslint-disable-next-line react/display-name
1414
const SuspendedImg = (props: SuspendedImgProps) => {
15-
const { src, cacheTime, staleTime, enabled, lazy, ...restProps } = props;
15+
const { src, cacheTime, staleTime, lazy, ...restProps } = props;
1616

1717
const img = new Image();
1818

1919
const { targetRef, isIntersected } = useIntersectionObserver<HTMLImageElement>({
2020
observerOptions: { threshold: 0.1 },
2121
});
2222

23-
const lazyOptions: ComponentPropsWithRef<'img'> & { 'data-src'?: string } = {
24-
loading: 'lazy',
25-
ref: targetRef,
26-
'data-src': src,
27-
};
28-
2923
useQuery({
3024
queryKey: [src],
3125
queryFn: () =>
3226
new Promise(resolve => {
3327
img.onload = resolve;
3428
img.onerror = resolve;
35-
3629
img.src = src!;
3730
}),
3831
...(staleTime == null ? {} : { staleTime }),
3932
...(cacheTime == null ? {} : { cacheTime }),
40-
enabled: enabled && Boolean(src),
33+
enabled: lazy ? isIntersected : true,
4134
});
4235

43-
useEffect(() => {
44-
if (!targetRef.current) return;
45-
46-
if ('loading' in HTMLImageElement.prototype || isIntersected) {
47-
targetRef.current.src = String(targetRef.current.dataset.src);
48-
}
49-
}, [isIntersected]);
50-
36+
if (lazy) {
37+
return <LazyImg src={src} ref={targetRef} {...restProps} />;
38+
}
5139
// eslint-disable-next-line jsx-a11y/alt-text
52-
return <img {...restProps} {...(lazy ? lazyOptions : { src })} />;
40+
return <img src={src} {...restProps} />;
5341
};
5442

55-
export default SuspendedImg;
43+
export default memo(SuspendedImg);

frontend/src/components/Food/FoodItem/FoodItem.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const FoodItem = (foodItemProps: FoodItemProps) => {
1313
return (
1414
<FoodItemWrapper to={`pet-food/${id}`}>
1515
<FoodImageWrapper>
16-
<FoodImage src={imageUrl} alt="Food image" />
16+
<FoodImage src={imageUrl} alt="Food image" lazy />
1717
</FoodImageWrapper>
1818
<BrandName>{brandName}</BrandName>
1919
<FoodName>{foodName}</FoodName>

frontend/src/hooks/@common/useIntersectionObserver.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { MutableRefObject, useCallback, useEffect, useRef, useState } from 'react';
1+
import { MutableRefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react';
22

33
interface UseIntersectionObserverOptions<T extends HTMLElement> {
44
ref?: MutableRefObject<T | null>;
@@ -8,7 +8,7 @@ interface UseIntersectionObserverOptions<T extends HTMLElement> {
88
export const useIntersectionObserver = <T extends HTMLElement>(
99
options?: UseIntersectionObserverOptions<T>,
1010
) => {
11-
const localRef = useRef<T>(null);
11+
const localRef: MutableRefObject<T | null> = useRef<T>(null);
1212

1313
const observerRef = useRef<IntersectionObserver | null>(null);
1414

0 commit comments

Comments
 (0)