[프론트엔드 성능 최적화] (5)

yongkini ·2023년 1월 30일
0

이미지 및 동영상 크기 최적화 그리고 lazy-loading

lazy-loading

: 먼저 lazy-loading 말그대로 지연 로딩이라는건 왜 필요할까에 대해 말해보자. 어떤 홈페이지가 스크롤로 내리면서 다양한 콘텐츠를 볼 수 있도록 만든 페이지라고 해보자(예를 들어, 지그재그 홈페이지를 들 수 있다). 이런 페이지에서 스크롤을 내리면서 크기가 큰 사이즈의 이미지를 다운받아야 한다거나, 랜딩하자마자 보여지는 이미지 혹은 영상이 굉장히 큰 파일이어서 아직 보이지 않는(스크롤을 내려야 보이는 부분) 파일들의 다운로드 때문에 랜딩 하자마자 보이는 파일의 다운로드가 후순위가 되는 경우가 있을 수 있다. 이렇게되면 유저 입장에서는 랜딩 페이지에 들어가서 정작 보여야할건 보이지 않고, 스크롤을 내렸을 때 보이는 파일만 다운이 되는 상황이 있을 수 있다(다운이 될뿐 스크롤을 내리지 않는 이상 안보이는 것).

위와 같은 케이스를 막기 위해서는 일단 랜딩 하자마자 보이는 파일(이미지 or 동영상)을 1순위로 다운 받는 것이 좋다. 추가로, 유저가 랜딩 페이지에 들어가서 스크롤을 내리지 않고 다른 페이지로 넘어가는 경우가 있기 때문에 이 경우에는 굳이 스크롤을 내렸을 때 보이는 파일을 다운받을 필요가 없다. 스크롤을 내려야 보이는데 스크롤을 안내려보고 유저가 넘어갈 경우 쓸데없이 다운받은꼴(?)이 되기 때문이다.

결론적으로 위의 케이스를 막기 위해서는

  • 랜딩 하자마자 보이는 부분을 먼저 다운 받기 위해 나머지 부분의 다운을 후순위로 미룬다.
  • 후순위로 미룸과 동시에 스크롤을 안내려볼 경우를 대비하여 스크롤을 내려서 해당 파일을 다운받아야하는 케이스에만 다운받도록 한다.

위와 같은 2가지 방법으로 가능하다. 그러면 결국 위의 2가지 방법은 1가지로 통합할 수 있게 되는데, 이 때 필요한게 IntersectionObserver 이다(** react-lazyload 이라는 모듈도 있다).

IntersectionObserver ?

: 보통 IntersectionObserver를 떠올리면 infinite-scroll을 먼저 떠올리게 되는데, 여기서도 원리가 같기에 쓰인다고 생각하면 된다. 어떤 원리로 쓰이는지는 너무 당연하게도, img tag or img tag를 감싸는 div 태그를 observer로 observe 하고, 해당 부분이 viewport 상에서 노출이되면(해당 부분으로 스크롤로 이동하면) callback이 실행되도록 하면 된다(이 때, 이 callback에서 해당 이미지 파일을 다운로드 하도록 하면 된다).

  const imgRef = useRef(null);
  
  useEffect(() => {
    const options = {
    	threshold: 0,
        // 관찰하고자 하는 element가 어느정도 노출됐을 때 activate할 것인지를 결정하는 옵션으로 1이면 모두다 노출됐을 때, 0이면 1px이라도 노출 됐을 때 이런식으로 설정한다. 
        rootMargin: 10px,
        // 특정 요소에 닿았을 때 isIntersecting이 true가 될텐데 그 범위 기준에 있어서 예를 들어, 관찰하고자 하는 element의 크기를 넘어서서 200px 정도 전부터 체킹을 하고 싶다면 이 옵션에 px값을 넣어주면 된다.
        root : null,
        // root는 default가 null 이다. 이 때, 특정 div 내에서 scroll을 하고 싶은거라면 root에 특정 element 값을 넣어주면 된다. ex)  document.querySelector('#scrollArea')
    };
    // 
 
    const callback = (entries, observer) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          const target = entry.target;
          const previousSibling = target.previousSibling;

          target.src = target.dataset.src;
          previousSibling.srcset = previousSibling.dataset.srcset;
          observer.unobserve(entry.target);
        }
      });
    };
    const observer = new IntersectionObserver(callback, options);
    observer.observe(imgRef1.current);

    return () => observer.disconnect();
  }, []);

