ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [스터디기록02/티코드(React)] 이미지 지연 로딩(Image Lazy Loading) 맛보기 - Intersection Observer API
    조각조각/티코드 스터디(React) 2024. 3. 5. 23:08
    🏔️ 티코드 스터디 소개
    - 작은 단위의 리액트 컴포넌트 기능을 페어프로그래밍으로 구현하는 스터디
    - 목표 : 프로그래밍 사고력 향상과 자신이 구현한 기능에 대해 명확한 설명을 할 수 있도록 연습하는 것

     

    👾  문제상황

    이미지 데이터를 대량으로 받아오는 페이지에 접속한다고 가정해 보자.

    예시처럼 좋은 화질의 이미지를 제공할수록 데이터의 용량은 커지고, 이미지를 받아오는 시간 또한 증가하게 된다.

    이에 따라 사용자는 이미지가 모두 받아와질 때까지 하염없이 빈 화면을 보며 기다려야 하는 상황을 마주하게 될 것이다.

     

    사용자가 보고있는 화면(뷰포트)에 보이는 이미지만 먼저 로드되어 보여진다면 사용자 경험을 더 향상시킬 수 있지 않을까? 이 때 사용할 수 있는 최적화 기법 중 하나로 이미지 지연 로딩(Image Lazy Loading)을 적용하면 좋다.

     

    📑  이미지 지연 로딩(Image Lazy Loading)이란?

    웹 페이지나 앱에서 이미지를 로드할 때, 사용자가 스크롤하거나 필요한 시점에 이미지를 로드하는 기술

     

    🧐  구현방법

    이미지 지연 로딩을 구현하는 방법은 Intersection Observer API, image placeholder 사용, react-lazy-load-image-component 라이브러리 사용 등 여러가지 방법이 존재한다. 오늘 스터디에서는 가장 기본적인 방법인 Observer API를 사용하여 구현하는 실습을 진행하면서 이미지 지연 로딩이 어떤 식으로 동작하는지 함께 알아보았다.

    Intersection Observer API란 브라우저에서 제공하는 API다. 이를 통해 해당 웹 페이지의 특정 요소를 관찰(observe)하게 되면 페이지를 스크롤 할 때, 해당 요소가 뷰포트 내에 들어왔는지 아닌지를 알려준다.

     

    Observer API를 생성하는 코드는 아래와 같다.

    intersection observer는 생성자를 호출하고, callback 함수를 전달하여 생성한다.

    let observer = new IntersectionObserver(callback, options);

    🖋️ IntersectionObserver() 생성자에 전달되는 options 객체는 observer의 콜백이 언제 호출되는지 제어할 수 있다. 

     

    이번 실습에서는 이미지가 뷰포트에 보이는 순간에 이미지를 로드하기 때문에 이미지를 관찰할 대상으로 잡고, Observer API를 적용했다. (코드는 핵심 기능만 첨부했다.)

     

    1) observer 선언하기

    // 1. observer와 ref 선언
    // 2. useEffect 안으로 observer 이동
    // 3. 이미지 ref에 current 값이 있으면 옵저버 설정
    // 4. observer를 등록하고 해지하는 로직 작성
    
    import { useEffect, useRef } from "react";
    
    const LazyImage = ({ src, alt, className, height }) => {
      const imgRef = useRef(null);
      const [isLoaded, setIsLoaded] = useState(false);
    
      useEffect(() => {
        let observer = new IntersectionObserver((entries) => {
          entries.forEach((entry) => {
            console.log(entry);
          });
        });
    
        if (imgRef?.current) {
          observer.observe(imgRef.current);
        }
    
        return () => {
          observer.disconnect();
        };
      }, [src]);
      
      return <img className={className} alt={alt} id={src} src={src} ref={imgRef} />
      };
      
      export default LazyImage;

     

    1) useEffect에 src(주소값)이 변경될 때마다 observer가 관찰될 수 있도록 의존성을 걸어준다.

    2) intersection observer 객체를 생성하면서, 콜백 함수를 전달한다. (이때 어떤 정보를 반환해주는지 확인하고자 콘솔로 반환 정보를 확인해봤다. 여기서 노출 여부를 알려주는 isIntersecting 속성을 사용하여 화면에 이미지 노출 여부를 적용시켜줄 예정이다.)

     

    3) ref를 통해 이미지 ref에 current 값이 있으면 관찰할 요소의 정보를 observe에 등록해준다.
    4) 스크롤이 될 때마다 관찰할 대상이 변경되도록 observer.disconnect()를 통해 관찰 대상을 제거해준다.

     

    2) 뷰포트 위치 확인하기

    // 4. load state 선언
    // 5. 이미지가 뷰포트 안에 들어오면 load 상태를 true로 변경 -> observer 구독 끊기
    
    import { useEffect, useRef, useState } from "react";
    
    const LazyImage = ({ src, alt, className, height }) => {
      const imgRef = useRef(null);
      const [isLoaded, setIsLoaded] = useState(false);
    
      useEffect(() => {
        let observer = new IntersectionObserver(
          (entries, observer) => {
            entries.forEach((entry) => {
              if (entry.isIntersecting) {
                imgRef.current.src = src;
                imgRef.current.onload = () => {
                  setIsLoaded(true);
                  observer.disconnect();
                };
              }
            });
          },
        );
    
        if (imgRef?.current) {
          observer.observe(imgRef.current);
        }
    
        return () => {
          observer.disconnect();
        };
      }, [src]);
      
        return <img className={className} alt={alt} id={src} src={src} ref={imgRef} />
      };
      
      export default LazyImage;

     

    3) 이미지 로드 전/후 보여줄 컴포넌트 설정하기

    // 6. load 상태값을 활용하여 placeholder img 태그 설정
    // 7. rootMargin을 설정하여 이미지 태그가 뷰포트에 보이기 전 설정한 px만큼 미리 로드되도록 설정
    
    import { useEffect, useRef, useState } from "react";
    
    const LazyImage = ({ src, alt, className, height }) => {
      const imgRef = useRef(null);
      const [isLoaded, setIsLoaded] = useState(false);
    
      useEffect(() => {
        let observer = new IntersectionObserver(
          (entries, observer) => {
            entries.forEach((entry) => {
              if (entry.isIntersecting) {
                imgRef.current.src = src;
                imgRef.current.onload = () => {
                  setIsLoaded(true);
                  observer.disconnect();
                };
              }
            });
          },
          {
            rootMargin: "100px",
          }
        );
    
        if (imgRef?.current) {
          observer.observe(imgRef.current);
        }
    
        return () => {
          observer.disconnect();
        };
      }, [src]);
    
      return isLoaded ? (
        <img className={className} alt={alt} id={src} src={src} />
      ) : (
        <img
          className={className}
          ref={imgRef}
          id={src}
          style={{
            height: `${height}px`,
            backgroundColor: "#b9d79c",
          }}
        />
      );
    };
    
    export default LazyImage;

     

    👀 placeholder image란?

    이미지가 로드되지 않았을 때 유저에게 로딩 중이라는 표시와 함께 이미지의 위치를 알려주는데 활용되는 대체 이미지

     

    🙌  결과

    처음 렌더링 될 때는 뷰포트에 보이는 이미지만 받아와지고, 스크롤을 내리면서 뷰포트에 노출되는 순간 이미지가 순차적으로 받아와지는 것을 확인할 수 있다. (연두색으로 보이는 박스는 placeholder image!)

    📚 라이브러리도 사용해보면서 어떤 기능을 추가로 사용할 수 있는지 비교해보는 것을 추후 숙제로 남겨둬야겠다.😁

     

     

     


    참고

    [mdn] Intersection Observer API(링크)

    [블로그] Intersection Observer 간단 정리하기 - 박성룡 ( Andrew park )(링크)

    [도서] 웹 개발 스킬을 한 단계 높여 주는 프론트엔드 성능 최적화 가이드

     

    💬 본 포스팅은 공부 기록용으로 정확하지 않은 정보가 존재할 수 있습니다. 발견하신다면 알려주세요!

Designed by Tistory.