Image lazy loading은 아직 화면에 보여지지 않은 이미지들은 로딩 시점을 뒤로 미루어 초기 로딩시간을 단축할 수 있는 웹 성능 최적화 기법.
위 이미지는 팔로우한 사용자들이 작성한 게시글들을 화면에 보여주는 홈 피드이다. 팔로우한 사람들이 점점 많아질수록 홈 피드에는 로드해야 될 게시글들 또한 많아져 로딩시간이 길어질 것이므로 최적화를 위해 Image Lazy Loading 을 사용하기로 하였다.
이미지가 로딩될 때 원본 이미지 대신 저화질의 이미지를 보여줌으로써 UX를 향상 시켰다.
Image lazy loading 기법은 홈피드에 로드되는 게시글 이미지 뿐만 아니라 클래스 이미지나, 유저 게시글 이미지에도 중복해서 사용할 수 있기 때문에 재사용성을 고려해
ProgressiveImg 컴포넌트를 만들고, 각 컴포넌트 내의 이미지에 적용시켜 주도록 설계하였다.
웹페이지의 특정 요소가 화면에 보이는지 아닌지를 감지하는 API인 react-intersection-observer 라이브러리를 이용하여 lazy-loading를 구현하였다.
//ProgressiveImg.jsx
import React, { useEffect, useState } from 'react';
import { Img } from './ProgressiveImgStyle';
import { useInView } from 'react-intersection-observer';
import NoImg from '../../../assets/img/No_Image_Available.jpg'; //원본 이미지 로드 실패시 보여줄 이미지
export default function ProgressiveImg({
placeholderSrc,
src,
styles,
...props
}) {
const [imgSrc, setImgSrc] = useState(placeholderSrc || src);
const [isLazy, setIsLazy] = useState(true);
const { ref, inView } = useInView();
const customClass = isLazy ? 'loading' : 'loaded';
useEffect(() => {
if (inView && imgSrc === placeholderSrc) {
const img = new Image();
img.src = src;
img.onload = () => {
setImgSrc(src);
setIsLazy(false);
};
img.onerror = () => {
setImgSrc(NoImg);
setIsLazy(false);
};
}
}, [src, inView]);
return (
<Img
{...{ src: imgSrc, ...props }}
className={customClass}
style={styles}
loading='lazy'
alt={props.alt || ''}
ref={ref}
/>
);
}
import { useInView } from 'react-intersection-observer';
const { ref, inView } = useInView();
react-intersection-observeruse 라이브러리에서 제공하는 InView hook을 사용하여 이미지 요소가 화면에 보이는지 여부를 감지한다. 화면에 보이는 경우, inView는 true가 된다.
ProgressvieImg Props
placeholderSrc
: 원본 이미지가 로딩되기전 보여줄 저화질의 이미지 url src
: 실제 로드할 원본 이미지의 URL...props
: 기타 이미지들에 전달할 props상태 관리
imgSrc
: 현재 보여줄 이미지의 URL이며 초기값은 placeholderSrc 또는 src로 설정
isLazy
: 이미지가 lazy-loading 중인지의 상태를 나타냄. 초기값은 true로 설정, 이미지가 로드되면 false로 변경되고 이미지 블러처리를 해제
const img = new Image();
img.src = src;
img.onload = () => {
setImgSrc(src);
setIsLazy(false);
};
img.onerror = () => {
console.log('이미지 로드 실패:', src);
setImgSrc(NoImg);
setIsLazy(false);
};
Image 객체를 생성하여 원본 이미지를 로드합니다. onload
이벤트는 이미지 로드가 성공적으로 완료되었을 때 setImgSrc에 원본 이미지 src를, onerror
이벤트는 이미지 로드에 실패했을 보여줄 이미지 src를 설정한다.
//ProgressiveImgStyle.jsx
import styled from 'styled-components';
export const Img = styled.img`
transition: all 0.5s;
&.loading {
filter: blur(10px);
clip-path: inset(0);
}
&.loaded {
filter: blur(0px);
}
`;
export const Placeholder = styled.div`
width: 100%;
height: 100%;
background-color: ${(props) => props.color || '#eee'};
`;
저화질 이미지 블러 설정/해제는 ProgressiveImgStyle에서 작성했다.
이제 위에서 만든 ProgressiveImg 컴포넌트를 작성 게시물 컴포넌트인 PostContent에 적용해보자
//PostContent에 필요한 Import
import ProgressiveImg from '../ProgressiveImg/ProgressiveImg';
import PlaceholderImg from '../../../assets/img/placeholderImg.svg'; // 로딩 전에 보여줄 저화질 이미지
<img
key={index}
src={HandleNormalizeImage(postImage)}
width={postImages.length > 1 ? '168px' : '304px'}
alt=''
/>
<ProgressiveImg
key={index}
src={HandleNormalizeImage(postImage)} // // 로딩 되면 보여줄 원본 이미지
width={postImages.length > 1 ? '168px' : '304px'}
alt='게시글 이미지'
placeholderSrc={PlaceholderImg} // 로딩 전에 보여줄 저화질 이미지
/>
개발자 전용 도구 네트워크탭에서 웹 페이지를 로드하는 동안 실제로 네트워크를 통해 전송된 데이터의 양 비교를 해본 결과
이 값들이 크면 페이지 로드 시간이 늘어날 수 있으므로, 최적화 작업에서 이 값을 최소화하는 것이 중요합니다.
이미지 전송량: 9.4MB => 14.7kb ( 99.85% 축소)
최적화 전 로딩속도 : 5.37초 => 3초(2.37초 단축)
페이지 로딩시간이 단축된 것을 확인할 수 있었다.