[React] 이미지 성능 최적화 시키기 (Lazyloading)

김채운·2024년 1월 30일
0

React

목록 보기
22/26

Lazyloading이란?

Lazyloading이란 resource를 필요로할 때 비동기적으로 로드하는 전략으로, 페이지나 애플리케이션의 초기 로드 시 모든 콘텐츠를 동기적으로 로드하는 것이 아니라, 사용자가 스크롤하거나 화면 안에 데이터가 보여지거나 특정 이벤트가 발생할 때 해당 콘텐츠를 비동기적으로 로드한다. 그래서 당장 화면에 보이지 않는 요소들의 로딩을 뒤로 미루고 보여지는 데이터만 우선적으로 로드해서 웹페이지의 로딩 퍼포먼스를 최적화하는 기술로, 초기 페이지 로드 시 사용자에게 불필요한 데이터나 자원을 제공하지 않고, 페이지 로딩 시간을 단축하여 사용자 경험을 향상시킬 수 있다. Lazy loading은 대개 이미지, 동적 컴포넌트, 혹은 데이터 요청과 같은 자원을 필요에 따라 비동기적으로 로드하는 데 사용된다.

Lazyloading 사용의 장점

Lazyloading의 원리로 사용자가 웹사이트에 방문한 후 페이지에 처음 접속한 시점에 페이지 로드 속도가 감소한다. 그리고 페이지의 초기 로딩 시 필요한 이미지의 수를 줄여 리소스의 요청을 줄이는 것은 다운로드 용량을 줄이는 것이기 때문에 이는 사용자가 사용할 수 있는 제한된 네트워크 대역폭의 경쟁을 줄이는 것을 의미, 대역폭을 절약할 수 있다. 이미지와 비디오를 그대로 로딩하지 않는다는 것은 웹사이트의 속도가 향상되는 이점이 발생한다.
요약하자면,

  • 지연 로드는 이미지.비디오의 전송 최적화로 사용자의 웹페이지 접속 시간을 향상시킨다.
  • 일부의 파일만 전송하게 되어 사용자가 콘텐츠를 더 빨리 제공 받을 수 있다.
  • 빠른 로딩 속도로 웹사이트의 이탈 확률이 줄어든다.
  • 서버의 대역폭을 절감할 수 있다.

프로젝트에 Lazyloading 적용하기.

// LazyLoadingImage

import styled from "styled-components";
import useInfiniteScroll from "../hooks/use-infinitescroll"
import { useEffect, useState } from "react";

export default function LazyLoadingImage({ 
  src, 
  alt, 
  onError, 
  onClick, 
  placeholderImg 
}) {
    const [isLoading, setIsLoading] = useState(true);

    const target = useInfiniteScroll(handleIntersection);

    useEffect(() => {
        const image = new Image();
        if (!isLoading) return
        image.src = src;
        image.onload = () => {
            setIsLoading(false);
        };
        image.onerror = () => {
            setIsLoading(true);
        };

        return () => {
            image.onload = null;
            image.onerror = null;
        };
    }, [src, isLoading]);

    function handleIntersection(entries) {
        const entry = entries[0];
        if (entry.isIntersecting) {
            setIsLoading(false);
        }
    }

    return (
        <LazyImage
            className={isLoading ? 'loading' : 'loaded'}
            src={isLoading ? placeholderImg : src}
            loading="lazy"
            alt={isLoading ? "" : alt}
            onError={onError || null}
            onClick={onClick}
            ref={target}
        />

    )
}



const LazyImage = styled.img`
    display: block;
    width: 100%;
    height: 100%;
    transition: all 0.5s;

  &.loading {
    filter: blur(10px);
    clip-path: inset(0);
  }
  &.loaded {
    filter: blur(0px);
  }
`
 const target = useInfiniteScroll(handleIntersection);

 function handleIntersection(entries) {
        const entry = entries[0];
        if (entry.isIntersecting) {
            setIsLoading(false);
        }
    }
  • useInfiniteScroll (Intersection Observer)훅을 사용해 화면에 이미지가 나타날 때를 감지하도록 한다. 이 target은 LazyImage태그에 ref로 설정을 해둬서 LazyImage(img)요소를 가리키게 한다.
    이 훅은 handleIntersection 콜백 함수를 전달하여 사용자가 스크롤하여 이미지가 화면에 보일 때 호출된다.
    handleIntersection의 함수를 보면 entry.isIntersection이 true일 경우 setIsLoading(false)가 되도록 해놨다. 그래서 target요소가 화면에 보일 경우 loading상태가 false가 되도록 해 이미지가 로드되었음을 나타낸다.

과정 설명

hadleIntersection함수는 Intersection Observer API를 사용하여 관찰 대상이 화면에 들어오거나 나갈 때 호출되는 콜백함수이다. 이 콜백 함수는 Intersection Observer가 관찰하는 element의 상태 변화를 감지하고 그에 따라 특정 동작을 수행한다.

