Skip to content
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

Merged
merged 5 commits into from
Oct 22, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

targetRef를 통해 img 엘리먼트에 접근하고 이미지 다운로드를 조작합니다


if (!ref) return;

// eslint-disable-next-line no-param-reassign
typeof ref === 'function' ? ref(instance) : (ref.current = instance);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

외부 ref가 callbackRef인 경우 그대로 실행시키고, 아닌 경우 외부 ref에 img 엘리먼트를 할당합니다

}, []);
Comment on lines +13 to +20
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

외부 ref와 내부 ref(targetRef)에 동일한 돔을 할당하고자 img 엘리먼트에 callbackRef를 삽입하는 방식으로 변경하였습니다.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

더 자세히 설명해주실수잇나요??

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LazyImg는 외부에서 ref를 주입받을 수 있는데요!

내부적으로는 lazy loading을 위해 useIntersectionObserver에서 반환된 targetRef를 사용하고 있어요! (옵저빙 되고 있는 ref)

외부 ref를 신경쓰지 않을 땐 targetRef를 img 엘리먼트에 바로 주입해도 문제가 없었지만 외부 ref가 주입될 수 있는 경우
ref는 콜백 함수 혹은 refObject일 수 있습니다!

각 경우의 수에 대비하고 외부 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
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

!targetRef.current.src: 이미지 중복 다운을 방지(이미지 다운 실패 시)하기 위하여 추가한 조건입니다
src: src 속성에 올바르지 않은 경로(ex. undefined)가 추가되는 것을 방지하기 위하여 추가한 조건입니다


// eslint-disable-next-line jsx-a11y/alt-text
return <img {...restProps} loading="lazy" 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;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

memo를 적용했습니다

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 />
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

누락됐던 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);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ref.current 수정이 필요하여 MutableRefObject로 타입을 지정하였습니다


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

Expand Down