ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 리플로우가 일어나는 환경에서 React Suspense 사용해보기
    SPA/REACT 2022. 1. 18. 01:22
    import React from "react";
    
    const imgCache = {
      __cache: {},
      read(src) {
        if (!src) {
          return;
        }
    
        if (!this.__cache[src]) {
          this.__cache[src] = new Promise((resolve) => {
            const img = new Image();
            img.onload = () => {
              this.__cache[src] = true;
              resolve(this.__cache[src]);
            };
            img.src = src;
            setTimeout(() => resolve({}), 7000);
          }).then((img) => {
            this.__cache[src] = true;
          });
        }
    
        if (this.__cache[src] instanceof Promise) {
          throw this.__cache[src];
        }
        return this.__cache[src];
      },
      clearImg: (src) => {
        delete this.__cache[src];
      }
    };
    
    export const SuspenseImg = ({ src, ...rest }) => {
      imgCache.read(src);
    
      return <img alt="" src={src} {...rest} />;
    };

    일반적으로 이미지를 캐싱하기 위한 완성된 코드는 위와 같습니다.

    일반적으로 이미지가 로드 되었을 때 imgCache.read(src)와 같이 쓰기 보다는 다음과 같이 생각하기 쉽습니다.

    const SuspenseImg = ({ src, ...rest }) => {
      throw new Promise((resolve) => {
        const img = new Image();
        img.onload = () => {
          resolve();
        };
      });
      return <img alt="" src={src} {...rest} />;
    };

    위와 같이 작성시에 문제점은 무엇일까요?

    그 문제점은 바로, 이미지가 렌더링될 때마다 Promise를 호출한다는 것입니다.

     

    그 문제점을 해결하기 위해서 read 메서드를 사용했습니다.

    const imgCache = {
      __cache: {},
      read(src) {
        if (!this.__cache[src]) {
          this.__cache[src] = new Promise((resolve) => {
            const img = new Image();
            img.onload = () => {
              this.__cache[src] = true;
              resolve(this.__cache[src]);
            };
            img.src = src;
          }).then((img) => {
            this.__cache[src] = true;
          });
        }
        if (this.__cache[src] instanceof Promise) {
          throw this.__cache[src];
        }
        return this.__cache[src];
      }
    };

    _cache라는 전역 변수 객체를 가지고 read 메서드에 src로 넘겨받은 이미 존재하는 이미지(caching된 이미지)가 있다면 바로 Pormise로 전달해야되는 인스턴스 상태라면 throw를 실행하고 아니면 바로 그 캐시된 이미지를 리턴해줍니다.

    그렇지 않고, 캐시된 이미지가 아니라면 프로미스로 onload과정을 거쳐서 this(즉, imgCache 객체)의 _cache[src]에 true라는 값을 할당해줌과 동시에 resolve 상태를 전달합니다. 

     

    이 정도로만도 훌륭하지만 Suspense를 사용해서 좀 더 완벽하게 만들어보시죠.

    import React, { Suspense, useState, useRef, useReducer } from "react";
    const { unstable_useTransition: useTransition } = React;
    
    import { useSuspenseQuery } from "micro-graphql-react";
    
    import FlowItems from "./layout/FlowItems";
    
    import Loading from "./ui/loading";
    import { SuspenseImg } from "./SuspenseImage";
    
    const GET_IMAGES_QUERY = `
    query HomeModuleBooks(
      $page: Int
    ) {
      allBooks(
        SORT: { title: 1 },
        PAGE: $page,
        PAGE_SIZE: 20
      ) {
        Books{
          smallImage
        }
      }
    }
    
    `;
    
    export default function App() {
      return (
        <Suspense fallback={<Loading />}>
          <ShowImages />
        </Suspense>
      );
    }
    
    const INITIAL_TIME = +new Date();
    
    function ShowImages() {
      const [page, setPage] = useState(1);
      const [cacheBuster, setCacheBuster] = useState(INITIAL_TIME);
      const [precacheImages, setPrecacheImages] = useState(true);
    
      const [startTransition, isPending] = useTransition({ timeoutMs: 10000 });
    
      const { data } = useSuspenseQuery(GET_IMAGES_QUERY, { page });
      const images = data.allBooks.Books.map(
        (b) => b.smallImage + `?cachebust=${cacheBuster}`
      );
    
      const onNext = () => {
        if (page < 20) {
          startTransition(() => {
            setPage((p) => p + 1);
          });
        } else {
          startTransition(() => {
            setCacheBuster(+new Date());
          });
        }
      };
    
      const togglePrecaching = (evt) => {
        setPrecacheImages((val) => evt.target.checked);
      };
    
      return (
        <div className="App">
          {isPending ? <Loading /> : null}
          <FlowItems>
            <button onClick={onNext} className="btn btn-xs btn-primary">
              Next images
            </button>
            <label style={{ display: "flex" }}>
              <span>Precache Images</span>
              <input
                defaultChecked={precacheImages}
                onChange={togglePrecaching}
                type="checkbox"
              />
            </label>
          </FlowItems>
          <FlowItems>
            {images.map((img) => (
              <div key={img}>
                {precacheImages ? (
                  <SuspenseImg alt="" src={img} />
                ) : (
                  <img alt="" src={img} />
                )}
              </div>
            ))}
          </FlowItems>
        </div>
      );
    }

    Suspense를 사용하면 특정 컴포넌트의 데이터의 준비가 아직 끝나지 않았음을 react에게 알릴 수 있고 이를 loading 화면을 띄우기 위해서 사용할 수 있습니다.

    위의 방법과 비교해서 가장 좋은 이유는 waterfall(네트워크 폭포수) 현상을 막아줍니다. 

    fetch를 사용해서 데이터를 호출하고 그것을 받아 데이터를 그리는 렌더링 과정을 거치는 순서가 일반적인데, 데이터를 fetching하는 과정 자체를 렌더링 이전에 받아낼 수 있는 것입니다.

    컴포넌트 렌더링과 관련없는 데이터렌더링이 가능하게 되는 것입니다.

    컴포넌트 렌더링 전에 데이터를 응답 받은 후 바인딩 하기 위해서 계속 데이터의 유무 체크를 해주던 때가 생각이 나는군요. : D

     

    감사합니다.

     

     

    출처 : https://css-tricks.com/pre-caching-image-with-react-suspense/?fbclid=IwAR3yHF0X-mqiqTP4KGTFnXXwy1VaTyT1CJCvQPKxKwxglzedotGdKRgm3ug

Designed by Tistory.