본문 바로가기
SPA/REACT

리플로우가 일어나는 환경에서 React Suspense 사용해보기

by F.E.D 2022. 1. 18.
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

댓글