지난 무한스크롤 기능과 이미지 로딩에 이어, 이미지 렌더링의 최적화를 연습해보기 위해 두가지의 페이지를 추가 구현 해 보았다.
우선, 이미지를 렌더링하는데 있어서 컴포넌트 혹은 이미지를 특정 시점에 로딩함으로써 불필요한 이미지 렌더링을 줄임으로 초기 로딩 시간을 대폭 줄일 수 있다.
물론 이미지를 효율적으로 렌더링 하는 방법은 다양하지만 (위의 방법 외에도 source와 picture 태그를 이용한 렌더링 등) 이미지를 받아오는 서버를 직접 만들지 않는점을 고려하여 우선은 두가지만 진행하였다.
첫번째 방법은 Chrome 75에서 소개한 Native Lazy Loading 방식으로, <img> 태그의 CSS property 중 loading에 lazy를 활용하는 것이다.
<img
alt="random_image"
key={randomImage.id}
src={randomImage.download_url}
style={{width: '500px', height: '500px'}}
loading="lazy"
/>
다음과 같이 loading property에 "lazy"를 활용하면, 해당 태그가 뷰포트에 올라갈 때 src의 이미지를 로딩하게 된다. 매우 간단하게 쓸 수 있게끔 만들어준 크롬 덕분에 개발자가 아니어도 쉽게 성능 향상을 할 수 있게 되었다.
조금 더 원초적인 방법의 두번째 방법은 Observer API를 활용하는 것 이다.
지난번에도 무한 스크롤을 이용하기 위해 Observer API를 ref에 부착한 것 처럼, 이번에도 이미지의 부모 div에 observer를 부착하였다.
이를 위해 이미지를 별도의 컴포넌트로 작성하고, 이미지 컴포넌트의 뷰포트 진입을 알려주는 Custom Hook을 작성하였다.
import { useRef, useState, useEffect } from 'react';
const useIsElementInViewport = (options?: IntersectionObserverInit) => {
const elementRef = useRef(null);
const [isVisible, setIsVisible] = useState(false);
const callback = (entries: IntersectionObserverEntry[]) => {
const [entry] = entries;
setIsVisible(entry.isIntersecting);
};
useEffect(() => {
if(isVisible){
return;
}
const observer = new IntersectionObserver(callback, options);
elementRef.current && observer.observe(elementRef.current);
return () => observer.disconnect();
}, [elementRef, options, isVisible]);
return { elementRef, isVisible };
};
export default useIsElementInViewport;
// ref https://velog.io/@syoung125/WIL-2021.11-1st
해당 커스텀 훅도 전과 같이 ref를 div에 부착하여 사용한다. 다른점이라면, boolean 값의 isVisible을 함께 리턴하는데, 해당 뷰포트 진입 시 isVisible값은 true가 된다. 언마운트 시점까지 이미지가 많다면 이미지 N개 만큼의 observer가 생성된 채로 살아있게 된다. 초기 이미지 로딩 감지를 위한 훅이기 때문에 뷰포트에 들어와 보여지게 되었을 때 (isVisible이 true가 되었을 때) 해당 훅을 언마운트하며 observer를 disconnect 해주었다.
//page 컴포넌트
...
return (
<div>
<h1>Observer API 이미지 로딩</h1>
{randomImageList.map((randomImage) => {
return(
<Image randomImage={randomImage}/>
)
})}
<div ref={ref}>
마지막 부분
</div>
</div>
)
//Image 컴포넌트
import React from "react";
import './index.css';
import useIsElementInViewport from '../../../hooks/useIsElementInViewport';
interface props {
randomImage: {
id: string,
download_url: string,
};
}
const Image = ({ randomImage }:props) =>{
const { elementRef, isVisible } = useIsElementInViewport();
return (
<div ref={elementRef} style={{width: '500px', height: '500px'}}>
{isVisible &&
<img
className="img"
alt="random_image"
key={randomImage.id}
src={randomImage.download_url}
style={{width: '500px', height: '500px'}}
/>}
<br/>
</div>
)
}
export default Image;
각 div에 부착 된 observer가 true로 변할 때, 이미지 태그를 렌더링한다. 현재는 클라이언트에게 보여지는 뷰포트 최 하단에 진입하였을 때 로딩하게 하였지만, useIsElementInViewport 훅의 prop인 option 값으로 rootMargin 값 (ex: '0px 0px 500px 0px')을 주면 뷰포트에 보이지 않아도 먼저 로딩 할 수 있다.
간단한 애니메이션 페이드 효과를 주어 UX 효과도 추가해보았다.
단순히 Lazy로딩을 이용 할 것이라면 시간적으로 그리고 성능적으로도 첫번 째 방법인 Native Lazy Loading을 사용하는 방법이 좋아보인다.
두번 째 방법을 사용하게 된다면 얻을 수 있는 이점은 아무래도 Customization인 것 같다. 어느 시점에 뷰포트에 들어오고, 뷰포트에 들어 올 때 어떤 이미지를 렌더링 할 것인지, 이미지 외에도 추가의 컨텐츠가 함께 렌더링 되는지 등 더욱 섬세한 터치가 가능해 보인다.
Lighthouse를 이용한 성능 측정 결과를 공유하며 마쳐보고자 한다.
'개발일지' 카테고리의 다른 글
[구글 애널리틱스: 01] Google Analytics 이해하기 (0) | 2022.08.21 |
---|---|
[데스크탑 애플리케이션: 01] Firebase + Nextron (Electron + Next.js) (4) | 2022.07.23 |
[이미지 렌더링: 01] 무한스크롤과 이미지 렌더링 (0) | 2022.07.01 |
[크롬 익스텐션: 06] StompJs & SockJs, 디자인 및 구현 - 2 (0) | 2022.06.23 |
[크롬 익스텐션: 05] StompJs & SockJs, 디자인 및 구현 - 1 (0) | 2022.06.23 |