사용자가 웹페이지를 열면 전체 페이지의 내용이 다운로드되어 단일 이동으로 렌더링 됨.
하지만 사용자는 다운로드한 모든 콘텐츠를 보는 게 아님.
원하는 부분만 잠깐 확인하고 페이지를 떠났을 때, 웹페이지에서는 메모리 및 대역폭 낭비 발생.
-> 페이지 엑세스 시 모든 콘텐츠를 한꺼번에 로드하는 대신, 사용자가 필요한 페이지 일부에 엑세스할 때만 콘텐츠를 로드하면 효율적
Lazy Loading 사용 시, 페이지가 placeholder 콘텐츠로 작성되며, 사용자가 필요할 때만 실제 콘텐츠로 대체됨.
이미지 로딩을 사전에 막기!
src 속성을 이용하면 이미지를 무조건 로드하므로, 대신 data-src 속성에 이미지 URL을 지정하면 src는 비워져 있고 브라우저는 해당 이미지를 로드하지 않음
-> 해당 이미지를 언제 로딩할 것인지 알려주어야 함
: 뷰포트에 들어오자마자 로딩해야함
=> 해당 이미지 로드가 완료되면 data-src에 있는 주소 값을 src값으로 세팅하고 data-src 속성은 삭제됨
어떻게?
scroll 이벤트 리스너 : 사용자가 페이지에서 스크롤하는 시점 확인 가능
resize 이벤트 리스너 : 윈도우 브라우저 크기가 바뀔때 발생
orientationChange 이벤트 리스너 : 디바이스 화면이 가로에서 세로 모드로 바뀔 때 발생
-> 모두 화면 내 보여지는 이미지 수가 바뀔 수 있으므로 이미지 로드하는 트리거가 필요할 수 있음
3개의 이벤트 중 하나가 발생할 때 뷰포트 안에서 로딩이 지연되었거나, 아직 로딩되지 않은 이미지들을 모두 찾음
-> 뷰포트 안에서 img 태그의 data-src 속성에 지정된 URL을 src 속성에 넣어서 이미지 로드
cf) scroll 이벤트는 스크롤 시 여러 이벤트가 급격히 발생하므로, 약간의 쓰로틀 기능을 이용해서 lazy loading 기능을 적용하는게 좋음. (캐러셀 공부할때도 나온 쓰로틀!)
document.addEventListener("DOMContentLoaded", function() {
var lazyloadImages = document.querySelectorAll("img.lazy");
var lazyloadThrottleTimeout;
function lazyload () {
if(lazyloadThrottleTimeout) {
clearTimeout(lazyloadThrottleTimeout);
}
lazyloadThrottleTimeout = setTimeout(function() {
var scrollTop = window.pageYOffset;
lazyloadImages.forEach(function(img) {
if(img.offsetTop < (window.innerHeight + scrollTop)) {
img.src = img.dataset.src;
img.classList.remove('lazy');
}
});
if(lazyloadImages.length == 0) {
document.removeEventListener("scroll", lazyload);
window.removeEventListener("resize", lazyload);
window.removeEventListener("orientationChange", lazyload);
}
}, 20);
}
document.addEventListener("scroll", lazyload);
window.addEventListener("resize", lazyload);
window.addEventListener("orientationChange", lazyload);
});
: 처음 3개 이미지는 미리 로딩하게 구현, data-src 속성대신 src 사용하여 더 빠름, timeout 때문에 이미지 로드에 약간의 딜레이 발생
엘리먼트 요소가 뷰포트에 들어가는 것을 감지하고 액션을 취하는 것을 간단하게 만들어줌.
JS 이벤트 방법은 엘리먼트가 뷰포트에 들어가는 것에 대해서 연산하는 것을 구현하고, 이벤트를 직접 바인드 시켰지만 Intersection Observer API는 그 부분을 쉽게 구현 가능!! 성능 면에서도 좋음
document.addEventListener("DOMContentLoaded", function() {
var lazyloadImages;
if ("IntersectionObserver" in window) {
lazyloadImages = document.querySelectorAll(".lazy");
var imageObserver = new IntersectionObserver(function(entries, observer) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
var image = entry.target;
image.src = image.dataset.src;
image.classList.remove("lazy");
imageObserver.unobserve(image);
}
});
});
lazyloadImages.forEach(function(image) {
imageObserver.observe(image);
});
} else {
var lazyloadThrottleTimeout;
lazyloadImages = document.querySelectorAll(".lazy");
function lazyload () {
if(lazyloadThrottleTimeout) {
clearTimeout(lazyloadThrottleTimeout);
}
lazyloadThrottleTimeout = setTimeout(function() {
var scrollTop = window.pageYOffset;
lazyloadImages.forEach(function(img) {
if(img.offsetTop < (window.innerHeight + scrollTop)) {
img.src = img.dataset.src;
img.classList.remove('lazy');
}
});
if(lazyloadImages.length == 0) {
document.removeEventListener("scroll", lazyload);
window.removeEventListener("resize", lazyload);
window.removeEventListener("orientationChange", lazyload);
}
}, 20);
}
document.addEventListener("scroll", lazyload);
window.addEventListener("resize", lazyload);
window.addEventListener("orientationChange", lazyload);
}
})
: 앞보다 로딩시간 훨씬 빠름, 스크롤 시 이미지가 느리게 나타나지 않음
(적용 안되는 브라우저가 있을 수 있음)
임베딩 할 이미지에 'loading' 속성만 추가해주면 됨
구현하는데 개발자가 필요없을 정도...
(적용 안되는 브라우저가 있을 수 있음)
로딩 지연된 이미지가 로드될 때 콘텐츠 밀림 방지하기
<img src="…" loading="lazy" alt="…" width="200" height="200">
<img src="…" loading="lazy" alt="…" style="height:200px; width:200px;">
사용자가 페이지에서 스크롤을 빠르게 할 시, 이미지가 로딩될 시간이 필요하다는 것이 placeholder로 보여지게 됨.
-> 이것은 이미지를 로딩하는 스크롤 이벤트를 성능 이슈로 인해 쓰로틀링을 사용해서 딜레이가 발생하게되는 것!
1)
이미지가 뷰포트에 완전히 들어올 때 로딩하는 대신, 뷰포트에 들어오는 부분에서 (예를 들면) 500px 정도 떨어진 곳에 들어오면 로딩을 하면 됨.
-> 로딩 트리거 시점을 더 빠르게 잡자는 의미
2)
Intersection Observer API에서는 'root'파라미터와 'rootMargin'파라미터(기본 CSS 마진 규칙에서 동작)를 함께 사용하여 'intersection'(이벤트가 발생되는 부분)을 찾는 위치를 증가시킬 수 있음
ex) 상단 3개의 이미지가 바로 보여질 때 5개의 이미지가 로딩, 4개의 이미지가 보여질 때 6개의 이미지가 로딩 -> 이미지가 완벽히 로딩되도록 충분히 시간을 가지게 함 -> 사용자가 placeholder를 볼일이 거의 없어짐
페이지 내 사용되는 모든 이미지에 lazy loadng 사용하는 것이 아님!
Lazy Loading에 어울리는 이미지 식별하는 기준
yall.js
lazysizes
JQuery Lazy
WeltPizel Lazy Loading Enhanced
Magento Lazy Image Loader
Shopify Lazy Image Plugin
Wordpress A3 Lazy Load 등
간단하게 크롬 브라우저 개발자 툴 이용
: 브라우저 내 네트워크 tab > image 확인
: waterfall column에서 이미지 로딩 타이밍도 알 수 있음, 이미지 로딩 트리거 관련 이슈사항이 있을 때 각 이미지 로딩에 대해 식별하는 것에 도움 줌
개인적으로 저희 프로젝트에는 Intersection Observer API 사용이 가장 좋아보입니다..!
대상 요소와 상위 요소 사이, 또는 최상위 document의 viewport 사이의 interaction 내 변화를 비동기적으로 관찰하는 방법
-> 화면에 내가 지정한 대상이 보이는지 관찰하는 API
저희 프로젝트에 적용한다면, 리스트를 뿌려주는 컴포넌트에 Interaction Observer을 적용시켜 현재 보이고있는(뷰 포트에 있는) 부분은 해당 리스트 컴포넌트를 로딩하고, 그 이외의 부분은 공용으로 사용되는 Loading 컴포넌트를 대신 보여주는게 어떨까요?

