๐ [React] React16๊ณผ Suspense, ์ง์ ๊ตฌํํด๋ณด๊ธฐ 2
๋ฆฌํฉํ ๋ง
์ด์ ๋ด๋ถ ๊ตฌํ์ ์๋ฃํ์ผ๋, ์ด ์ ํธ์ ์ข ๋ ์ฝ๊ฒ ์ฌ์ฉํ ์ ์๋๋ก ํ ์ผ๋ก ๋ง๋ค์ด๋ณด์.
- Promise์ status์ result(error), suspender๋ฅผ ํ๋๋ก ๋ฌถ์ด map ์๋ฃ๊ตฌ์กฐ์ ์ ์ฅ
- createCustomPromise ๋ฅผ ํตํด ์ ์ ๋ณด๋ค์ ๊ตฌ์กฐํ
- useFetch ์์๋ customPromise์ ์ํ์ ๋ฐ๋ผ suspender๋ฅผ ๋์ง์ง(pending), ์๋ฌ๋ฅผ ๋์ง์ง(rejected), ๊ฒฐ๊ณผ๋ฅผ ๋ฐํํ ์ง(fulfilled) ๊ฒฐ์ ํ๋ค.
- ์ด ๋ suspender๋ฅผ ๋์ง๊ฒ ๋๋ฉด pending์ด Suspense์ ๊ฐํ๊ฒ ๋๋๋ฐ, ์ด๋ฅผ ๊ตฌ๋ถํ๊ธฐ ์ํด status์ ์ด๊น๊ฐ์ null๋ก ์ง์ ํ๋ค.
- ์ด๋ฅผ ํตํด ์ฒ์์๋ suspender๋ฅผ ๋์ง๊ณ , ์ดํ์ status๊ฐ pending์ด ๊ฐ์ง๋๋ ๊ฒฝ์ฐ๋ Suspense๊ฐ pending์ ์ ๋๋ก ๊ฐ๋์ง ๋ชปํ ์ํฉ์ผ๋ก ํ๋จํ๊ณ ์๋ฌ๋ฅผ ๋์ง๋ค.
const PROMISE_STATUS = {
PENDING: "pending",
SUCCESS: "fulfilled",
FAIL: "rejected",
};
const promiseStoreMap = new Map();
const createCustomPromise = (asyncFunction) => {
const customPromise = {
status: null,
result: null,
error: null,
};
const suspender = async () => {
// ๊ธฐ์กด suspender ๋ถ๋ถ
try {
customPromise.status = PROMISE_STATUS.PENDING;
customPromise.result = await asyncFunction();
customPromise.status = PROMISE_STATUS.SUCCESS;
} catch (err) {
customPromise.status = PROMISE_STATUS.FAIL;
customPromise.error = err;
}
};
customPromise.run = suspender;
return customPromise;
};
function useFetch(key, asyncFunction) {
if (!promiseStoreMap.has(key)) {
promiseStoreMap.set(key, createCustomPromise(asyncFunction));
}
const customPromise = promiseStoreMap.get(key);
if (customPromise.status === null) {
throw customPromise.run(); // suspender
} else if (customPromise.status === PROMISE_STATUS.FAIL) {
throw customPromise.error;
}
if (customPromise.stauts === PROMISE_STATUS.PENDING) {
// status์ ์ด๊ธฐ ์ํ๋ null์ด๊ณ ์ดํ์ pending์ผ๋ก ๋ฐ๋ ๋ค, fulfilled ํน์ rejected๊ฐ ๋๋ค.
// pending์ผ ๊ฒฝ์ฐ suspense์ ๊ฐํ๊ฒ ๋ ํ
๋ฐ, ์ฌ๊ธฐ์ ๊ฐ์ง๋ ๊ฒฝ์ฐ๋ suspense์ ๋์์ด ์๋ชป๋ ๊ฒ
throw new Error("Suspense Error");
}
return { data: customPromise.result };
}
export default useFetch;
QueryKey๋ฅผ ๊ณ ๋ํ ํ ์ ์์ง ์์๊น?
react-query์ ๊ฐ์ด queryKey๋ฅผ ๋ฐฐ์ด๋ก ๋๊ธด๋ค๋๊ฐ, ์บ์ฑ ๋ฑ์ ๊ตฌํํ ์ ์์ง ์์๊น์ ๋ํ ์๊ฐ์ด ์ค์ณ ์ง๋๊ฐ๋ค.
์ฐ์ ์ฒซ ๋ฒ์งธ๋ก queryKey๋ฅผ ๋ฐฐ์ด๋ก ๋๊ธธ ์ ์๋๋ก ๋ณ๊ฒฝํ๋ค.
์ด๋ฅผ ์ํด์ map.has()
์ map.get()
์ key ๊ฐ ๋ฐฐ์ด์ผ ๊ฒฝ์ฐ์๋ ์ฌ์ฉํ ์ ์๋๋ก ๋ณ๊ฒฝ์ด ํ์ํ๋ค. lodash์ isEqual์ ํตํด ๋ฐฐ์ด์ด ๊ฐ์ ๋ฐฐ์ด์ธ์ง ๋น๊ตํ๊ณ ํ์ํ ๊ฐ์ ๋ฐํํ๋ค.
const hasKey = (key) => {
let isExist = false;
promiseStoreMap.forEach((value, _key) => {
if (_isEqual(_key, key)) {
isExist = true;
}
});
return isExist;
};
const getByKey = (key) => {
let mapValue = null;
promiseStoreMap.forEach((value, _key) => {
if (_isEqual(_key, key)) {
mapValue = value;
}
});
return mapValue;
};
function UserListAxios() {
const ids = [1, 1, 3, 4, 5, 6];
return (
<div>
<h3>์ ์ ํ๋กํ ๋ฆฌ์คํธ์
๋๋ค.</h3>
{ids.map((id) => {
return (
<div key={id}>
<AsyncBoundary
pendingFallback={<div>๋ก๋ฉ์ค...</div>}
rejectedFallback={(e) => <div>์๋ฌ ๋ฐ์!!!{e.message}</div>}
>
<UserItemAxios id={id} />
</AsyncBoundary>
<hr />
</div>
);
})}
</div>
);
}
export default UserListAxios;
์ด ๊ฒฝ์ฐ์ ๋์ผํ ์ฟผ๋ฆฌ ํค๋ฅผ ๋๊ฒจ๋ฐ์ ๊ฒฝ์ฐ ์๋ ํ๋ฉด๊ณผ ๊ฐ์ด ์๋ฌ๊ฐ ๋ฐ์ํด๋ฒ๋ฆฌ๋๋ฐ,
์ฐ๋ฆฌ๋ ๋์ผํ ์ฟผ๋ฆฌ ํค๊ฐ ๋์ด์์ ๋๋ ์ด์ ์ ์บ์ฑ๋ ๊ฐ์ ๋ฐํํ๋๋ก ๋ณ๊ฒฝํด์ค๊ฒ์ด๋ค.
์๋ฌ๋ ์ ๋ฐ์ํ ๊น?
useFetch ํ๋จ์ ์ถ๊ฐํ ๋ฐ๋ก ์๋ ๋ถ๋ถ์ ๊ฑธ๋ ค์ ์๋ฌ๋ฅผ throw ํ๊ฒ ๋ ๊ฒ์ธ๋ฐ, ๋์ผํ queryKey๋ก ํธ์ถํ์ ๊ฒฝ์ฐ ๋ ๋ฒ์งธ๋ ๋น์ฐํ ์ฒซ ๋ฒ์งธ์์ ๋์ง suspender๊ฐ ์๊ธฐ ๋๋ฌธ์ pending ์ํ๊ฐ ๋๋ค.
if (customPromise.stauts === PROMISE_STATUS.PENDING) {
// status์ ์ด๊ธฐ ์ํ๋ null์ด๊ณ ์ดํ์ pending์ผ๋ก ๋ฐ๋ ๋ค, fulfilled ํน์ rejected๊ฐ ๋๋ค.
// pending์ผ ๊ฒฝ์ฐ suspense์ ๊ฐํ๊ฒ ๋ ํ
๋ฐ, ์ฌ๊ธฐ์ ๊ฐ์ง๋ ๊ฒฝ์ฐ๋ suspense์ ๋์์ด ์๋ชป๋ ๊ฒ
throw new Error("Suspense Error");
}
๋ฐ๋ผ์ ํด๋น ์กฐ๊ฑด๋ฌธ์ ๋ค์ด์์ ๋ ์ด๋ฏธ ๋์ ธ์ง suspender๊ฐ ์๋ค๋ฉด ๋ฐ๋ก ๊ทธ suspender๋ฅผ ๋๊ฐ์ด ๋์ง๋ฉด ๋๋ค.
์ด ๋ ์ฃผ์ํด์ผ ํ๋ ๋ถ๋ถ์ pending์ผ ๋ throw ํ๋ suspender๋ ์ด์ ์ ๋์ง suspender์ฌ์ผ ํ๋ค๋ ์ ์ด๋ค. throw customPromise.run()
์ ํด๋ฒ๋ฆฌ๋ฉด ์๋ก์ด proimse๊ฐ ์คํ๋๊ธฐ ๋๋ฌธ์ network ํญ์์ ๋ณด๋ฉด ๋ถํ์ํ๊ฒ ์์ฒญ์ด ํ ๋ฒ ๋ ๊ฐ๋ ๊ฒ์ ํ์ธํ ์ ์๋ค.
์์ ํ ์ฝ๋๋ ์๋์ ๊ฐ๋ค.
if (customPromise.status === null) {
// ๊ฐ์ queryKey ๊ฐ์ผ๋ก ๋ถ๋ ธ์ ๊ฒฝ์ฐ, ๊ฐ์ promise๋ฅผ ๋ฐ๋ผ๋ณผ ์ ์๋๋ก cache์ ์คํํ suspender(.run())๋ฅผ ๋ด์์ค๋ค.
customPromise.cache = customPromise.run(); // ์ถ๊ฐ๋ ๋ถ๋ถ
throw customPromise.run(); // suspender
} else if (customPromise.status === PROMISE_STATUS.FAIL) {
throw customPromise.error;
}
if (customPromise.status === PROMISE_STATUS.PENDING) {
if (customPromise.cache) {
// ์บ์๋ ๊ฐ์ด ์์ ๊ฒฝ์ฐ ํด๋น suspender๋ฅผ ๋ค์ ๋์ง๊ณ ์ด์ธ์ ๊ฒฝ์ฐ์๋ ์๋ฌ๋ก ํ๋จ
throw customPromise.cache; // ์ถ๊ฐ๋ ๋ถ๋ถ
}
throw new Error("Suspense Error");
}
๋ค๋ฅธ ์์ ์ฌํญ์ผ๋ก๋ ๋ฐฐ์ด๋ก ๋ฐ์ queryKey๋ฅผ ๊ทธ๋๋ก map ์ key ๋ก ์ฌ์ฉํ์ง ์๊ณ JSON.stringify๋ฅผ ํด์ string ํ์
์ key๋ก ์ธ ์ ์๊ฒ ๋ณ๊ฒฝํ๋ค. (๋ฐฐ์ด์ key๋ก ์ธ ๊ฒฝ์ฐ, set, get, has
๋ฑ์์ ์ํ๋๋๋ก ๋์ํ์ง ์์ ๋ฐ๋ก ๋ง๋ค์ด ์คฌ์ด์ผ ํ์)
์์ฑ๋ ์ฝ๋๋ ์๋์ ๊ฐ๋ค!
const PROMISE_STATUS = {
PENDING: "pending",
SUCCESS: "fulfilled",
FAIL: "rejected",
};
const promiseStoreMap = new Map();
window.promiseMap = promiseStoreMap;
const createCustomPromise = (asyncFunction) => {
const customPromise = {
status: null,
result: null,
error: null,
cache: null,
};
const suspender = async () => {
// ๊ธฐ์กด suspender ๋ถ๋ถ
try {
customPromise.status = PROMISE_STATUS.PENDING;
customPromise.result = await asyncFunction();
customPromise.status = PROMISE_STATUS.SUCCESS;
} catch (err) {
customPromise.status = PROMISE_STATUS.FAIL;
customPromise.error = err;
}
};
customPromise.run = suspender;
return customPromise;
};
function useFetch(key, asyncFunction) {
const parsedKey = JSON.stringify(key);
if (!promiseStoreMap.has(parsedKey)) {
promiseStoreMap.set(parsedKey, createCustomPromise(asyncFunction));
}
const customPromise = promiseStoreMap.get(parsedKey);
if (customPromise.status === null) {
// ๊ฐ์ queryKey ๊ฐ์ผ๋ก ๋ถ๋ ธ์ ๊ฒฝ์ฐ, ๊ฐ์ promise๋ฅผ ๋ฐ๋ผ๋ณผ ์ ์๋๋ก cache์ ์คํํ suspender(.run())๋ฅผ ๋ด์์ค๋ค.
customPromise.cache = customPromise.run();
throw customPromise.run(); // suspender
} else if (customPromise.status === PROMISE_STATUS.FAIL) {
throw customPromise.error;
}
if (customPromise.status === PROMISE_STATUS.PENDING) {
// status์ ์ด๊ธฐ ์ํ๋ null์ด๊ณ ์ดํ์ pending์ผ๋ก ๋ฐ๋ ๋ค, fulfilled ํน์ rejected๊ฐ ๋๋ค.
// pending์ผ ๊ฒฝ์ฐ suspense์ ๊ฐํ๊ฒ ๋ ํ
๋ฐ, ์ฌ๊ธฐ์ ๊ฐ์ง๋ ๊ฒฝ์ฐ๋ suspense์ ๋์์ด ์๋ชป๋ ๊ฒ
if (customPromise.cache) {
// ์บ์๋ ๊ฐ์ด ์์ ๊ฒฝ์ฐ ํด๋น suspender๋ฅผ ๋ค์ ๋์ง๊ณ ์ด์ธ์ ๊ฒฝ์ฐ์๋ ์๋ฌ๋ก ํ๋จ
throw customPromise.cache;
// throw customPromise.run();
}
throw new Error("Suspens Error");
}
return { data: customPromise.result };
}
export default useFetch;
๊ฒฐ๋ก
react-query๊ฐ ์๋ ๋ค๋ฅธ ๋น๋๊ธฐ ๋ฐ์ดํฐ ํจ์นญ ์์๋ ์ฌ์ฉํ ์ ์๋ ํ ์ ๋ง๋ค์ด๋ณด๋ฉฐ, Suspense์ ๊ธฐ๋ณธ ์๋ฆฌ์ ๋ํด์ ์ดํดํ ์ ์์๋ค.
ErrorBoundary๊ฐ throw ํ Error๋ฅผ ๊ฐ์งํ๋ฏ Suspense๋ throw ํ Promise๋ฅผ ๊ฐ์งํ๋ค.
๋๋ฌธ์ ๋ฐ์ดํฐ ํจ์นญ ์์ Promise์ ์ํ์ ๋ฐ๋ผ response๋ฅผ ๋ฆฌํดํ๊ฑฐ๋ error๋ฅผ ๋์ง๊ฑฐ๋, pending์ผ ๊ฒฝ์ฐ promise๋ฅผ ๋์ง ์ ์๋ ๊ตฌ์กฐ๋ฅผ ์ถ๊ฐํ ๊ฒ ๋ง์ผ๋ก promise๋ฅผ suspense์ ๊ฐ๋ ์ ์๊ฒ ๋์๋ค.
โ react-query์ suspense ์ต์ ๊ณผ React.Suspense๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ ๋ฌธ์ ๊ฐ ๋์ง ์์ ๊ฒ ๊ฐ๋ค.
๋ํ SSR์ ์ง์ํ๊ธฐ ์ํด SSRSuspender๋ฅผ ๋ง๋ค์๋๋ฐ, ์ฌ๊ธฐ์๋ mount ๋์ง ์์์ ๋ fallback์ ๋ด๋ ค์ฃผ๋๋ก ํ๋ ๋ถ๋ถ๋ง ์ถ๊ฐํ๋ ๊ฒ์ผ๋ก ๊ฐ๋ฐํ ์ ์์๋ค.
# ์นดํ ๊ณ ๋ฆฌ
- BOJ 36
- Algorithm 12
- CodingTest 11
- Web 9
- Javascript 8
- Vue 7
- React 7
- DBProject 4
- Python 3
- Tech-interview 3
- Express 3
- Next 3
- Github 2
- Django 2
- C 1
- C++ 1
- WebGame 1