useInfiniteScroll훅은 handleIntersection 콜백 함수를 전달하여 사용자가 스크롤하여 이미지가 화면에 보일 때 호출된다.

  1. useInfiniteScroll훅 내부에서는 Intersection Observer를 생성하고 관찰할 대상을 설정한다. 이 때 사용자가 전달한 handleIntersection콜백 함수가 Intersection Observer의 콜백으로 등록된다.
  2. 사용자가 전달한 handleIntersection 콜백 함수는 Intersection Observer에 의해 관찰 대상이 화면에 들어올 때 호출된다.
  3. handleIntersection 콜백 함수는 entries라는 배열을 인자로 받는다. 이 배열은 화면에 들어온 element에 대한 정보를 담고있다.
  4. 일반적으로 Lazyloading 기능을 구현할 때는 첫 번째 엘리먼트만 관찰하면 된다. 따라서 entries배열의 첫 번재 요소를 가져와서 해당 element가 화면에 들어왔는지 확인한다.
  5. 만약 해당 element가 화면에 들어왔다면 (entry.isIntersection이 true인 경우)loading을 멈추고 img를 보여준다.

  • LazyLoadingImage컴포넌트의 이미지의 로딩 상태를 관리하기 위해 isLoading상태를 만든다. 초기 상태는 true로 설정.

  • useEffect 훅을 사용하여 이미지의 로딩 상태를 감지하고, 필요한 경우 이미지를 로드한다. isLoading 상태가 true이면 이미지를 로드하고, 이미지가 로드되면 isLoading 상태를 false로 업데이트합니다. 이미지 로드 중 에러가 발생하면 isLoading 상태를 다시 true로 설정합니다.

과정 설명

  1. usEffect의 의존성 배열에 src, isLoading을 포함시켜, src 또는 isLoading의 상태가 변경될 때마다 리렌더링이 되게 한다.
  2. image 객체를 생성한다. 이 객체는 로딩 상태를 확인하기 위해 사용된다.
  3. isLoading상태가 false인 경우에는 아무런 작업도 수행하지 않고 함수를 종료한다. 왜냐, 이미지의 로딩 상태가 false인 경우에는 이미지가 이미 로드되었거나 로드 중이라는 것을 의미. 이미지를 다시 로딩할 필요가 없기 때문이다.
  4. isLoading상태가 true인 경우, LazyLoadingImage 컴포넌트의 src props를 사용하여 해당 이미지의 주소를 지정해 이미지의 src 속성을 설정하여 이미지를 로딩한다. 이때 이미지가 로딩되면 onload콜백 함수가 호출된다. 이 함수에서는 이미지의 로딩 상태를 false로 변경한다.
  5. 만약 이미지 로딩에 실패하면 true로 변경하여 재시도할 수 있도록 한다.
  6. useEffect함수의 반환값으로 클린업 함수를 제공하는데 이 클린업 함수는 이전에 등록한 이벤트 handler를 제거한다. 이렇게 함으로써 불필요한 리소스가 메모리에 남지 않도록 메모리 누수를 방지하고 성능을 최적화할 수 있다.

  • LazyImage 컴포넌트는 isLoading 상태에 따라 로딩 중인 이미지에 대한 스타일을 적용한다. loading 클래스가 적용되면 이미지에 blur 효과와 clip-path가 적용되어 로딩 중인 것처럼 보이도록 합니다. loaded 클래스가 적용되면 blur 효과가 제거되어 이미지가 로드된 것처럼 보이게 됩니다.

컴포넌트 적용하기.

// MainGrid

function MainGrid() {
    return (
        <Container>
            {
                list.map((p, i) => {
                    return <div key={p.product_id}>
                        <LazyLoadingImage
                            src={
                                // 'https://d2a0m4zl4hi5gz.cloudfront.net/dev/tumbler-mint.jpg?w=380&h=380'
                                `https://d2a0m4zl4hi5gz.cloudfront.net/dev/${(p.image).substring((p.image).lastIndexOf("/") + 1).split('_')[0]}.jpg?w=380&h=380`
                                // p.image
                            }
                            onError={(e: React.ChangeEvent<HTMLImageElement>) => {
                                e.target.onerror = null; // 에러 핸들러 무한 루프 방지
                                e.target.src = p.image // 이미지 로드 실패 시 p.image 사용
                                e.target.width = 380;
                                e.target.height = 380;
                            }}
                            alt={p.product_name}
                            onClick={() => navigate(`/detail/${p.product_id}`)}
                            placeholderImg={PlaceholderImg}
                        />
                    </div>
                })
            }
            {moreData ? <div ref={target}></div> : null}
        </Container>
    )
}

➡️ Lazyloading 적용 결과

😥 Lazyloading 적용 전

캐시 비우기 및 강력 새로고침 적용했을 때

새로고침

😝 Lazyloading 적용 후

캐시 비우기 및 강력 새로고침 적용했을 때

새로고침

최종적으로,

✨ 캐시 비우기 및 강력 새로고침시

전송된 데이터 양 (Transferred Data)

  • 약 5.9MB 줄어듬.

리소스 사용량 (Resource Usage)

  • 약 5.9MB 줄어듬.

Finish Time (로딩 완료 시간)

  • 약 2.78초 단축됨.

DOMContentLoaded

  • 약 2.11초 단축됨.

Load시간

  • 약 2.11초 단축됨.

✨ 새로고침

전송된 데이터 양 (Transferred Data)

  • 거의 변경되지 않음.

리소스 사용량 (Resource Usage)

  • 약 5.9MB 줄어듬.

Finish Time (로딩 완료 시간)

  • 약 0.26초 단축됨.

DOMContentLoaded

  • 약 116ms 단축됨.

Load시간

  • 약 102ms 단축됨.

0개의 댓글