먼저 위와 같이

    const observer = new IntersectionObserver(callback, options);
    observer.observe(imgRef1.current);

useEffect 문 안에 observer를 새로 생성하고, observe해준다. componentDidMount 시점에 하는 것은 한번 observe하고 해당 스크롤 위치에 닿았을 때 이미지를 한번 로딩하고 더이상 observe 할 필요가 없기 때문이다.

이 때, observer 생성 시점에 callback에는

const callback = (entries, observer) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      const target = entry.target;
      target.src = target.dataset.src;
      observer.unobserve(entry.target);
    }
  });
};

위와 같은 로직이 들어간다. 그리고 위의 imgRef이 레퍼런싱 하는 태그는

<img ref={imgRef} data-src={props.image}  />

위와 같다. 여기서 data-src는 일종의 placholder image이다. 실제 이미지를 표시하기 전에 매우 작은 사이즈의 이미지를 먼저 보여주는 기법인데 이것을 Placeholder Image라고 한다. 예를들어 원본 이미지가 4000x2000이라면 이것을 40x20정도로 줄여서 먼저 표시하고 원본 이미지 로드가 완료되면 교체하는 방법을 쓴다. 이에 따라 해당 img tag에는 src 가 설정돼있지 않다. 단지 placeholder-image만 가지고 이미지가 로딩되면 해당 이미지 주소를 src로 쓰게 된다. 위와 같이 해놓으면 처음에는(랜딩 시에) 해당 이미지를 다운로드 하지 않고, observer의 범위내에 해당 부분이 들어오게 되면 그 때 이미지를 다운로드 한다. 이렇게 하면 아까 위에서 말했던 가장 먼저 보이는 이미지를 가장 먼저 다운로드 하도록 할 수 있고, 스크롤을 안하고 다음 페이지로 넘어가는 경우에 쓸데없는 리소스 다운로드도 막을 수 있다.

또다른 문제점?

: 위의 방법을 써서 깔끔하게 문제가 해결된 것 같았지만, 실상은 문제가 또 남아있다. 저 방법을 쓰면 유저 입장에서 랜딩을 하는 시점에 UX가 개선되지만, 스크롤을 내릴 경우 해당 이미지 or 동영상을 그 시점에 다운받기 때문에 로딩 시간에 인터넷이 느리다는 느낌을 받을 수 있다(미리 다운받아놓는 것과 달리 지연 로딩이기 때문에 당연한 결과라고 할 수 있다). 이에 따라 해당 사진 or 동영상의 파일 크기를 줄이는 방법으로 지연 로딩을 하더라도 UX 상 굉장히 빠른 속도로 다운을 받아서 렉같이 안보이도록 해보자.

그 전에 파일 포맷을 좀더 효율적인걸로 바꿔보자.

사이즈 측면 : PNG > JPG > WebP
화질 : PNG = WebP > JPG
호환성 : PNG = JPG > WebP

위와같이 포맷별로 장단점이 있다. JPG는 사이즈는 최대로 압축할 수 있지만, 그만큼의 정보 손실이 이뤄져 퀄이 낮아질 위험이 높고, PNG는 퀄이 낮아질 위험은 없지만 그만큼 압축률이 적다. 하지만 이 두가지 장점을 하이브리드로 만들어놓은 WebP는 브라우저별 호환성이 상대적으로 낮다는 단점이 있다.

여기서는 WebP로 압축을 한다고 생각하고, 호환성 문제도 해결을 해보자.

