react에서 image Lazyload

otter·2022년 6월 12일
0

Overview

사용자의 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이면 교차되는 엘리먼트가 전부 보일때 실행된다. 이미지를 예로 들면 이미지에 접근하자마자, 또는 반쯤보일때를 기준할 수 있는 옵션이다.

일단 사용해보니, options는 크게 신경안쓰고 해도 일반적으로 - 뷰포트에 보일때 로드하기 - 는 성공할 수 있었다.

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 image lazyload

const Card = ({ accommInfo }: { accommInfo: CardDataType }) => {
  const [isVisible, setIsVisible] = useState<boolean>(false);
	// 지금 현재 보이는지 여부를 상태로 두고 사용한다.

  const observer = useRef<IntersectionObserver>();
  const imgRef = useRef<HTMLImageElement>(null);
	// 여기는 단순히 ref를 선언해 주는 곳
  useLayoutEffect(() => {
    observer.current = new IntersectionObserver(intersectionObserver);
    imgRef.current && observer.current.observe(imgRef.current);
    // 여기서 observer.current에 new IntersectionObserver
  }, []);
	// 동기적으로 current객체에 observer과 imgRef를 등록한다.
  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속성을 사용해 볼 수 있다.
그런데 문제는, 이 Card 컴포넌트는 내에서 적용할 수는 없다. root를 기준으로 옵져버가 작동한다는 것을 생각해보면 여기에는 root로 삼을만한게 없다. 아래에 올린 gif를 보자면, 카드들의 윗부분에 어떠한 컨테이너를 root의 기준으로 삼고 그에대한 아래 마진값으로 구성해야 할것이다.

지금은 이미 구현해놓았던 프로젝트를 기반으로, 글을 작성중이라 이런 부분은 고려하지 못했었는데 고려할 수 있으면 고려하는 게 좋을 것 같다. 그 부분이 하필이면 데이터를 fetch받아서 렌더링 되는부분이라 lazy로 막아두어서 구현하기가 힘들 것 같다.

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

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

ref

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

profile
http://otter-log.world 로 이사했어요!

0개의 댓글