Skip to content

Commit

Permalink
refactor: 식품 이미지 미처 적용 안됐던 lazy loading 적용 (#529)
Browse files Browse the repository at this point in the history
* feat: FoodItem lazy loading 적용

* refactor: observerRef를 RefObject > MutableRefObject로 변경

* refactor: SuspendedImg의 lazy loading을 LazyImage를 활용하도록 변경

* refactor: LazyImage > LazyImg 네이밍 변경

* refactor: LazyImg 불필요한 loading 속성 제거
  • Loading branch information
n0eyes authored Oct 22, 2023
1 parent ef95a6f commit 727dbcc
Show file tree
Hide file tree
Showing 5 changed files with 46 additions and 52 deletions.
27 changes: 0 additions & 27 deletions frontend/src/components/@common/LazyImage/LazyImage.tsx

This file was deleted.

33 changes: 33 additions & 0 deletions frontend/src/components/@common/LazyImg/LazyImg.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { ComponentPropsWithoutRef, ForwardedRef, forwardRef, memo, useCallback } from 'react';

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

const LazyImg = forwardRef(
(props: ComponentPropsWithoutRef<'img'>, ref: ForwardedRef<HTMLImageElement>) => {
const { src, ...restProps } = props;

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

const callbackRef = useCallback((instance: HTMLImageElement | null) => {
targetRef.current = instance;

if (!ref) return;

// eslint-disable-next-line no-param-reassign
typeof ref === 'function' ? ref(instance) : (ref.current = instance);
}, []);

if (isIntersected && targetRef.current && !targetRef.current.src && src) {
targetRef.current.src = src;
}

// eslint-disable-next-line jsx-a11y/alt-text
return <img {...restProps} ref={callbackRef} />;
},
);

LazyImg.displayName = 'LazyImg';

export default memo(LazyImg);
32 changes: 10 additions & 22 deletions frontend/src/components/@common/SuspendedImg/SuspendedImg.tsx
Original file line number Diff line number Diff line change
@@ -1,55 +1,43 @@
import { useQuery } from '@tanstack/react-query';
import { ComponentPropsWithoutRef, ComponentPropsWithRef, useEffect } from 'react';
import { ComponentPropsWithoutRef, memo } from 'react';

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

import LazyImg from '../LazyImg/LazyImg';

interface SuspendedImgProps extends ComponentPropsWithoutRef<'img'> {
staleTime?: number;
cacheTime?: number;
enabled?: boolean;
lazy?: boolean;
}

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

const img = new Image();

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

const lazyOptions: ComponentPropsWithRef<'img'> & { 'data-src'?: string } = {
loading: 'lazy',
ref: targetRef,
'data-src': src,
};

useQuery({
queryKey: [src],
queryFn: () =>
new Promise(resolve => {
img.onload = resolve;
img.onerror = resolve;

img.src = src!;
}),
...(staleTime == null ? {} : { staleTime }),
...(cacheTime == null ? {} : { cacheTime }),
enabled: enabled && Boolean(src),
enabled: lazy ? isIntersected : true,
});

useEffect(() => {
if (!targetRef.current) return;

if ('loading' in HTMLImageElement.prototype || isIntersected) {
targetRef.current.src = String(targetRef.current.dataset.src);
}
}, [isIntersected]);

if (lazy) {
return <LazyImg src={src} ref={targetRef} {...restProps} />;
}
// eslint-disable-next-line jsx-a11y/alt-text
return <img {...restProps} {...(lazy ? lazyOptions : { src })} />;
return <img src={src} {...restProps} />;
};

export default SuspendedImg;
export default memo(SuspendedImg);
2 changes: 1 addition & 1 deletion frontend/src/components/Food/FoodItem/FoodItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const FoodItem = (foodItemProps: FoodItemProps) => {
return (
<FoodItemWrapper to={`pet-food/${id}`}>
<FoodImageWrapper>
<FoodImage src={imageUrl} alt="Food image" />
<FoodImage src={imageUrl} alt="Food image" lazy />
</FoodImageWrapper>
<BrandName>{brandName}</BrandName>
<FoodName>{foodName}</FoodName>
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/hooks/@common/useIntersectionObserver.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MutableRefObject, useCallback, useEffect, useRef, useState } from 'react';
import { MutableRefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react';

interface UseIntersectionObserverOptions<T extends HTMLElement> {
ref?: MutableRefObject<T | null>;
Expand All @@ -8,7 +8,7 @@ interface UseIntersectionObserverOptions<T extends HTMLElement> {
export const useIntersectionObserver = <T extends HTMLElement>(
options?: UseIntersectionObserverOptions<T>,
) => {
const localRef = useRef<T>(null);
const localRef: MutableRefObject<T | null> = useRef<T>(null);

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

Expand Down

0 comments on commit 727dbcc

Please sign in to comment.