먼저, squoosh라는 사이트를 이용하면 이미지 포맷을 변환함과 동시에 resizing도 할 수 있다. squoosh를 통해 jpg 파일을 webP로 바꾸고, 이미지의 크기도 렌더링하려는 비율에 맞게 축소해보자. 그리고 그 webP 파일을 기존의 jpg로 교체하면 이미지의 파일 사이즈를 줄이는건 이미 성공이다.

그럼 앞서 말한 webp의 호환성 문제는 어떻게 해결할 수 있을까?. 이 경우에는 picture 태그를 사용해서 해결한다.

    <picture>
       <source data-srcset={props.webp} type="image/webp" />
       <img ref={imgRef} data-src={props.image} alt="long board features" />
     </picture>

위와 같이 해주면 picture 태그를 기점으로 가장 상단의 resource를 먼저 참조해보고, 이게 이 브라우저에서 호환이 안되는거면 다음으로 넘어간다. 위 경우에는 webp파일을 먼저 참조해보고 호환이 안되면 img 태그로 jpg 파일을 참조하도록 했다. 추가로 이렇게 하려면 아까 useEffect에서 하던 부분을 일부 수정해야한다.

 useEffect(() => {
    const options = {
    };
    const callback = (entries, observer) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          const target = entry.target;
          const previousSibling = target.previousSibling;
          target.src = target.dataset.src;
          previousSibling.srcset = previousSibling.dataset.srcset;
          observer.unobserve(entry.target);
        }
      });
    };
    const observer = new IntersectionObserver(callback, options);
    observer.observe(imgRef.current);
    return () => observer.disconnect();
  }, []);

이렇게 바꿔준다. 여기서 핵심은

target.src = target.dataset.src;
previousSibling.srcset = previousSibling.dataset.srcset;

이 부분이다. img tag와 source tag(img tag의 sibling 태그) 의 data-src, data-srcset에 각각 값을 넣어준다. 이 때 이렇게 둘다 해줘도 결국 picture 태그의 상위부터 해석하면서 값을 읽기 때문에 두 포맷의 파일을 모두 로딩하진 않는다(만약 webP 호환이 안되면 둘다 시도할테지만). 이런식으로 파일의 사이즈도 최적화하여 lazy-loading의 단점인 다운로드 속도 문제도 해결했다.

참고로 후순위인 jpg 이미지 파일 자체도 squoosh를 통해 resize는 진행해두는 것이 좋다(=cropping)

추가로, 동영상 최적화도 위 방법과 유사하게 media.io라는 플랫폼을 통해 가능하다. 이 때는 mp4 등의 파일을 webm 포맷으로 바꿔주면 된다. 그리고 동영상도 webM 자체가 모든 플랫폼에 호환되는게 아니라 이미지처럼 후순위를 설정해줘야 한다. 동영상의 경우

        <video
          style={{ filter: "blur(10px)" }}
          autoPlay
          loop
          muted
          className="absolute translateX--1/2 h-screen max-w-none min-w-screen -z-1 bg-black min-w-full min-h-screen"
        >
          <source src={video_wepm} type="video/webm" />
          <source src={video} type="video/mp4" />
        </video>

위와 같이 video 태그와 source 태그를 이용해서 만들어주면 된다. 이 때 video 태그의 경우는 아까와 달리 intersectionObserver 세팅을 안했기에 따로 data-src 등을 이용하지 않음을 주의하자(파일 크기 최적화만 한 코드임). 추가로, 동영상도 lazy-loading을 할 수 있다는 것 기억하자. 마지막으로 동영상을 압축하게 되면 화질이 저하되는데, 이 때 이 화질 저하를 커버하기 위해 blur 속성을 이용하는 등의 skill을 써서 화질이 낮지만 그게 렉처럼 보이지 않도록 할 수 있다. 물론 동영상이 굉장히 중요한 역할을 하는 플랫폼이라면 다른 방법을 찾아야한다(화질이 중요한 기획이라면).

profile
완벽함 보다는 최선의 결과를 위해 끊임없이 노력하는 개발자

0개의 댓글