실무에서, text editor에 이미지를 업로드하는 기능을 고도화 하던 중, 이미지를 업로드하면서 로딩중일 때, 취소 버튼을 누르면 업로드하는 중이었던 api 요청도 같이 취소하는 기능을 구현해보기로 했다. 이때, "진행중인 api 요청 취소" 라는 기능은 js에서 특별하게 지원해주는 객체가 있었다.
AbortController
는 js에서 비동기 작업(특히 fetch 요청)을 중단할 수 있는 기능을 제공하는 객체이다. 예를 들어, 네트워크 요청이 너무 오래 걸리거나 사용자가 다른 페이지로 이동할 때, 더 이상 필요하지 않은 요청을 중단할 수 있다. 이를 통해 불필요한 리소스 낭비를 줄이고, 성능을 최적화할 수 있다.
AbortController
객체는 signal
과 abort
라는 두 가지 주요 속성을 가지고 있다.
AbortSignal
객체로, 비동기 작업을 제어하는 신호 역할을 한다. 이 signal
을 fetch
같은 비동기 작업에 연결해 중단 요청을 보낼 수 있다.아래는 fetch
요청을 AbortController
로 취소하는 예제이다.
이곳에선 useEffect
훅을 사용해 컴포넌트가 마운트될 때 요청을 보내고, 언마운트될 때 controller.abort()
를 호출하여 요청을 취소한다.
const DataFetcher: FC = () => {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController(); // AbortController 생성
const signal = controller.signal;
// fetch 요청
fetch('https://api.example.com/data', { signal })
.then((response) => response.json())
.then((data) => setData(data))
.catch((error) => {
if (error.name === 'AbortError') {
console.log('요청이 중단되었습니다.');
} else {
setError(error);
}
});
// 컴포넌트가 언마운트될 때 요청 중단
return () => {
controller.abort();
};
}, []); // 빈 배열 의존성으로 첫 렌더링 때만 실행
return (
<div>
{error && <p>에러: {error.message}</p>}
{data ? <pre>{JSON.stringify(data, null, 2)}</pre> : <p>데이터 로딩 중...</p>}
</div>
);
}
이 기능은 `axios` 에서 timeout 속성으로 간단하게 지원해주긴 한다..
React에서 setTimeout을 사용해 일정 시간 후 요청을 자동으로 중단할 수 있다. 아래 예제에서는 5초 후에 요청을 취소하는 타임아웃을 설정한다.
const TimeoutDataFetcher = () => {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
fetch('https://api.example.com/data', { signal })
.then((response) => response.json())
.then((data) => setData(data))
.catch((error) => {
if (error.name === 'AbortError') {
console.log('요청이 타임아웃되어 중단되었습니다.');
} else {
setError(error);
}
});
// 5초후 자동으로 요청 중단
const timerId = setTimeout(() => controller.abort(), 5000)
return () => {
clearTimeout(timeoutId); // 타임아웃 해제
controller.abort(); // 컴포넌트 언마운트 시 요청 중단
};
}, []);
return (
<div>
{error && <p>에러: {error.message}</p>}
{data ? <pre>{JSON.stringify(data, null, 2)}</pre> : <p>데이터 로딩 중...</p>}
</div>
);
}
React에서 검색 기능을 구현할 때, 사용자가 입력할 때마다 이전 요청을 취소하고 새로운 요청을 보낼 수 있다. 이렇게 하면 사용자가 빠르게 입력할 때 불필요한 요청을 방지할 수 있다. (사실 이렇게 구현하기 보단 debounce
를 활용해서 최적화된 이벤트에 요청을 보내는 방법이 더 좋긴하다.)
export const SearchComponent = () => {
const [searchKeyword, setSearchKeyword] = useState<string>('')
const [isLoading, setIsLoading] = useState<boolean>(false)
const [result, setResult] = useState<any | null>(null)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
if (!searchKeyword || searchKeyword.trim().length < 0) return
setIsLoading(true)
const controller = new AbortController()
const signal = controller.signal
fetch(`https://openlibrary.org/search.json?title=${searchKeyword}`, {
signal,
})
.then((response) => response.json())
.then((data) => setResult(data))
.catch((err) => {
if (err.name === 'AbortError') {
console.log('Request was aborted')
} else {
setError(err)
}
})
.finally(() => setIsLoading(false))
return () => {
controller.abort()
}
}, [searchKeyword])
return (
<div>
<input
type='text'
placeholder='검색어 입력...'
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
/>
{error && <p>에러: {error.message}</p>}
{isLoading ? (
<p>검색 결과 로딩 중...</p>
) : (
!error && <pre>{result?.numFound}</pre>
)}
</div>
)
}
여러 개의 fetch
요청을 동시에 중단하기위해 AbortController
하나를 여러 요청에 공유할 수 있다.
const MultiRequestComponent = () => {
const [data1, setData1] = useState(null);
const [data2, setData2] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
Promise.all([
fetch('https://api.example.com/data1', { signal }).then((res) => res.json()),
fetch('https://api.example.com/data2', { signal }).then((res) => res.json()),
])
.then(([result1, result2]) => {
setData1(result1);
setData2(result2);
})
.catch((error) => {
if (error.name === 'AbortError') {
console.log('모든 요청이 중단되었습니다.');
} else {
setError(error);
}
});
return () => {
controller.abort(); // 컴포넌트 언마운트 시 모든 요청 중단
};
}, []);
return (
<div>
{error && <p>에러: {error.message}</p>}
{data1 && <pre>데이터 1: {JSON.stringify(data1, null, 2)}</pre>}
{data2 && <pre>데이터 2: {JSON.stringify(data2, null, 2)}</pre>}
</div>
);
}