반응형

 

 

복잡한 비동기 로직을 사용할때 Suspense를 사용하면 데이터를 사용하는 컴포넌트와 로딩 & 에러 상태를 핸들링하는 방식을 분리시켜 선언적으로 프로그래밍이 가능하다. 

 

react-query나 recoil에서 suspense를 사용하는 방식을 지원해주는데  라이브러리의 지원이 없이 직접 처리하는 방식이 궁금해서 해보았다. 

 

우선 suspense에 대해 인터넷에 찾아보니 나온 코드는 다음과 같다.

 

function wrapPromise(promise: AxiosPromise) {
	let status = "pending";
	let result = {};
	const suspender = promise.then(
		(successResult) => {
			status = "success";
			result = successResult;
		},
		(errorResult) => {
			status = "error";
			result = errorResult;
		}
	);

	return function read() {
		switch (status) {
			case "pending":
				throw suspender;
			case "error":
				throw result;
			case "success":
			default:
				return result;
		}
	};
}

function fetchDataVersion1(requestAPI: Promise) {
	return wrapPromise(requestAPI);
}

const Main = () => {
	return (
			<Suspense fallback={<div>loading...</div>}>
				<Dddd />
			</Suspense>
	);
};

let count = 0;

setInterval(() => {
	count += 1;
}, 1000);

const axiosRequestExample = new Promise((res) => {
	setTimeout(() => {
		res(`${count} is shown`);
	}, 6000);
});

const requestResource = fetchDataVersion1(axiosRequestExample);

const Dddd = () => {
	const data = requestResource();

	return <div>{JSON.stringify(data)}</div>;
};

export default Main;

 

로직을 간단히 설명하자면, 

 

function wrapPromise(promise: AxiosPromise) {
	let status = "pending";
	let result = {};
	const suspender = promise.then(
		(successResult) => {
			status = "success";
			result = successResult;
		},
		(errorResult) => {
			status = "error";
			result = errorResult;
		}
	);

	return function read() {
		switch (status) {
			case "pending":
				throw suspender;
			case "error":
				throw result;
			case "success":
			default:
				return result;
		}
	};
}

비동기 데이터를 처리중인 상태에는 상위 컴포넌트로 promise를 넘겨서 Suspense가 처리하게 만들고,
(동일하게 error도 상위 컴포넌트로 넘겨서 ErrorBoundary(직접 구현해야함)가 처리하게 함)

데이터가 도착하면 렌더링 하는식으로 

 

Suspense를 이용하면 데이터를 불러오는 event(what)과 어떻게 처리하는 로직(how)을 분리시켜 선언적으로 처리하기 쉽게 만들어주는 강력한 도구가 될 수 있다.

 

그런데 위 코드는 실제로 사용하려고 하니 다음과 같은 기능이 부족했다.

 

1. 캐싱처리로 동일한 데이터는 캐싱처리해서 불러오는 시간을 줄임

2. 필요시 캐싱 처리를 무효화하고 새로운 데이터를 받아올 수 있어야함.

3. 여러 컴포넌트에서 재사용할 수 있어야함. 

 

그래서 다음의 블로그를 참고해서 위 조건을 만족하게 새로 만들어보았다.

 

https://coffeeandcakeandnewjeong.tistory.com/56

 

최종 구현형태는 다음과 같다.

 

let count = 1;

const TEST_URL = () => {
	console.log(`https://jsonplaceholder.typicode.com/todos/${count}`);

	return `https://jsonplaceholder.typicode.com/todos/${count}`;
};

setInterval(() => {
	count += 1;
}, 1000);

const Main = () => {
	return (
		<ErrorBoundary fallback={<div>error</div>}>
			<Suspense fallback={<div>213</div>}>
				<Dddd />
			</Suspense>
		</ErrorBoundary>
	);
};

const Dddd = () => {
	const { data, refresh } = useSuspense("useSuspense", () => axios(TEST_URL()), [123, 456, 789, 1011]);

	return (
		<div>
			<button type="button" onClick={() => refresh()}>
				refresh
			</button>
			<Link to="/login">to login</Link>
			{JSON.stringify(data)}
		</div>
	);
};

 

그런데 실제 구현물을 보니 react-query와 비슷하게 만든거 같다;;

