Lazyloading이란 resource를 필요로할 때 비동기적으로 로드하는 전략으로, 페이지나 애플리케이션의 초기 로드 시 모든 콘텐츠를 동기적으로 로드하는 것이 아니라, 사용자가 스크롤하거나 화면 안에 데이터가 보여지거나 특정 이벤트가 발생할 때 해당 콘텐츠를 비동기적으로 로드한다. 그래서 당장 화면에 보이지 않는 요소들의 로딩을 뒤로 미루고 보여지는 데이터만 우선적으로 로드해서 웹페이지의 로딩 퍼포먼스를 최적화하는 기술로, 초기 페이지 로드 시 사용자에게 불필요한 데이터나 자원을 제공하지 않고, 페이지 로딩 시간을 단축하여 사용자 경험을 향상시킬 수 있다. Lazy loading은 대개 이미지, 동적 컴포넌트, 혹은 데이터 요청과 같은 자원을 필요에 따라 비동기적으로 로드하는 데 사용된다.
Lazyloading의 원리로 사용자가 웹사이트에 방문한 후 페이지에 처음 접속한 시점에 페이지 로드 속도가 감소한다. 그리고 페이지의 초기 로딩 시 필요한 이미지의 수를 줄여 리소스의 요청을 줄이는 것은 다운로드 용량을 줄이는 것이기 때문에 이는 사용자가 사용할 수 있는 제한된 네트워크 대역폭의 경쟁을 줄이는 것을 의미, 대역폭을 절약할 수 있다. 이미지와 비디오를 그대로 로딩하지 않는다는 것은 웹사이트의 속도가 향상되는 이점이 발생한다.
요약하자면,
// LazyLoadingImage
import styled from "styled-components";
import useInfiniteScroll from "../hooks/use-infinitescroll"
import { useEffect, useState } from "react";
export default function LazyLoadingImage({
src,
alt,
onError,
onClick,
placeholderImg
}) {
const [isLoading, setIsLoading] = useState(true);
const target = useInfiniteScroll(handleIntersection);
useEffect(() => {
const image = new Image();
if (!isLoading) return
image.src = src;
image.onload = () => {
setIsLoading(false);
};
image.onerror = () => {
setIsLoading(true);
};
return () => {
image.onload = null;
image.onerror = null;
};
}, [src, isLoading]);
function handleIntersection(entries) {
const entry = entries[0];
if (entry.isIntersecting) {
setIsLoading(false);
}
}
return (
<LazyImage
className={isLoading ? 'loading' : 'loaded'}
src={isLoading ? placeholderImg : src}
loading="lazy"
alt={isLoading ? "" : alt}
onError={onError || null}
onClick={onClick}
ref={target}
/>
)
}
const LazyImage = styled.img`
display: block;
width: 100%;
height: 100%;
transition: all 0.5s;
&.loading {
filter: blur(10px);
clip-path: inset(0);
}
&.loaded {
filter: blur(0px);
}
`
const target = useInfiniteScroll(handleIntersection);
function handleIntersection(entries) {
const entry = entries[0];
if (entry.isIntersecting) {
setIsLoading(false);
}
}
hadleIntersection함수는 Intersection Observer API를 사용하여 관찰 대상이 화면에 들어오거나 나갈 때 호출되는 콜백함수이다. 이 콜백 함수는 Intersection Observer가 관찰하는 element의 상태 변화를 감지하고 그에 따라 특정 동작을 수행한다.
useInfiniteScroll훅은 handleIntersection 콜백 함수를 전달하여 사용자가 스크롤하여 이미지가 화면에 보일 때 호출된다.
- useInfiniteScroll훅 내부에서는 Intersection Observer를 생성하고 관찰할 대상을 설정한다. 이 때 사용자가 전달한 handleIntersection콜백 함수가 Intersection Observer의 콜백으로 등록된다.
- 사용자가 전달한 handleIntersection 콜백 함수는 Intersection Observer에 의해 관찰 대상이 화면에 들어올 때 호출된다.
- handleIntersection 콜백 함수는 entries라는 배열을 인자로 받는다. 이 배열은 화면에 들어온 element에 대한 정보를 담고있다.
- 일반적으로 Lazyloading 기능을 구현할 때는 첫 번째 엘리먼트만 관찰하면 된다. 따라서 entries배열의 첫 번재 요소를 가져와서 해당 element가 화면에 들어왔는지 확인한다.
- 만약 해당 element가 화면에 들어왔다면 (entry.isIntersection이 true인 경우)loading을 멈추고 img를 보여준다.
- usEffect의 의존성 배열에 src, isLoading을 포함시켜, src 또는 isLoading의 상태가 변경될 때마다 리렌더링이 되게 한다.
- image 객체를 생성한다. 이 객체는 로딩 상태를 확인하기 위해 사용된다.
- isLoading상태가 false인 경우에는 아무런 작업도 수행하지 않고 함수를 종료한다. 왜냐, 이미지의 로딩 상태가 false인 경우에는 이미지가 이미 로드되었거나 로드 중이라는 것을 의미. 이미지를 다시 로딩할 필요가 없기 때문이다.
- isLoading상태가 true인 경우, LazyLoadingImage 컴포넌트의 src props를 사용하여 해당 이미지의 주소를 지정해 이미지의 src 속성을 설정하여 이미지를 로딩한다. 이때 이미지가 로딩되면 onload콜백 함수가 호출된다. 이 함수에서는 이미지의 로딩 상태를 false로 변경한다.
- 만약 이미지 로딩에 실패하면 true로 변경하여 재시도할 수 있도록 한다.
- useEffect함수의 반환값으로 클린업 함수를 제공하는데 이 클린업 함수는 이전에 등록한 이벤트 handler를 제거한다. 이렇게 함으로써 불필요한 리소스가 메모리에 남지 않도록 메모리 누수를 방지하고 성능을 최적화할 수 있다.
// MainGrid
function MainGrid() {
return (
<Container>
{
list.map((p, i) => {
return <div key={p.product_id}>
<LazyLoadingImage
src={
// 'https://d2a0m4zl4hi5gz.cloudfront.net/dev/tumbler-mint.jpg?w=380&h=380'
`https://d2a0m4zl4hi5gz.cloudfront.net/dev/${(p.image).substring((p.image).lastIndexOf("/") + 1).split('_')[0]}.jpg?w=380&h=380`
// p.image
}
onError={(e: React.ChangeEvent<HTMLImageElement>) => {
e.target.onerror = null; // 에러 핸들러 무한 루프 방지
e.target.src = p.image // 이미지 로드 실패 시 p.image 사용
e.target.width = 380;
e.target.height = 380;
}}
alt={p.product_name}
onClick={() => navigate(`/detail/${p.product_id}`)}
placeholderImg={PlaceholderImg}
/>
</div>
})
}
{moreData ? <div ref={target}></div> : null}
</Container>
)
}
캐시 비우기 및 강력 새로고침 적용했을 때
새로고침
캐시 비우기 및 강력 새로고침 적용했을 때
새로고침
최종적으로,
전송된 데이터 양 (Transferred Data)
리소스 사용량 (Resource Usage)
Finish Time (로딩 완료 시간)
DOMContentLoaded
Load시간
전송된 데이터 양 (Transferred Data)
리소스 사용량 (Resource Usage)
Finish Time (로딩 완료 시간)
DOMContentLoaded
Load시간