-
리플로우가 일어나는 환경에서 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
'SPA > REACT' 카테고리의 다른 글
React memo를 사용하기 전에 (0) 2022.11.13 다크모드를 위해서 Context API보다 CSS 변수를 활용하세요 (0) 2022.10.30 transition에 대한 물리엔진 패키지 - react-spring (0) 2022.08.21 [React] useState의 초깃값 지연 (0) 2021.01.01 [React] React를 위한 렌더링 캐시 (0) 2019.01.27