이렇게 되면 위 코드를 실제로 사용하기는 애매해진거 같은 기분이다.

 

top-down 방식으로 위 코드를 파해쳐보자. 

type depsType = number[] | string[];
type axiosType = () => Promise<any>;

function useForceUpdate(key: string) {
	const [value, setValue] = useState(0);
	return () => {
		asyncManager.delete(key);
		setValue(Date.now());
	};
}

const useSuspense = (key: string, getPromiseCandidate: axiosType, deps: depsType = []) => {
	const resetFunction = useForceUpdate(key);

	const refresh = () => {
		resetFunction();
	};

	useEffect(() => {
		return () => asyncManager.delete(key);
	}, []);

	const { nextValue, force } = depsMemorization(key, getPromiseCandidate, deps); //deps 비교
	const { status, suspender, result } = getPromiseData(key, nextValue, force); // Promise 처리 

	if (status === "pending") {
		throw suspender;
	}

	if (status === "error") {
		throw result;
	}

	return { data: result, refresh };
};

 

가장 먼저 useSuspense hook부터 살펴보자.

 

맨 처음 소개되었던 코드에 다음과 같은 기능이 추가됐다.

 

1. depsMemorization - 캐싱처리(key와 deps 비교를 통해)

2. getPromiseData - Promise 처리 및 key값에 따라 메모라이징 

3. refresh - 캐싱 무효화 & refetch 후 re-rendering

 

status에 따라 throw & return 하는 방식은 동일하다.

다만 asyncManager라는 새로운 객체를 추가했고 해당 객체로 캐싱처리 관련을 해줄 예정이고(crud 가능),

key가 동일하고 deps가 동일하다면 한 컴포넌트에서 fetch를 받아왔으면 캐싱처리해 여러 컴포넌트에서 동일하게 사용할 수 있도록 구현했다.

 

다음은 depsMemorization 코드이다.

 

const depsMemorization = (key: string, axiosRequest: axiosType, deps: depsType) => {
	if (!deps || !Array.isArray(deps) || deps.length === 0) return { nextValue: axiosRequest(), force: true };

	if (asyncManager.has(key)) {
		return asyncManager.update(axiosRequest, deps, key);
	}

	return asyncManager.mount(axiosRequest, deps, key);
};

사실 deps란 기능이 필요없다면 key의 유무만 판단해서 구현해줘도 된다.

그게 구현하기 더 심플한거 같기도 하다.

 

useEffect등의 deps 배열과 비슷한 역할을 하게 구현이 되어있다.

deps를 비교해 동일하다면 캐싱 값을 리턴하고 아니라면 key값을 기준으로 새로 캐싱해주는 형태로 구현이 되어있다.

 

다음은 Promise를 핸들링하는 코드인 getPromise이다.

 

const getPromiseDataClosure = () => {
	const promiseMem = new Map();

	return (key: string, axiosRequest: Promise<any>, force: boolean) => {
		if (promiseMem.has(key) && !force) {
			return promiseMem.get(key);
		}
		const promiseData = {
			promise: axiosRequest,
			status: "pending",
			result: null,
			suspender: axiosRequest.then(
				(response) => {
					promiseData.status = "success";
					promiseData.result = response;
				},
				(error) => {
					promiseData.status = "error";
					promiseData.result = error;
				}
			),
		};

		promiseMem.set(key, promiseData);
		return promiseData;
	};
};

const getPromiseData = getPromiseDataClosure();

기능은 심플한데 아까 맨 처음 나온 코드에 key값을 통한 메모리제이션 기능이 추가되었다.

 

1초마다 count가 올라가고, refresh 누를때마다 testurl/{count} 주소 로 request를 보내는 방식으로 구현해봤는데 실험 결과 잘 작동하는것 같다.

 

최종 코드는 다음 주소에서 볼 수 있다.

 

https://codesandbox.io/s/zealous-hugle-ozz7j0?file=/src/App.tsx

 

 

반응형

'Front-end > React' 카테고리의 다른 글

react-flow에 undo, redo 구현 (2) - 함수형 패러다임  (0) 2022.09.17
react-flow에 undo, redo 구현  (0) 2022.09.03
react 17(개인 공부용)  (1) 2022.06.09
React18(개인 공부 메모용)  (0) 2022.06.09
react TMI  (0) 2022.04.13

+ Recent posts