OTTER-LOG

리액트에서 image lazyload

#optimazation#react
by otter2022년 5월 13일에 최종수정되었습니다.
잘못된 내용이 있으면 댓글을 달아주세요.

사용자의 UX적 측면을 개선하기 위한 방법 중 하나로 lazyload가 있다. 간단하게 말하면 지금 당장 필요하지 않은 부분의 로딩을 지연시키는 것이다. 지금 필요하지 않은 부분을 렌더링 하지 않는 다는 것은 서버로부터 해당부분의 데이터를 전송받을 필요가 없다는 것이고 이를 통해 초기페이지 렌더링이 빨라지게 개선할 수 있다.

또 이런측면에서 서버의 비용 관점에서도 긍정적이다. 당장 필요하지 않은 부분이 렌더링 되지 않는다면 데이터 요청비용이 줄어들 것이다. 그러면 서버에서도 그 부분을 전송할 필요가 없기 때문이다.

이미지 레이지 로딩

이미지는 레이지 로드를 적용하기에 적절한 부분이다. 이미지는 js파일이나 여타 파일보다 큰 용량을 가지고 있고 이를 서버에서 내려받으려면 시간이 상대적으로 오래 걸릴 것이다. 만약 당장 필요하지 않은데 그것을 모두 다 내려받는 다고 생각해보면 시간이 엄청 오래걸릴 것이다.

이미지 레이지 로딩은 js에 있는 Intersection Observer API로 구현할 수 있다.

Intersection Observer API

그런데 Intersection Observer는 무엇인가?

let options = { root: document.querySelector("#scrollArea"), rootMargin: "0px", threshold: 1.0, }; let observer = new IntersectionObserver(callback, options);

js에서는 이렇게 observer를 선언해줄 수 있다. IntersectionObservercallbackoptions를 받아 작동하게 된다.

  • callback은 타겟엘리먼트가 교차되었을 때 실행할 콜백함수를 말한다.
  • options에서는 아래와 같은 옵션들을 적용할 수 있다.
let options = { root: document.querySelector("#scrollArea"), rootMargin: "0px", threshold: 1.0, };

옵션에는 이러한 속성이 있다.

  • root는 교차 영역의 기준이 될 root엘리먼트를 말한다. 어느 지점을 기준으로 교차되는지 확인할지 여기서 정하면 되는데 일반적으로 이미지 레이지로드를 사용할 때에는 veiwport를 기준으로 하므로 크게 신경쓰지 않아도 된다. 기본값이 null = viewport로 적용된다.
  • rootMargin은 css에서 margin값을 정하는 것과 똑같이 사용한다. root기준으로 하위에 있는 교차영역까지 observe하게 된다.
  • threshold는 교차영역의 진입 시점이다. 0은 진입할때 바로 시작되고, 1이면 교차되는 엘리먼트가 전부 보일때 실행된다. 이미지를 예로 들면 이미지에 접근하자마자, 또는 반쯤보일때를 기준할 수 있는 옵션이다.
let target = document.querySelector("#listItem"); observer.observe(target); // 이런 방식으로 observer가 observe하게 할 수 있다. 이제, target을 관찰한다. let callback = (entries, observer) => { entries.forEach((entry) => { // Each entry describes an intersection change for one observed // target element: // entry.boundingClientRect // 타겟 엘리먼트의 정보를 반환한다. // entry.intersectionRatio // threshold값을 반환한다.-타겟과 루트가 얼마나 교차되어 있는지 // entry.intersectionRect // 교차 영역의 정보를 반환한다. // entry.isIntersecting // 교차되었는지 여부를 t,f로 반환한다. // entry.rootBounds // 루트의 정보를 확인한다. // entry.target // 타겟 엘리먼트를 반환한다. // entry.time // 교차된 시간을 반환한다. }); }; // callback을 정의해줌으로써 접근했을때 그 entry에 이벤트를 적용할 수 있다. // entry는 현재 교차된 타겟들이다.

react에서 적용하기

const Card = ({ accommInfo }: { accommInfo: CardDataType }) => { const [isVisible, setIsVisible] = useState<boolean>(false); // 지금 현재 보이는지 여부를 상태로 두고 사용한다. const observer = useRef<IntersectionObserver>(); // ref에 intersection observer API를 담아 사용할 수 있다. const imgRef = useRef<HTMLImageElement>(null); useEffect(() => { observer.current = new IntersectionObserver(intersectionObserver); imgRef.current && observer.current.observe(imgRef.current); }, []); const intersectionObserver = ( entries: IntersectionObserverEntry[], io: IntersectionObserver, ) => { entries.forEach((entry) => { if (entry.isIntersecting) { // entry.isIntersection 메서드를 통해 // root와 targt이 교차되었는지 확인한다. io.unobserve(entry.target); // 교차되었으므로, 이제 target은 관찰하지 않는다. setIsVisible(true); // 상태를 visibie === true로 설정해준다. } }); }; return ( <CardContainer id={accommInfo.roomId} onClick={() => setIsReservationModalOpened(true)} > <CardImage src={isVisible ? accommInfo.imgSrc : undefined} alt='hotels' ref={imgRef} /> // 레이지로드를 할 타겟에 ref를 적용한다. 이게 타겟이 된다. // visible가 true일때만 // src를 등록하고 아니면 undefined를 넣는다. <CardText price={accommInfo.price} name={accommInfo.name} /> <HeartIcon /> {isReservationModalOpened && ( <ReservationModal accommInfo={accommInfo} closeModal={closeModal} /> )} </CardContainer> ); };

사용자 관점을 조금만 더 신경쓰기

이러한 방식을 통해 간단히 이미지 레이지로드를 구현할 수 있었다. 그런데, 옵션기능과 관련해서 한가지 더 신경쓸점이 있었다. 사용자 관점에서 현재 교차되는 이미지가 로딩되는 것보다는, 현재에는 보이지 않지만 어느정도 밑에 있는 이미지까지는 먼저 로딩해놓는 것이 좋지 않을까? 이렇게 한다면 사용자가 스크롤을 내릴때마다 이미지의 로딩을 기다릴 필요가 없을 것이다. 또한, 이를 통해 새롭게 요청하는 이미지의 개수도 3~4개라고 가정한다면 데이터를 요청받는데 추가되는 시간도 길지 않을 것이다.

이 부분을 실제로 구현해보지는 못했지만 optionsrootMargin속성을 사용해보면 구현이 가능할 것으로 예상된다.

잘 적용되었는지 확인해보기

네트워크 탭을 천천히 보다보면 확인할 수 있다. 왼쪽에 사진이 업로드 되기전에 잠깐 스켈레톤 UI가 나온다. 또 스크롤을 내릴때마다 cats로 시작되는 이미지를 다운로드 받고 있다.

ref

https://helloinyong.tistory.com/297https://developer.mozilla.org/ko/docs/Web/API/Intersection_Observer_API