오라운드 메인 페이지를 다양한 상품을 노출하는 방향으로 개편하면서 많은 양의 이미지 로드에 따라 메인이 늦게 노출되는 문제가 발생했다.
아래의 리스트에 해당하는 이미지는 lazy loading을 통해 유저가 스크롤을 내려 화면에 해당 리스트가 노출될 필요가 있을 때 로드되도록 해결했다. lazy loading을 위해 intersectionObserver와 context api를 사용했다.
// 옵션 객체
const options = {
// null을 설정하거나 무엇도 설정하지 않으면 브라우저 viewport가 기준이 된다.
root: null,
// 타겟 요소의 20%가 루트 요소와 겹치면 콜백을 실행한다.
threshold: 0.2
}
// Intersection Observer 인스턴스
const observer = new IntersectionObserver(function(entries,observer) {
entries.forEach(entry => {
// 루트 요소와 타겟 요소가 교차하면 dataset에 있는 이미지 url을 타겟요소의 src 에 넣어준다.
if (entry.isIntersecting) {
entry.target.src = entry.target.dataset.src
// 지연 로딩이 실행되면 unobserve 해준다.
observer.unobserve(entry.target)
}
})
}, options)
const imgs = document.querySelectorAll('img')
imgs.forEach((img) => {
// img들을 observe한다.
observer.observe(img)
})
참조: 실무에서 느낀 점을 곁들인 Intersection Observer API 정리
⇒ image의 경우 src를 html 속성에 넣어두었다가 관찰대상이 교차되었을 때, src를 대체한다.
2. 오라운드 메인에서 해야할 lazy loading
⇒ 오라운드에서 해야할 lazy loading은 mock component를 화면에 보이는 다음 컴포넌트로 넣어두었다가, 스크롤에 해당 컴포넌트가 교차되는 순간 데이터를 패치한다.
그리고 패치가 완료되면, 보여주고자 한 데이터가 있는 컴포넌트를 보여준다.
(데이터 패치 중이면 loading 카드들이 보이고, 데이터 패치에 실패했다면 해당 섹션을 날려준다.)
(1) useOnScreen hook
import { useState, useEffect, useRef, MutableRefObject } from 'react';
/**
* ref 객체가 화면에 출력되는 시점을 감지하는 훅
*
* @param ref 감지 할 대상
* @param rootMargin 해당 값만큼 미리 감지 한다.
*/
export function useOnScreen<T extends Element>(ref: MutableRefObject<T>, rootMargin = '0px'): boolean {
// State and setter for storing whether element is visible
const [isIntersecting, setIntersecting] = useState<boolean>(false);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
setIntersecting(entry.isIntersecting);
if (entry.isIntersecting && ref.current) observer.unobserve(ref.current);
},
{
rootMargin
}
);
if (ref.current) {
observer.observe(ref.current);
}
return () => {
if (ref.current) observer.unobserve(ref.current);
};
}, []);
return isIntersecting;
}
⇒ 해당 훅은 관촬되는 대상이 화면에 들어올 경우 boolean 값을 내놓는 훅이다.
(2)LazyLoadingLayOut 컴포넌트
interface LazyLoadingLayoutProps {
children: React.ReactElement
skeletonView?: React.ReactNode
}
interface GetSkeletonProps {
skeletonType: 'basic' | 'tag';
}
const Loading = () => (
<SkeletonWrapper>
<LoadingWrapper>
<Title.Loading/>
</LoadingWrapper>
<OroundSwiper freeModeYn={true}>
{[...Array(5)].map((value, index) => (
<ProductBox key={`Skeleton_${index}`}>
<ProductCard.Loading/>
</ProductBox>
))}
</OroundSwiper>
</SkeletonWrapper>
);
export const LazyLoadingContext = createContext(true);
// section 별로 skeleton을 Props로 내려받을지 고민중
// skeleton을 props로 받는다면 common한 skeleton 컴포넌트 만든 뒤, default로 사용하고 props 여부 조건문 필요
const LazyLoadingLayout: React.FC<LazyLoadingLayoutProps> = (props) => {
const { children, skeletonView } = props;
const mockTarget: any = useRef<HTMLDivElement>();
const isScreen: boolean = useOnScreen<HTMLDivElement>(mockTarget, '20%');
return (
<LazyLoadingContext.Provider value={isScreen}>
<div ref={mockTarget}>
{children}
{/*{!isScreen ? <Loading/> : (children)}*/}
</div>
</LazyLoadingContext.Provider>
);
};
export default LazyLoadingLayout;
⇒ children으로 각 섹션의 리스트가 관찰대상이 되고, 20%의 margin 내에 관찰대상이 들어오게 되면 boolean 값을 내놓아주는 레이아웃 컴포넌트이다.
[문제상황]
[해결 방법]
const Section = () => {
const isOnScreenYn = useContext(LazyLoadingContext);
const router = useRouter();
const { loading, data, error } = useData(isOnScreenYn);
if (!isOnScreenYn || loading) {
return (
<Loading/>
);
}
if (!list?.length) return null;
return (
<Wrapper>
<Title>
<Title.TitleName title={data.title}/>
</Title>
<List productList={data?.productlist}/>
</Wrapper>
);
};
export default Section;