14. 자바스크립트에서 프로미스를 취소하는 방법
https://velog.io/@eunbinn/cancel-promises-javascript?utm_source=substack&utm_medium=email
1. Promise.withResolvers() 사용하기
- 새 Promise 객체와 이를 이행(resolve)하거나, 거부 (reject)할 수 있는 두개의 함수가 포함된 객체를 반환한다.
const buildCancelableTask = <T> (asyncFn: ()=> Promise<T>)=> {
let rejected = false;
const { promise, resolve, reject } = Promise.withResolvers<T>();
return {
run:()=>{
if(!rejected){
asyncFn().then(resolve, reject)
}
return promsie;
},
cancel:()=>{
rejected=true;
reject(new Error("CanceledError"));
}
}
}
- 요건 정말 작업을 '취소'했다기 보다는, '조기 거부(early rejection)'를 수행한 것.
2. AbortController 사용하기
const buildCancelableTask = <T> (asyncFn:()=>Promiose<T>)=>{
const abortController = new AbortController();
return {
run: ()=>{
new Promise()<T>((resolve, reject)=> {
const cancelTask = () => reject(new Error('CanceledError'));
});
if(abortController.signal.aborted){
cancelTask();
return;
}
asyncFn().then(resolve, reject);
abortController.signal.addEventListnenr("abort", cancelTask);
},
cancel: ()=> {
abortController.abort();
}
}
}
- AbortController 는 cancel 을 여러번 호출해도,
abort
이벤트가 한번만 트리거 된다는 장점이 있다
3. 순차적 비동기 처리 리액트 훅만들기
- 동시에 여러 요청이 발생하면, 이전 요청을 자동으로 취소한다.
import { useCallback, useRef } from "react";
const buildCancelableFetch = <T>(
requestFn: (signal: AbortSignal) => Promise<T>
) => {
const abortController = new AbortController();
return {
run: () =>
new Promise<T>((resolve, reject) => {
if (abortController.signal.aborted) {
reject(new Error("CanceledError"));
return;
}
requestFn(abortController.signal).then(resolve, reject);
}),
cancel: () => {
abortController.abort();
},
};
};
// 항상 최신값 참조
function useLatest<T>(value: T) {
const ref = useRef(value);
ref.current = value;
return ref;
}
export function useSequentialRequest<T>(
requestFn: (signal: AbortSignal) => Promise<T>
) {
// 항상 최신의 requestFn 을 참조한다.
const requestFnRef = useLatest(requestFn);
// 현재 진행중인 요청을 추적한다.
const currentRequest = useRef<{ cancel: () => void } | null>(null);
return useCallback(async () => {
// 1. 새 요청 시작시, 진행중인 요청이 있다면 취소한다.
if (currentRequest.current) {
currentRequest.current.cancel();
}
// 2. 새 요청을 생성한다.
const { run, cancel } = buildCancelableFetch(requestFnRef.current);
// 3. 현재 요청 정보를 업데이트한다.
currentRequest.current = { cancel };
// 4. 요청 완료 후, 현재 요청이 완료된 요청과 동일한 경우 currentrequest 를 null로 설정한다.
return run().finally(() => {
if (currentRequest.current?.cancel === cancel) {
currentRequest.current = null;
}
});
}, []);
}
사용 예시
const fetchData = useSequentialRequest(async (signal) => {
const response = await fetch('https://api.example.com/data', { signal });
return response.json();
});
// 사용
useEffect(() => {
fetchData().then(data => console.log(data));
}, [fetchData]);