외주 개발에서 AbortController를 써야 하는 이유
한줄 요약
비동기 요청은 시작보다 취소가 더 중요하다. AbortController로 깔끔하게 끊어라.
본문
AbortController는 자바스크립트에서 진행 중인 비동기 작업을 명시적으로 중단시키는 내장 API다. 외주 프로젝트에서 이 개념이 제대로 구현되지 않으면, 클라이언트가 실제로 사용하기 전까지 드러나지 않는 버그들이 쌓인다.
에이전시 입장에서 AbortController는 단순한 기술 지식이 아니다. 납품 품질을 가르는 구현 기준 중 하나다.
왜 외주 개발에서 비동기 취소가 자주 빠지나
기능 명세서에는 "검색 결과 출력"이라고 적혀 있다. 하지만 "이전 요청이 남아 있을 때 새 요청이 들어오면 어떻게 처리하는가"까지 적혀 있는 명세는 드물다.
에이전시 개발에서 비동기 취소 로직이 빠지는 이유는 크게 두 가지다. 첫째, 기획 단계에서 이 상황 자체가 정의되지 않는다. 둘째, 구현 시간을 줄이기 위해 "일단 동작하면 된다"는 기준으로 개발이 마무리된다.
결과는 예측 가능하다. 빠른 사용자가 연속으로 입력을 바꾸면 응답 순서가 뒤바뀌고, 화면에는 이미 무의미해진 데이터가 표시된다. 클라이언트는 "버그"라고 부르지만, 원인은 처음부터 설계에 없었던 것이다.
AbortController의 구조, 딱 두 가지만 알면 된다
AbortController를 생성하면 내부적으로 AbortSignal 객체가 함께 만들어진다. 컨트롤러가 취소 명령을 내리는 쪽이고, 시그널은 그 명령을 수신하는 쪽이다.
const controller = new AbortController();
const { signal } = controller;
// signal을 fetch에 연결
fetch('/api/results', { signal })
.then(res => res.json())
.then(data => render(data))
.catch(err => {
if (err.name === 'AbortError') return; // 취소된 요청은 무시
console.error(err);
});
// 필요한 시점에 중단
controller.abort();
abort()를 호출하는 순간 signal의 상태가 바뀌고, 연결된 fetch 요청은 즉시 중단된다. 에러 핸들링에서 AbortError를 구분하지 않으면 취소된 요청이 오류로 로그에 쌓이고, 클라이언트는 불필요한 에러 알림을 받게 된다.
한 번 abort된 signal은 재사용할 수 없다. 연속 요청이 필요한 상황에서는 매 요청마다 새 AbortController를 생성해야 한다.
실무에서 쓰는 패턴 두 가지
연속 요청 제어 패턴
입력 필드에서 API를 반복 호출하는 구조라면 이전 요청을 항상 취소하고 새 컨트롤러를 만드는 방식이 기본이다.
let activeController = null;
async function fetchOnInput(query) {
if (activeController) activeController.abort();
activeController = new AbortController();
try {
const res = await fetch(`/api/search?q=${query}`, {
signal: activeController.signal,
});
const data = await res.json();
renderResults(data);
} catch (err) {
if (err.name !== 'AbortError') console.error(err);
}
}
타임아웃 자동 처리 패턴
서버 응답이 특정 시간을 초과하면 자동으로 요청을 중단하고 싶을 때는 AbortSignal.timeout()을 쓴다. 별도의 타이머 관리 없이 한 줄로 처리된다.
fetch('/api/data', {
signal: AbortSignal.timeout(5000),
})
.catch(err => {
if (err.name === 'TimeoutError') {
// 타임아웃 처리
}
});
복수의 조건을 동시에 적용해야 한다면 AbortSignal.any()로 여러 시그널을 묶을 수 있다. 다만 구형 브라우저 지원 범위는 사전에 확인해야 한다.
이벤트 리스너 정리에도 쓴다
AbortController는 fetch에만 쓰이지 않는다. addEventListener의 옵션으로 signal을 넘기면, abort() 한 번으로 연결된 이벤트 리스너들을 전부 해제할 수 있다.
const controller = new AbortController();
const { signal } = controller;
window.addEventListener('scroll', onScroll, { signal });
window.addEventListener('resize', onResize, { signal });
document.addEventListener('keydown', onKeydown, { signal });
// 컴포넌트 해제 시 한 번에 정리
controller.abort();
기존 방식은 removeEventListener에 동일한 함수 참조를 유지해야 했다. 이 방식을 쓰면 그 번거로움이 사라지고, 이벤트 해제 누락으로 인한 메모리 누수 가능성도 줄어든다. 컴포넌트 라이프사이클이 복잡한 SPA 프로젝트에서 특히 유용하다.
에이전시가 이걸 챙겨야 하는 실제 이유
기술적 완성도는 납품 이후에 드러난다. 클라이언트가 실제 사용 환경에서 겪는 불편함 대부분은 "기능이 없는 것"이 아니라 "엣지 케이스를 처리하지 않은 것"에서 나온다.
AbortController는 구현 난이도가 높지 않다. 기본 사용법을 익히면 대부분의 상황은 10줄 안에 처리된다. 하지만 이걸 챙기지 않는 프로젝트와 챙기는 프로젝트 사이의 품질 차이는 실제 사용자 경험에서 명확하게 갈린다.
유지보수 계약을 맺고 싶은 에이전시라면, 납품물의 안정성이 곧 신뢰다. 비동기 취소 처리는 그 신뢰를 쌓는 작은 기준 중 하나다.
자주 묻는 질문
Q.AbortController를 쓰면 서버 쪽 처리도 자동으로 중단되나?
클라이언트 측 연결만 끊는다. 이미 서버에 도달한 요청은 서버에서 계속 처리된다. 클라이언트는 응답을 받지 않을 뿐이다. 서버 처리 자체를 중단하려면 서버 측에서 별도의 취소 메커니즘을 구현해야 한다. 외주 프로젝트에서는 이 차이를 클라이언트에게 명확히 설명하는 것이 중요하다.
Q.React 같은 프레임워크 프로젝트에서는 어떻게 통합하나?
컴포넌트가 언마운트되는 시점에 `abort()`를 호출하면 된다. React라면 `useEffect`의 cleanup 함수에서 처리하는 게 일반적이다. 상태 업데이트가 언마운트된 컴포넌트에서 발생하는 경고를 방지하는 데도 효과적이다. Vue나 Svelte 같은 프레임워크도 같은 개념을 라이프사이클 훅에 적용하면 된다.
Q.외주 프로젝트 견적이나 일정 산정 시 이런 처리 비용을 따로 반영해야 하나?
별도 항목으로 구분하기보다 "비동기 처리 안정성"을 품질 기준으로 포함시키는 편이 현실적이다. AbortController 자체는 구현 시간이 크지 않다. 다만 어떤 API 호출에 취소 로직이 필요한지 분석하고 설계하는 단계가 필요하다. 이 작업을 처음부터 포함시키면 유지보수 단계에서 수정 비용이 줄어든다.