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 |
댓글