개인 프로젝트에서 유튜브 영상 URL을 입력하면 영상 속 레시피를 분석해 재료와 조리 과정을 구조화된 데이터로 추출하는 기능을 구현했습니다.
사용자가 유튜브 링크를 입력하면 해당 영상을 분석하고 레시피 정보를 추출하기 위해 LLM API를 호출하는 구조였습니다.
하지만 이 과정에서 응답을 받기까지 약 20~30초 정도의 시간이 소요되기도 했습니다.
테스트를 진행하다 보니 다음과 같은 상황이 종종 발생했습니다.
하지만 한 번 보낸 API 요청은 기본적으로 클라이언트에서 쉽게 취소할 수 없었습니다.
이 경우 더 이상 필요하지 않은 요청도 계속 서버에서 처리될 수 있었습니다. 특히 LLM 기반 분석 작업은 응답 시간이 길어질 수 있기 때문에, 사용자가 요청을 중단할 수 있는 UX가 중요하다고 느꼈습니다.
그래서 진행 중인 API 요청을 사용자가 직접 중단할 수 있는 방법을 찾게 되었습니다.
문제를 해결하기 위해 LLM API 요청을 취소할 수 있는 방법을 찾아보았습니다.
조사해보니 JavaScript에서는 AbortController라는 Web API를 사용해 진행 중인 비동기 작업을 중단할 수 있다는 것을 알게 되었습니다.
AbortController는 fetch와 같은 네트워크 요청에 취소 신호를 전달하여 요청을 중단할 수 있도록 해주는 인터페이스입니다.
이를 활용하면 다음과 같은 상황에서 API 요청을 취소할 수 있습니다.
이 기능을 활용하면 불필요한 네트워크 요청을 줄이고 사용자 경험을 개선할 수 있습니다.
AbortController는 진행 중인 비동기 작업을 중단할 수 있도록 해주는 Web API입니다. 대표적으로 fetch와 같은 HTTP 요청을 취소할 때 많이 사용됩니다.
일반적으로 브라우저에서 API 요청을 보내면, 요청이 시작된 이후에는 클라이언트에서 이를 직접 취소하기 어렵습니다. 하지만 AbortController를 사용하면 진행 중인 요청을 명시적으로 중단(abort)할 수 있습니다.
AbortController는 크게 두 가지 요소로 구성됩니다.
먼저 AbortController 인스턴스를 생성한 뒤, 해당 객체의 signal을 API 요청에 전달합니다. 이후 필요할 때 abort() 메서드를 호출하면 요청이 중단됩니다.
const controller = new AbortController();
fetch("/api/data", {
signal: controller.signal,
});
// 요청 취소
controller.abort();
abort()가 호출되면 해당 요청은 중단되고, fetch의 Promise는 AbortError와 함께 reject 됩니다.
이러한 방식으로 AbortController는 네트워크 요청, 스트림 처리, 응답 데이터 소비 등의 비동기 작업을 중간에 취소할 수 있는 메커니즘을 제공합니다.
AbortController는 단순히 HTTP 요청 자체만 취소하는 것이 아니라, 요청 이후에 이어지는 데이터 처리 과정도 함께 중단할 수 있습니다.
예를 들어 fetch 요청이 완료된 뒤에도 다음과 같은 작업이 이어질 수 있습니다.
일부 API는 데이터를 한 번에 보내지 않고 조각(chunk) 단위로 나누어 전송합니다. 대표적인 예가 LLM 응답 스트리밍입니다.
예를 들어 AI 응답이 다음과 같이 순차적으로 도착할 수 있습니다.
오늘은
바스크 치즈케이크
레시피를
알려드리겠습니다.
이런 경우 클라이언트는 데이터를 스트림으로 계속 읽어들이게 됩니다.
만약 사용자가 중간에 “응답 중단” 버튼을 누르면 AbortController를 통해 진행 중인 스트림 읽기를 중단할 수 있습니다.
fetch로 받은 응답은 보통 다음과 같은 메서드를 통해 데이터로 변환하는 과정이 필요합니다.
const response = await fetch("/api/v1/recipe/1");
const data = await response.json();
여기서 response.json()은 응답 데이터를 파싱하는 비동기 작업입니다. 만약 응답 데이터가 매우 크거나 처리 시간이 길다면, 이 과정 역시 AbortController로 중단할 수 있습니다.
AbortController는 사용자 인터랙션이 빠르게 변하는 상황에서 특히 유용합니다. 대표적으로 다음과 같은 경우에 사용됩니다.
검색창 자동완성이나 필터 기능처럼 사용자가 빠르게 입력을 변경하는 경우, 이전 요청의 결과는 더 이상 필요하지 않을 수 있습니다.
예를 들어 사용자가 "cheese"를 검색하는 과정에서 다음과 같은 요청이 연속적으로 발생할 수 있습니다.
입력: c
GET /api/v1/search?query=c
입력: ch
GET /api/v1/search?query=ch
입력: che
GET /api/v1/search?query=che
입력: chee
GET /api/v1/search?query=chee
입력: chees
GET /api/v1/search?query=chees
입력: cheese
GET /api/v1/search?query=cheese
이때 이전 요청이 취소되지 않으면 불필요한 요청이 계속 서버로 전송됩니다. 또한 응답 순서가 뒤바뀌면서 오래 걸린 요청의 결과가 UI를 덮어쓰는 문제도 발생할 수 있습니다.
이런 경우 새 요청을 보내기 전에 이전 요청을 abort하여 문제를 방지할 수 있습니다.
API 응답 시간이 긴 작업에서는 사용자가 작업을 중단할 수 있는 UX가 필요합니다.
응답이 오래 걸리는 작업의 경우 사용자는 결과를 기다리다가 중간에 작업을 취소하고 싶을 수 있습니다. 예를 들어 요청 시간이 예상보다 길어지거나, 더 이상 해당 작업이 필요하지 않다고 판단하는 상황이 있을 수 있습니다.
이때 사용자가 요청을 중단할 수 있는 방법이 없다면 불필요한 요청이 계속 서버에서 처리될 수 있습니다. 따라서 사용자에게 요청을 취소할 수 있는 인터페이스를 제공하는 것이 중요합니다.
특히 AI / LLM 응답은 생성 과정이 길어질 수 있기 때문에 사용자가 응답을 기다리다가 중간에 생성을 중단하고 싶어하는 상황이 자주 발생합니다.
예를 들어 다음과 같은 상황입니다.
이러한 기능에서는 응답 시간이 길어질 수 있기 때문에, 사용자가 “취소” 버튼을 통해 진행 중인 작업을 중단할 수 있도록 하는 UX가 자주 사용됩니다.
사용자가 페이지를 떠났는데도 이전 요청이 계속 실행되는 경우가 있습니다. 이 경우 다음과 같은 문제가 발생할 수 있습니다.
API 요청이 완료되지 않은 상태에서 페이지를 떠나더라도, 해당 요청과 관련된 Promise, 콜백, 응답 데이터 처리 로직은 메모리에 남아 계속 실행될 수 있습니다.
예를 들어 사용자가 어떤 페이지에서 데이터를 요청한 뒤 곧바로 다른 페이지로 이동했다고 가정해 보겠습니다.
useEffect(() => {
fetch("/api/v1/recipe/1")
.then(res => res.json())
.then(data => {
setData(data);
});
}, []);
이때 네트워크 요청은 여전히 진행 중입니다.
만약 이런 요청이 반복적으로 발생하면 불필요한 비동기 작업이 계속 쌓이게 되고, 브라우저 메모리 사용량이 증가할 수 있습니다.
특히 다음과 같은 상황에서 문제가 더 커질 수 있습니다.
따라서 더 이상 필요하지 않은 요청은 AbortController로 중단하여 불필요한 리소스 사용을 줄이는 것이 좋습니다.
페이지를 떠났거나 컴포넌트가 사라진 뒤에도 API 요청이 완료되면, 더 이상 존재하지 않는 화면을 업데이트하려는 코드가 실행될 수 있습니다.
예를 들어 다음과 같은 코드가 있다고 가정해 보겠습니다.
useEffect(() => {
fetch("/api/v1/recipe/1")
.then(res => res.json())
.then(data => {
setData(data);
});
}, []);
만약 사용자가 API 응답이 오기 전에 다른 페이지로 이동하면, 해당 컴포넌트는 이미 언마운트된 상태가 됩니다.
그런데 요청이 늦게 완료되어 setData(data); 코드가 실행됩니다.
이때 React에서는 이미 사라진 컴포넌트의 상태를 업데이트 하려는 문제가 발생합니다.
요청을 취소하지 않으면 다음과 같은 문제가 발생할 수 있습니다.
따라서 페이지 이동이나 컴포넌트 언마운트 시 진행 중인 요청을 정리하는 것이 중요합니다.
컴포넌트 언마운트?
사용자가 페이지를 이동하면 해당 화면을 구성하던 요소들은 화면에서 제거됩니다.
React에서는 이러한 상태를‘컴포넌트가 언마운트 되었다’고 표현합니다.
이러한 문제를 방지하기 위해 진행 중인 비동기 요청을 중단할 수 있는 방법이 필요합니다. JavaScript에서는 AbortController를 사용해 이러한 요청을 취소할 수 있습니다.
최근에는 AI API 호출을 중단하기 위해서도 AbortController가 많이 사용됩니다.
LLM 응답은 다음과 같은 특징이 있습니다.
이때 AbortController를 사용하면 진행 중인 AI 응답을 즉시 중단할 수 있어 사용자 경험을 개선할 수 있습니다.
앞에서 살펴본 것처럼 AbortController를 사용하면 진행 중인 API 요청을 중단할 수 있습니다.
이번에는 실제로 AbortController를 사용해 LLM API 요청을 취소하는 방법을 살펴보겠습니다.
AbortController 인스턴스를 생성합니다.
const controller = new AbortController();
AbortController 객체는 요청을 취소하는 역할을 하며, 여기서 생성된 signal을 API 요청에 전달해 취소 신호를 보낼 수 있습니다.
fetch 요청을 보낼 때 signal을 함께 전달합니다.
const controller = new AbortController();
fetch("/api/v1/recipes/extract", {
method: "POST",
body: JSON.stringify({ url: youtubeUrl }),
signal: controller.signal,
});
요청을 중단하려면 AbortController의 abort() 메서드를 호출하면 됩니다.
controller.abort();
이 메서드가 호출되면 진행 중인 fetch 요청이 즉시 취소됩니다.
요청이 취소되면 fetch는 AbortError를 발생시킵니다. 따라서 이를 구분해 처리하는 것이 좋습니다.
try {
const response = await fetch("/api/v1/recipes/extract", {
method: "POST",
body: JSON.stringify({ url: youtubeUrl }),
signal: controller.signal,
});
const data = await response.json();
} catch (error) {
if (error.name === "AbortError") {
console.log("요청이 취소되었습니다.");
} else {
console.error(error);
}
}
이렇게 AbortController를 사용하면 사용자가 원할 때 진행 중인 API 요청을 중단할 수 있습니다.
프론트엔드에서 AbortController로 요청을 취소하면 서버에서는 요청의 abort signal을 통해 클라이언트 연결이 종료된 것을 감지할 수 있으며, 이를 활용해 진행 중인 작업을 중단할 수 있습니다.
이를 통해 사용자가 요청을 취소했을 때 네트워크 요청뿐 아니라 서버에서 수행 중이던 크롤링이나 LLM 분석 작업도 함께 중단되어 불필요한 리소스 사용을 줄일 수 있습니다.
이번 글을 통해 AbortController를 사용하면 진행 중인 백엔드 API나 LLM API 요청을 클라이언트에서 중단할 수 있다는 것을 알게 되었습니다.
특히 사용자와의 인터랙션이 빈번하게 발생하는 브라우저 환경에서는, 더 이상 필요하지 않은 요청을 취소함으로써 불필요한 네트워크 요청을 줄일 수 있습니다.
LLM과 같이 응답 시간이 긴 API를 사용하는 경우, 사용자가 요청을 중간에 취소할 수 있는 기능을 제공하는 것이 사용자 경험을 개선하는 데 도움이 된다는 점도 확인할 수 있었습니다.