-
Notifications
You must be signed in to change notification settings - Fork 3
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
refactor: 식품 이미지(SuspendedImg)에 lazy loading 적용 #529
Changes from 4 commits
435a1e0
1b1e392
73e58a2
c26b989
33165c7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 외부 ref가 callbackRef인 경우 그대로 실행시키고, 아닌 경우 외부 ref에 img 엘리먼트를 할당합니다 |
||
}, []); | ||
Comment on lines
+13
to
+20
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 외부 ref와 내부 ref(targetRef)에 동일한 돔을 할당하고자 img 엘리먼트에 callbackRef를 삽입하는 방식으로 변경하였습니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 더 자세히 설명해주실수잇나요?? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
내부적으로는 lazy loading을 위해 외부 ref를 신경쓰지 않을 땐 targetRef를 img 엘리먼트에 바로 주입해도 문제가 없었지만 외부 ref가 주입될 수 있는 경우 각 경우의 수에 대비하고 외부 ref에 img 엘리먼트를 제공함과 동시에 targetRef에도 ref와 동일한 img 엘리먼트를 할당하기 위해 커밋 페이즈에 돔과 함께 실행되는 callbackRef 방식을 사용하여 각 ref에 동일한 img 엘리먼트를 할당하였습니다! |
||
|
||
if (isIntersected && targetRef.current && !targetRef.current.src && src) { | ||
targetRef.current.src = src; | ||
} | ||
Comment on lines
+22
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
||
// eslint-disable-next-line jsx-a11y/alt-text | ||
return <img {...restProps} loading="lazy" ref={callbackRef} />; | ||
}, | ||
); | ||
|
||
LazyImg.displayName = 'LazyImg'; | ||
|
||
export default memo(LazyImg); |
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 유용할 것 같지 않은 속성을 제거하였습니다 |
||
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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. lazy loading의 경우 돔이 감지되었을 때 이미지 요청을 하도록 변경(버그 수정)하였습니다 |
||
}); | ||
|
||
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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. memo를 적용했습니다 |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 /> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 누락됐던 lazy 속성을 추가했습니다 |
||
</FoodImageWrapper> | ||
<BrandName>{brandName}</BrandName> | ||
<FoodName>{foodName}</FoodName> | ||
|
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>; | ||
|
@@ -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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ref.current 수정이 필요하여 |
||
|
||
const observerRef = useRef<IntersectionObserver | null>(null); | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
targetRef를 통해 img 엘리먼트에 접근하고 이미지 다운로드를 조작합니다