덕풀 프로젝트가 성공적으로 끝나면서 리팩토링 해야 할 부분을 지속적으로 찾기 시작했다. 확실히 프로젝트를 실제로 사용하면서 이미지의 레이아웃 시프트, 이미지 리소스 용량 비대와 같은 문제점을 발견했고, 해당 부분을 좀 더 개선시켜야겠다고 생각했다.
홈 페이지를 기준으로 덕질자랑과 덕질토크의 최근 10개 게시물을 불러오도록 하고, 실제 유저에게는 swiper를 사용하여 덕질자랑과 덕질토크 게시물 각각 데스크탑 기준 5개, 4개 모바일 기준 3개, 2개의 이미지만 보여지게된다. 이 때 총 20개의 이미지 리소스를 모두 다운로드 받게되면 페이지 로딩 시간이 길어지는 문제가 발생했다.
이 부분을 Lazy Loading을 통해 성능적으로 많은 개선을 이루어서 해당 포스팅을 통해 소개해보고자 한다.
가장 먼저 이미지 Lazy Loading을 적용하기 전의 성능을 살펴보자.
Lazy Loading 구현 전
1️⃣ 홈 페이지
홈페이지의 네트워크 탭을 보면 최근 덕질 자랑, 덕질 토크의 각 게시물 10개를 불러오고, 각 게시물에 해당하는 총 20개의 이미지 리소스를 한꺼번에 로드하고 있어 DOMContentLoaded 시간은 190ms, 전체 페이지 로딩 시간은 1.29s로 다소 느린 성능을 보여주고 있다.
또한 실제 홈 페이지를 보면 레이아웃 시프트와 이미지가 보여지기 전까지 뚝뚝 끊기며 하얀색 화면이 보여지는 걸 볼 수 있다.
2️⃣ 덕질 자랑 페이지
덕질 자랑 페이지는 페이지 첫 진입 시에 유저의 덕질 자랑 게시물 20개를 불러오고 실제 가시권인 게시물 8개 이외의 모든 이미지 리소스를 다운로드 하면서 DOMContentLoaded 244ms, 전체 페이지 로딩 시간은 1.58s로 홈 페이지 보다 조금 더 느린 로딩 시간을 보여준다.
Lazy Loading 구현
Lazy Loading을 구현함으로써 위와 같이 보이지 않는 이미지 리소스의 다운로드를 줄여 페이지 로딩 시간을 감소 시킬 수 있고, 추가적으로 페이지를 이탈하거나 스크롤하지 않는 유저들에게 최소한의 리소스만 사용하도록 통신 비용을 감소시키는 효과 또한 줄 수 있다.
이번에 덕풀 프로젝트에서 Lazy Loading을 구현하기 위해 나는 Intersection Observer를 이용했다. 기존에 덕질 자랑과 덕질 토크 페이지에서 페이지 네이션을 사용하지 않고 추가 데이터를 불러오기 위해 무한 스크롤을 구현하면서 만들어놨던 useIntersecitonObserver 훅을 재사용했다. 확실히 이렇게 커스텀 훅을 만들어 놓으면 추가적인 기능을 개발하는데 엄청난 장점이 있는 것 같다.
import { useEffect, useRef } from 'react';
type IntersectionObserverProps = {
root?: null;
rootMargin?: string;
threshold?: number[];
onIntersect: IntersectionObserverCallback;
};
const useIntersectionObserver = ({
onIntersect,
root,
rootMargin = '0px',
threshold = [0.5],
}: IntersectionObserverProps) => {
const target = useRef<HTMLDivElement | null>(null);
useEffect(() => {
let observer: IntersectionObserver;
if (target) {
observer = new IntersectionObserver(onIntersect, {
root,
rootMargin,
threshold,
});
observer.observe(target.current as HTMLDivElement);
}
return () => observer && observer.disconnect();
}, [onIntersect]);
return { target };
};
export default useIntersectionObserver;
이 포스팅에서는 Intersection Observer API의 사용법에 대해서는 자세하게 다루지 않도록 하겠다. 그래도 위의 useIntersectionObserver 훅을 간단하게 소개하자면 관찰 대상 요소의 ref를 담은 target, Intersection Observer의 콜백 함수인 onIntersect를 통해 관찰 요소(target)를 참조하고, 해당 관찰 대상 요소를 관찰하면서 지정된 조건을 만족하면 onIntersect 콜백함수를 실행하도록 한다.
import useIntersectionObserver from '@hooks/useIntersectionObserver';
import { useCallback, useState } from 'react';
import styled from 'styled-components';
import placeholderImage from '@assets/images/placeholder-image.png';
type LazyImageProps = {
src: string;
alt: string;
};
const LazyImage = ({ src, alt }: LazyImageProps) => {
const [isVisible, setIsVisible] = useState(false);
const onIntersect = useCallback(
async (
[entry]: IntersectionObserverEntry[],
observer: IntersectionObserver,
) => {
if (entry.isIntersecting && !isVisible) {
observer.unobserve(entry.target);
setIsVisible(true);
observer.observe(entry.target);
}
},
[isVisible],
);
const { target } = useIntersectionObserver({
onIntersect,
});
return (
<StyledImgContainer ref={target}>
<StyledImage
alt={alt}
src={isVisible && src ? src : placeholderImage}
$isVisible={isVisible}
/>
</StyledImgContainer>
);
};
...
export default LazyImage;
LazyImage 컴포넌트는 위와 같다. isVisible을 통해서 이미지의 가시성 상태를 관리하고, target 요소가 Intersection Observer 옵션에 맞게 가시성이 확보되면 가시성 상태를 true로 바꿔준다. 따라서 Image의 경우 isVisible 상태와 src의 유무를 통해 이미지를 Lazy Loading 한다.
Lazy Loading 구현 후
1️⃣ 홈 페이지
Lazy Loading 구현 후 홈페이지 리소스를 보면 가시성이 확보된 이미지의 리소스만 다운로드 받고, 추가적으로 스크롤 후에 이미지가 보여지게되면 Lazy Loading을 통해 추가로 이미지를 다운로드 받게 된다. 따라서 적용 후의 DOMContentLoaded 시간은 85ms, 전체 페이지 로딩 시간은 602ms로 기존 로딩 시간보다 53% 개선된 것을 확인할 수 있다.
Lazy Loading이 적용된 화면을 보면 이미지가 다운로드가 완료되지 않았을 때 placeholder 이미지를 넣어주면서 확실히 레이아웃 시프트도 줄고 뚝뚝 끊기며 하얀색 화면이 보여지는 것을 개선할 수 있었다. 또한 swiper를 넘겼을 때 가시성이 확보된 이미지가 다운로드 되면서 정상적으로 잘 채워지는 것 또한 확인할 수 있다.
2️⃣ 덕질 자랑 페이지
덕질 자랑 페이지 또한 기존 DOMContentLoaded, Load 시간이 각각 244ms, 1.58s에서 84ms, 582ms로 전체 페이지 로딩 시간이 약 63%가 개선된 것을 확인할 수 있다.
위와 같이 덕질 자랑 페이지도 Lazy Loading을 적용하여 좀 더 부드럽고 빠른 이미지 로딩을 볼 수 있다.
이렇게 이번 포스팅은 당장 필요하지 않은 모든 이미지의 로딩을 잠시 지연시켜 페이지 초기 로딩 시간과 리소스를 절역할 수 있는 방법에 대해서 알아봤다. Lazy Loading의 적용 전과 후로 나누어서 개선된 성능을 수치로 확인했을 뿐만 아니라, 실제로 UI 적으로도 부드러운 느낌을 줄 수 있다는 것을 확인했다. 덕풀 프로젝트를 계속해서 리팩토링 해가는 과정을 담으려고 했는데, 다행히 이번에 유의미한 개선을 보여주면서 리팩토링을 진행할 수 있어서 다행이었다. 이후에도 서비스 성능을 개선하는 방안에 대해서 지속적으로 참고하고 반영하려고 노력해야겠다.
'개발' 카테고리의 다른 글
애플리케이션 에러처리 중앙화 (0) | 2024.03.14 |
---|---|
애플리케이션 유저 로그인 상태 및 인증 로직 관리 (feat. Jotai) (1) | 2024.03.11 |
promise를 사용한 전역 모달 관리 (feat. contextAPI) (0) | 2024.03.05 |
[개발] Nextjs에서 SEO 최적화하기 (0) | 2023.06.02 |
[개발] Nextjs Link 컴포넌트와 <a> 태그 (feat. 이메일 링크걸기) (0) | 2023.05.28 |