Lazy loading

Judo·2021년 7월 12일
0

프로젝트를 진행하다가 여러가지 이미지 파일로 인해 페이지 로딩이 느려지는 상황을 만났다.
이 문제를 해결하기 위해 lazy loading에 대해 다시 찾아봤고 새로운 점을 알게 되어 글을 작성한다. 이전에 알고 있던 lazy loading은 scroll event를 이용하여 해결한다고 알고 있었는데 이 방법은 몇가지 문제점을 갖고 있었다.

scroll 이벤트의 문제점

scroll 이벤트는 scroll 이벤트가 발생하면 짧은 시간에 수백번씩 호출되고 된다. 또한 scroll 이벤트뿐만 아니라 다른 이벤트가 존재할 경우 이벤트는 예상보다 더 많이 호출된다. 그리고 element의 위치를 알기 위해 getBoundingClientRect() 함수를 사용하는데 이 함수는 리플로우 현상을 유발하는 문제점을 갖고 있다.
위와 같은 문제점을 보완하기 위해 Intersection Observer가 나왔다는 사실을 알았다.

Intersection Observer

직역하면 교차 옵저버(!?)라고 할 수 있다. 옵저버의 역할은 관찰, 지켜보는 역할인데 말그대로 지정한 엘리먼트 사이의 교차하는 상황이 발생하면 옵저버가 지켜보고 있다가 알려준다는 것이다. 옵저버를 생성해보자.

const options = {
    root: null,
    threshold: 1,
};

const observer = new IntersectionObserver(callback, [options]);

옵저버는 모양만 가지고 만들어진다. 옵저버에게 지켜보다가 교차가 발생한 경우 취해야할 행동(callback)과 교차한다는 기준이 무엇인지(options)를 입력해줘야 한다.
callback은 두개의 인자를 받을 수 있는데 그 중 entries는 IntersectionObserverEntry 객체의 리스트 라고 한다. 쉽게 말하면 개발자는 옵저버에게 지켜볼 대상들을 지정할 수 있는데 지정을 하면 entries라는 명단에 관찰 대상으로 배열로 올라간다고 보면 된다. 관찰 대상 중 교차가 발생한 녀석만 잡으면 되니까
options는 옵저버에게 관할 구역을 지정해주는 개념이다. root는 dom요소를 입력할 수 있는데 입력하지 않거나 null을 입력한 경우 브라우저의 viewport가 자동으로 설정된다. threshold는 관할 구역에서 교차 범위가 어느정도 됐을 때 잡느냐를 지정해주는 것이다. [0, 0.5, 1]과 같이 배열로 설정해줘도 되고 숫자만 입력해도 되는데 0.5는 50% 관할 구역에 넘어왔다고 보면 된다.

function callback(entries, testObserver) {
	entries.forEach((entry) => {
    	// 교차된 엘리먼트가 entry로 들어온다. 이를 이용해 취해야할 행동을 입력해주면 된다.
    })
}

옵저버에게 지켜볼 대상을 지정해주는 방법

우리는 위에서 observer를 만들었다.
여기선 react의 ref의 개념을 알아야 하는데 이 개념은 직접 찾아보면서 적용해보면 해결하길 바란다.
useEffect를 이용해 렌더링 이후 img 엘리먼트의 명단을 다 작성한다. 이후 observer에게 img태그들을 보여주면 observe 시킨다.
imgList.forEach((img) => {observer.observe(img)});

useEffect(() => {
    const imgList = Array.from(containerRef.current.children);
    
    imgList.forEach((img) => {
      observer.observe(img);
    });
  });

  return (
    <div className="container" ref={containerRef}>
      <img className="img" ref={imgRef} src={logo} data-src={logo} />
      <img className="img" ref={imgRef} data-src={logo} />
      <img className="img" ref={imgRef} data-src={logo} />
      <img className="img" ref={imgRef} data-src={logo} />
      <img className="img" ref={imgRef} data-src={logo} />
      <img className="img" ref={imgRef} data-src={logo} />
      <img className="img" ref={imgRef} data-src={logo} />
      <img className="img" ref={imgRef} data-src={logo} />
      <img className="img" ref={imgRef} data-src={logo} />
      <img className="img" ref={imgRef} data-src={logo} />
      <img className="img" ref={imgRef} data-src={logo} />
    </div>
  );

교차한 시점은 어떻게 알 수 있을까요

위에서 말했다시피 옵저버는 이제 지켜볼 명단을 가지고 있다. 그리고 만약 교차되는 엘리먼트를 발견한 경우 if (entry.isIntersecting)를 통해 경고음이 발생하고 행동을 취할 수 있다.
여기선 사용자 지정 속성(dataset)을 이용하여 사진을 보여주지 않다가 구역에 넘어설 경우 src로 dataset-src값을 할당하여 사진을 보여주고 색깔과 기존에 blur처리도 제거해주는 행동을 취했다.

const observer = new IntersectionObserver((entries, testObserver) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        // 취할 행동
        entry.target.src = entry.target.dataset.src;
        entry.target.style.backgroundColor = "blue";
        entry.target.style.filter = "blur(0px)";
        observer.unobserve(entry.target);
      }
    });
  }, options);

전체 코드

function App() {
  const imgRef = useRef(null);
  const containerRef = useRef(null);
  const options = {
    root: null,
    threshold: 1,
  };
  const observer = new IntersectionObserver((entries, testObserver) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        entry.target.src = entry.target.dataset.src;
        entry.target.style.backgroundColor = "blue";
        entry.target.style.filter = "blur(0px)";
        observer.unobserve(entry.target);
      }
    });
  }, options);
  useEffect(() => {
    const imgList = Array.from(containerRef.current.children);
    imgList.forEach((img) => {
      observer.observe(img);
    });
  });

  return (
    <div className="container" ref={containerRef}>
      <img className="img" ref={imgRef} src={logo} data-src={logo} />
      <img className="img" ref={imgRef} data-src={logo} />
      <img className="img" ref={imgRef} data-src={logo} />
      <img className="img" ref={imgRef} data-src={logo} />
      <img className="img" ref={imgRef} data-src={logo} />
      <img className="img" ref={imgRef} data-src={logo} />
      <img className="img" ref={imgRef} data-src={logo} />
      <img className="img" ref={imgRef} data-src={logo} />
      <img className="img" ref={imgRef} data-src={logo} />
      <img className="img" ref={imgRef} data-src={logo} />
      <img className="img" ref={imgRef} data-src={logo} />
    </div>
  );
}

export default App;

profile
즐거운 코딩

0개의 댓글