-
Notifications
You must be signed in to change notification settings - Fork 12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Feature]: useAsyncQueue #584
Comments
@haejunejung 비동기 함수를 순차적으로 사용하기 위한 현재는 아래와 같이 활용 할 수 있습니다만... 사용 방식이라던가 사용성이 부족하고, 사용 케이스가 적은 것 같습니다. const { addToProcessQueue } = useAsyncProcessQueue();
addToProcessQueue(promiseCallback1);
addToProcessQueue(promiseCallback2); 제가 느끼기에는 제안 주신 로직이 각각 tasks에 대한 상태를 관리할 수 있어 좋아보이네요 몇 가지 의견을 드리고 싶습니다 고려해주시면 감사드립니다.
type AsyncQueueTask<T> = (...args: any[]) => Promise<T>; // ?
type AsyncQueueTask<T> = (...args: any[]) => T | Promise<T>; // ?
interface UseAsyncQueueOptions {
onSuccess:? () => void;
onError?: () => void;
onFinished?: () => void;
}
interface UseAsyncQueueReturnType<T> {
result: T;
isLoading: boolean;
}
const promiseState = ...
useAsyncQueue() {
// ...
}
위 의견을 기반으로 아래 코멘트에 디테일한 의견을 남겼습니다! 검토부탁드립니다 |
@haejunejung cc. @Sangminnn // (*) State 추출
type State = 'aborted' | 'fulfilled' | 'pending' | 'rejected';
// (*) 'use' suffix 제거, T를 제거할 지 말지 고민 필요
type AsyncQueueTask<T> = (...args: any[]) => T | Promise<T>;
// (*) 'use' suffix 제거 및 QueueTasks -> AsyncQueueTasks 네이밍 변경
// any[] 타입을 readonly any[] 로 변경, 이유는? as const 처리된 배열 요소도 허용하기 위함
// 이후 모든 any[]를 readonly any[]로 활용
type AsyncQueueTasks<T extends readonly any[]> = {
[K in keyof T]: AsyncQueueTask<T[K]>;
};
// (*) 'use' suffix 제거
interface AsyncQueueResult<T> {
state: State;
data: T | null;
}
// (*) onSuccess 추가
interface UseAsyncQueueOptions {
onError?: () => void;
onFinished?: () => void;
onSuccess?: () => void;
}
// 1. 네이밍을 다른 훅이랑 통일화 하기 위해 {~~~}ReturnType 으로 변경
// 2. T -> T extends readonly any[] 로 변경, 이유는? { [P in keyof T]: AsyncQueueResult<T[P]> } 중복 코드 개선를 여기서 개선
// 3. 통일성 있게 P를 K로 변경
// 4. 전체 task에 대한 isLoading 추가
interface UseAsyncQueueReturnType<T extends readonly any[]> {
result: { [K in keyof T]: AsyncQueueResult<T[K]> };
isLoading: boolean;
}
// (*) 훅 외부로 추출
// 더욱 명확하게 관리하기 위해 Record 타입 제거 및 as const 추가
// 네이밍 상수로서 관리하기 때문에 스네이크 케이스로 변경
const PROMISE_STATE = {
aborted: 'aborted',
fulfilled: 'fulfilled',
pending: 'pending',
rejected: 'rejected',
} as const;
// (*) S 제네릭 제거 및 tasks 타입 변경
// 이 내용 이유는 아래에 추가로 남겨놓겠습니다.
export function useAsyncQueue<T extends readonly any[]>(
tasks: AsyncQueueTasks<T>,
options: UseAsyncQueueOptions = {}
): UseAsyncQueueReturnType<T> {
// (*) UseAsyncQueueReturnType<T>['result']; 활용으로
// { [P in keyof T]: AsyncQueueResult<T[P]> } 반복 코드 제거
const defaultValue = tasks.map(() => ({
state: PROMISE_STATE.pending,
data: null,
})) as UseAsyncQueueReturnType<T>['result']; // (*)
const [result, setResult] =
useState<UseAsyncQueueReturnType<T>['result']>(defaultValue); // (*)
} useAsyncQueue의 S제네릭 타입 제거와 tasks 타입 변경 이유는?
const validatePaymentInfo = async () => 1; // number
const processPayment = async () => true; // boolean
const generateReceipt = async () => '2'; // string
// const tasks: (
// | (() => Promise<number>)
// | (() => Promise<string>)
// | (() => Promise<boolean>)
// )[];
const tasks = [
() => validatePaymentInfo(), // 결제 정보 검증
() => processPayment(), // 결제 승인
() => generateReceipt(), // 영수증 발행
]; 기존에는 result타입이 // as-is
// type result: UseAsyncQueueResult<any>[]
const { result } = useAsyncQueue(tasks, {
onError: () => alert('결제 처리 중 오류가 발생했습니다.'),
onFinished: () => alert('결제 처리 완료!'),
}); 개선 후 // to-be
// type result: AsyncQueueResult<string | number | boolean>
const { result } = useAsyncQueue(tasks, {
onError: () => alert('결제 처리 중 오류가 발생했습니다.'),
onFinished: () => alert('결제 처리 완료!'),
}); |
좋은 훅에 대한 논의가 있어 지나가면서 보게되었습니다! 처음 제안해주신 haejunejung님의 의견도 너무 좋았고 이에 대해 보완해주신 의견도 너무 좋아서 실제 구현이 기대되네요! 👍👍 저는 다른 부분은 전반적으로 좋아보이고 다만 새로 의견주신 useEffect(() => {
// 남은 작업이 없으면 onFinished 수행
if (tasks.length === 0) {
onFinished();
return;
}
const executeTasks = async () => {
// 각 task 실행, result에 결과값 저장
};
executeTasks();
}, []); |
@Sangminnn 오우 ㅎㅎ 저는 onFinished는 성공,실패와 별개로 모든 프로세스 종료되면 호출되는 예를 들어 |
@ssi02014 아하! 말씀해주신 부분도 이해했습니다! 처음에 haejunejung 님께서 제안해주신 인터페이스를 기준으로 생각하다보니 말씀해주신 status를 잠시 놓쳤네요! 해당 Status를 두는 것에는 저도 동의합니다! 👍 추가적으로 한가지 고민인 부분은
|
@ssi02014 @Sangminnn const 컴포넌트 = () => {
const { results, isLoading } = useAsyncQueue([fn1, fn2]);
// 예시: 특정 작업이 취소된 경우 해야 하는 작업
useEffect(() => {
const abortedTask = results.find(result => result.state === 'aborted');
if (abortedTask) {
// 예를 들어, 다른 API를 호출하거나 UI를 업데이트하는 로직 추가 ??
}
}, [results]);
}; 추가적인 의견은, // (*) signal 추가
interface UseAsyncQueueOptions {
onError?: () => void;
onFinished?: () => void;
onSuccess?: () => void;
signal?: AbortSignal
}
export function useAsyncQueue<T extends readonly any[]>(
tasks: AsyncQueueTasks<T>,
options: UseAsyncQueueOptions = {}
): UseAsyncQueueReturnType<T> {
const defaultValue = tasks.map(() => ({
state: PROMISE_STATE.pending,
data: null,
})) as UseAsyncQueueReturnType<T>['result']; // (*)
const [result, setResult] =
useState<UseAsyncQueueReturnType<T>['result']>(defaultValue); // (*)
// 만약 비동기 실행 중에 `signal?.abort`가 `true`가 될 때,
// 남아있는 모든 작업을 `aborted`로 처리한다.
} 아래는 활용 예시입니다! const 컴포넌트 = () => {
const [abortController] = useState(new AbortController()); // AbortController 생성
const tasks = [fn1, fn2];
const { result, isLoading } = useAsyncQueue(tasks, {
signal: abortController.signal, // signal을 전달
onError: () => console.log('Error occurred'),
onFinished: () => console.log('All tasks finished'),
onSuccess: () => console.log('All tasks completed successfully'),
});
const handleCancel = () => {
abortController.abort(); // 갑작스럽게 결제 취소와 같이 취소 요청이 필요하다면?
};
return (
<div>
<button onClick={handleCancel}>Cancel Request</button>
{isLoading ? <Loading /> : <div>로딩끝</div>}
</div>
);
}; |
저는 좋습니다. 일반적으로 자주 사용하는 사용 케이스만 따졌을 때는
위 내용과 연장선으로
const executeTasks = async (index: number = 0) => {
console.log("before");
for (let i = index; i < tasks.length; i++) {
if (signal.aborted) {
console.log("aborted 상태 처리");
// results 상태만 처리하고 이후 실제 fetch라던가 비동기 task 호출 X
continue;
}
const task = tasks[i];
const res = await task();
console.log(res);
}
console.log("after");
}; const abortController = new AbortController();
const tasks = [1, 2, 3, 4, 5]; // 1~5 특정 비동기 함수
const cancel = () => {
abortController.abort();
}
// 2가 진행되는 시점에 cancel 호출
cancel();
// 3부터 abort 처리 추가적인 의견
// 내부적으로 currentIndex를 관리
const currentIndex = useRef(-1);
return {
// ...
currentResult: results[currentIndex]
}
interface UseAsyncQueueOptions {
onError?: () => void;
onFinished?: () => void;
onSuccess?: () => void;
signal?: AbortSignal;
enabled?: boolean; (*) 추가
} useEffect(() => {
if (!enabled) return; // 호출 X
// ...
}, [/* ... */])
// usePreservedCallback로 참조 유지해 useEffect 내에서의 사이드이펙트 방지
const executeTasks = usePreservedCallback(async (index: number = 0) => {
// 각 task 실행, result에 결과값 저장
});
// 네이밍에 따른 목적에 맞게 retryTasks로 정의 후 활용
const retryTasks = useCallback((index: number) => {
executeTasks(index);
}, [executeTasks]);
useEffect(() => {
if (tasks.length === 0) {
onFinished();
return;
}
executeTasks(0);
}, [executeTasks]);
return {
// ...
retryTasks,
} retryTasks(2); // tasks의 인덱스 2부터 차례대로 호출
// 전체 프로세스에 대한 retry도 가능
retryTasks(0);
// 아래와 같이 retryAll을 정의해서 활용하는 케이스도 OK입니다만..
// retryTasks만으로도 충분해보입니다 :)
const retryAll = useCallback(() => {
executeTasks(0);
}, []);
return {
// ...
retryTasks,
retryAll,
} 추가 적인 의견 남겨주시면 감사드립니다 :) |
@haejunejung @ssi02014 먼저 두분 다 좋은 의견과 상세한 설명을 해주셔서 감사합니다! 👍 haejunejung 님께서 말씀해주신 부분의 의도도 이해했고, 제가 처음에 우려했던 부분에 대해서는 ssi02014님께서 잘 말씀해주시면서도 해결방법을 제시해주신것같아요! 저는 처음에 각 ssi02014님께서 언급해주신
이 맥락처럼 |
@ssi02014
개별 아이템에 대해서 반환을 다룬다는 점은 좋은 것 같습니다. 다만, (개인적인 견해로는 currentIndex와 results를 사용하는 방안이 사용자가 더 의도를 가지고 사용할 것 같아서요!) AS-IS const { currentResult, currentIndex, results } = useAsyncQueue(...);
// currentResult가 어떤 인덱스에서 실행되는지를
// currentIndex를 통해 한 번 더 알아야 함.
// 의식의 흐름이 currentResult -> currentIndex -> currentResult TO-BE const { currentIndex, results } = useAsyncQueue(...);
// 사용자가 results[currentIndex]로 알 수 있도록 한다면
// 의식의 흐름이 currentIndex -> currentResult
특정 index부터 프로세스를 재호출한다는 방법은 굉장히 좋은 것 같아요 👍. 다만,
|
넵 좋은 의견이라고 생각합니다 동의합니다! :)
저는 retry가 오히려 "전체"의 의미가 포함되어 있지 않다보니 사실 최초에 의견드렸던 retryTasks의 인자로 즉, |
저는 currentIndex와 retryAll/resume 이 전체 재실행과 재시도의 차이를 드러내기에 더 적합해보이네요! 👍 |
@ssi02014 |
@Sangminnn @haejunejung 어느 정도 큰 틀을 잡힌 것 같습니다.🚀 개발 전 논의에 참여해주셔서 감사드립니다! 🙇🏻♂️ |
@ssi02014 @Sangminnn |
@haejunejung 넵 좋습니다 ! modern-kit에 관심가져주셔서 감사드립니다 🙇🏻♂️ |
@haejunejung @ssi02014 좋은 훅 제안해주셔서 감사드리고 좋은 의견 주셔서 많이 배워갑니다 :) 논의하시느라 고생하셨습니다! 👍 |
Package Scope
Overview
비동기 함수를 순차적으로 실행할 수 있는 커스텀 훅을 만드는 건 어떨지 의견을 묻고자 ISSUE 남깁니다.
간단 기능 소개
비동기 함수를 실행시켜야 하되, 순차적으로 실행되기를 바라는 경우가 가끔 있는데요.
예를 들어, 결제 과정을 처리할 때, 결제 정보 검증 -> 결제 승인 -> 영수증 발행처럼 각 과정은 비동기로 처리하고 싶으나 그 과정이 순차적으로 진행되어야 하는 경우에 쉽게 처리할 수 있는 훅이 있으면 좋지 않을까 생각해봤습니다.
현재 생각한 것은 tasks와 options를 props로 전달했을 때, tasks를 처리해주는 것으로 생각했습니다. 간단하게 구현해본 건 아래와 같은 형태로 생각했습니다.
아래와 같은 상황에서 쓸 수 있지 않을까 싶습니다.
The text was updated successfully, but these errors were encountered: