From 727dbccecfd02cee2f6d3e54d6e2a37e02f6b569 Mon Sep 17 00:00:00 2001 From: Seyeon Jeong <79056677+n0eyes@users.noreply.github.com> Date: Sun, 22 Oct 2023 19:23:54 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20=EC=8B=9D=ED=92=88=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EB=AF=B8=EC=B2=98=20=EC=A0=81=EC=9A=A9=20?= =?UTF-8?q?=EC=95=88=EB=90=90=EB=8D=98=20lazy=20loading=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=20(#529)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: FoodItem lazy loading 적용 * refactor: observerRef를 RefObject > MutableRefObject로 변경 * refactor: SuspendedImg의 lazy loading을 LazyImage를 활용하도록 변경 * refactor: LazyImage > LazyImg 네이밍 변경 * refactor: LazyImg 불필요한 loading 속성 제거 --- .../@common/LazyImage/LazyImage.tsx | 27 --------------- .../components/@common/LazyImg/LazyImg.tsx | 33 +++++++++++++++++++ .../@common/SuspendedImg/SuspendedImg.tsx | 32 ++++++------------ .../src/components/Food/FoodItem/FoodItem.tsx | 2 +- .../hooks/@common/useIntersectionObserver.ts | 4 +-- 5 files changed, 46 insertions(+), 52 deletions(-) delete mode 100644 frontend/src/components/@common/LazyImage/LazyImage.tsx create mode 100644 frontend/src/components/@common/LazyImg/LazyImg.tsx diff --git a/frontend/src/components/@common/LazyImage/LazyImage.tsx b/frontend/src/components/@common/LazyImage/LazyImage.tsx deleted file mode 100644 index 5a5cf6e9..00000000 --- a/frontend/src/components/@common/LazyImage/LazyImage.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { ComponentPropsWithoutRef, useEffect, useRef } from 'react'; - -import { useIntersectionObserver } from '@/hooks/@common/useIntersectionObserver'; - -interface LazyImageProps extends ComponentPropsWithoutRef<'img'> { - src: string; -} - -const LazyImage = (props: LazyImageProps) => { - const { src, ...restProps } = props; - - const { targetRef, isIntersected } = useIntersectionObserver({ - observerOptions: { threshold: 0.1 }, - }); - - useEffect(() => { - if (!targetRef.current) return; - - if ('loading' in HTMLImageElement.prototype || isIntersected) { - targetRef.current.src = String(targetRef.current.dataset.src); - } - }, [isIntersected]); - - return ; -}; - -export default LazyImage; diff --git a/frontend/src/components/@common/LazyImg/LazyImg.tsx b/frontend/src/components/@common/LazyImg/LazyImg.tsx new file mode 100644 index 00000000..78ee0e83 --- /dev/null +++ b/frontend/src/components/@common/LazyImg/LazyImg.tsx @@ -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) => { + const { src, ...restProps } = props; + + const { targetRef, isIntersected } = useIntersectionObserver({ + 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 ; + }, +); + +LazyImg.displayName = 'LazyImg'; + +export default memo(LazyImg); diff --git a/frontend/src/components/@common/SuspendedImg/SuspendedImg.tsx b/frontend/src/components/@common/SuspendedImg/SuspendedImg.tsx index cc183c0b..a66a6a3a 100644 --- a/frontend/src/components/@common/SuspendedImg/SuspendedImg.tsx +++ b/frontend/src/components/@common/SuspendedImg/SuspendedImg.tsx @@ -1,18 +1,18 @@ 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(); @@ -20,36 +20,24 @@ const SuspendedImg = (props: SuspendedImgProps) => { 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 ; + } // eslint-disable-next-line jsx-a11y/alt-text - return ; + return ; }; -export default SuspendedImg; +export default memo(SuspendedImg); diff --git a/frontend/src/components/Food/FoodItem/FoodItem.tsx b/frontend/src/components/Food/FoodItem/FoodItem.tsx index dc53fd77..72ca2951 100644 --- a/frontend/src/components/Food/FoodItem/FoodItem.tsx +++ b/frontend/src/components/Food/FoodItem/FoodItem.tsx @@ -13,7 +13,7 @@ const FoodItem = (foodItemProps: FoodItemProps) => { return ( - + {brandName} {foodName} diff --git a/frontend/src/hooks/@common/useIntersectionObserver.ts b/frontend/src/hooks/@common/useIntersectionObserver.ts index 2bef6e22..bd3a8daa 100644 --- a/frontend/src/hooks/@common/useIntersectionObserver.ts +++ b/frontend/src/hooks/@common/useIntersectionObserver.ts @@ -1,4 +1,4 @@ -import { MutableRefObject, useCallback, useEffect, useRef, useState } from 'react'; +import { MutableRefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react'; interface UseIntersectionObserverOptions { ref?: MutableRefObject; @@ -8,7 +8,7 @@ interface UseIntersectionObserverOptions { export const useIntersectionObserver = ( options?: UseIntersectionObserverOptions, ) => { - const localRef = useRef(null); + const localRef: MutableRefObject = useRef(null); const observerRef = useRef(null);