다른분이 API를 사용하여 구현한 코드를 참고하여 어떤 느낌으로 구현해야할지 작성한 코드입니다.
: Lazy Loading의 대표적 솔루션
: 사용자가 페이지로 스크롤할 때 지속적으로 로드
기본적인 방법인 Scroll Event를 감지해서 유저가 화면 제일 끝에 도달했을 때 아이템을 불러오게도 할 수 있지만, Intersection Observer API를 사용하여 무한 스크롤을 구현해보겠습니다.
let observer = new IntersectionObserver(callback, options);

- root
: 이 옵션에 정의된 element를 기준으로 Target Element가 노출되었는지, 노출되지 않았는지를 판단
: 기본값은 Browser Viewport, root 값이 null이나 지정되지 않았을 때 기본값으로 설정됨- rootMargin
: root에 정의된 element가 가진 margin값, threshold를 계산할때 rootMargin만큼 더 계산- threshold
: Target element가 root에 정의된 element에 얼만큼 노출되었을 때 콜백함수를 실행시킬지 정의하는 옵션
: number 또는 number[ ]로 정의
: number로 정의할 경우엔 Target element의 노출 비율에 따라 콜백함수를 한번 호출할 수 있지만, number[ ]로 정의할 경우, 각각의 비율로 노출될 때마다 콜백함수를 호출
import { useEffect, useState } from "react";
import styled from "styled-components";
import Item from '../lazyLoading/Item'
import Loader from '../lazyLoading/Loader'
const AppWrap = styled.div`
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
align-items: center;
`
const TargetElement = styled.div`
width: 100vw;
height: 140px;
display: flex;
justify-content: center;
text-align: center;
align-items: center;
`;
const InfiniteScroll = () => {
const [target, setTarget] = useState(null);
const [isLoaded, setIsLoaded] = useState(false);
const [itemLists, setItemLists] = useState([1]);
// useEffect(() => {
// console.log(itemLists);
// }, [itemLists]);
const getMoreItem = async () => {
//Loader 컴포넌트 보이게
setIsLoaded(true);
//1.5초 기다린 후 아이템 로드하여 비동기통신처럼 보임
await new Promise((resolve) => setTimeout(resolve, 1500));
//Item을 state로 만들어 로드해올때마다 10개씩
let Items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
//concat 함수를 이용해 ItemList에 이어붙임
setItemLists((itemLists) => itemLists.concat(Items));
//Loader 컴포넌트 안보이게
setIsLoaded(false);
};
const onIntersect = async ([entry], observer) => {
if (entry.isIntersecting && !isLoaded) {
observer.unobserve(entry.target);
await getMoreItem();
observer.observe(entry.target);
}
};
useEffect(() => {
//IntersectionObserver 담을 observer 변수 선언
let observer;
if (target) {
//IntersectionObserver 생성하여 observer에 담음
observer = new IntersectionObserver(onIntersect, {
threshold: 0.4,
});
//observer가 관찰할 대상(TargetElement) 지정
observer.observe(target);
}
//disconnect로 관찰요소 없애고 새로 지정
return () => observer && observer.disconnect();
//의존성 배열 : target 요소가 바뀌면(새 아이템 받아오면) target state가 변경
}, [target]);
return (
<AppWrap>
{itemLists.map((v, i) => {
return <Item number={i+1} key={i}/>;
})}
<TargetElement ref={setTarget}>
{isLoaded && <Loader/>}
</TargetElement>
</AppWrap>
);
};
export default InfiniteScroll;