ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 취소 버튼을 누르면 서버 작업도 취소될까: AbortController의 범위
    Programming 2026. 9. 22. 08:35
    728x90
    반응형

    서로 연결되지 않은 두 투명 관에 달린 별도 황동 밸브
    브라우저와 서버의 취소 제어가 별개임을 표현한 AI 생성 개념 이미지입니다.

    취소 기능은 브라우저 요청 중단과 서버 작업 중단을 따로 정의하고 화면에 표시할 완료 상태도 구분하세요.

    취소 버튼을 누르자 로딩 표시가 사라졌습니다. 그렇다면 서버의 작업도 멈췄을까요. AbortController로 fetch를 중단한 경우라면 그 사실만으로 서버의 처리 결과를 알 수는 없습니다.

    검색 결과를 더 이상 기다리지 않는 기능과, 서버에서 시작한 파일 변환 작업을 실제로 중단하는 기능은 서로 다른 계약입니다. 이 글은 2026년 9월 13일 MDN 문서를 바탕으로 두 범위를 나눕니다. 코드는 가상의 읽기 전용 검색 예시이며 실제 서버 요청이나 작업 취소를 실행한 결과는 아닙니다.

    abort는 연결된 비동기 작업에 중단을 알립니다

    AbortController를 만들면 signal을 얻을 수 있습니다. 취소를 지원하는 작업에 이 signal을 전달하고 controller.abort를 호출해 중단을 알립니다. fetch 요청뿐 아니라 응답 본문을 읽는 작업이나 스트림 소비도 관련 범위에 포함됩니다. AbortController.abort

    하지만 컨트롤러를 만들기만 해서는 아무 요청과도 연결되지 않습니다. fetch 옵션의 signal에 연결해야 그 요청이 신호를 받습니다. 독립적으로 실행한 다른 요청이 같은 시점에 알아서 중단되는 것도 아닙니다.

    같은 signal을 여러 작업에 전달하면 함께 취소되는 범위를 만들 수 있습니다. 반대로 검색 하나를 취소했는데 화면의 다른 필수 조회까지 중단되면 공유 범위를 너무 크게 잡았는지 확인해야 합니다. 컨트롤러의 수명은 취소하려는 작업 묶음과 맞추는 편이 좋습니다.

    한 번 중단된 signal은 다시 초기 상태로 되돌아가지 않습니다. 그 signal로 새 fetch를 시작하면 즉시 거부될 수 있으므로 다음 독립 요청에는 새 컨트롤러가 필요합니다. 재시도 버튼을 눌러도 계속 즉시 취소되는 경우 이 재사용을 확인합니다. AbortSignal의 재사용 제한

    헤더를 받았어도 본문 읽기는 남아 있을 수 있습니다

    fetch의 Promise가 이행됐다고 응답 본문을 모두 읽었다는 뜻은 아닙니다. 상태와 헤더를 받은 뒤 response.json이나 response.text로 본문을 소비하는 단계가 남을 수 있습니다. 그 전에 취소하면 본문을 읽는 Promise가 AbortError로 거부될 수 있습니다. 요청 취소와 본문 소비

    따라서 fetch만 try 안에 넣고 본문 파싱의 오류는 따로 놓치지 않도록 합니다. HTTP 오류도 같은 기준으로 보지 않습니다. 404나 500 응답은 fetch 자체가 반드시 거부되는 상황이 아니므로 response.ok 같은 상태 확인이 필요합니다.

    기본 abort를 사용하면 AbortError가 관련되지만, abort에는 별도의 reason을 전달할 수도 있습니다. 모든 취소 코드가 언제나 같은 메시지 문자열을 가진다고 하드코딩하기보다는 자신이 연결한 signal의 상태와 오류 정책을 함께 봅니다. 취소 사유

    이 구분은 서버가 요청을 받았는지와도 다릅니다. 본문 다운로드를 중단한 시점이라면 서버는 이미 응답을 만들었을 수 있습니다. 클라이언트가 기다리기를 멈춘 시각만으로 서버의 작업 진행을 추정해서는 안 됩니다.

    검색어 A 다음에 B를 입력하는 경우를 구현해 봅니다

    가상의 검색 화면에서 처음에는 “캐시”를 입력하고 곧바로 “쿠키”로 바꿨다고 하겠습니다. 두 번째 요청이 먼저 끝나도 나중에 도착한 첫 번째 결과가 화면을 덮어쓰면 안 됩니다. 이전 요청 중단과 오래된 결과 무시를 함께 설계합니다.

    다음 코드는 #results와 #search-state 요소가 준비된 모듈 스크립트의 예시입니다. /api/search는 같은 출처의 읽기 전용 API이고, 응답은 문자열 배열 items를 가진다는 계약입니다. 실제 검색 서비스와 입력 이벤트 연결은 포함하지 않았습니다.

    const results = document.querySelector("#results");
    const state = document.querySelector("#search-state");
    if (!results || !state) throw new Error("검색 화면 요소가 없습니다.");
    
    let active = null;
    let latest = 0;
    
    export function cancelSearch() {
      latest += 1;
      active?.abort();
      active = null;
      results.textContent = "";
      state.textContent = "검색 결과 기다리기를 중단했습니다.";
    }
    
    export async function search(query) {
      active?.abort();
      const mine = ++latest;
      const controller = new AbortController();
      active = controller;
      results.textContent = "";
      state.textContent = "검색 중입니다.";
    
      try {
        const params = new URLSearchParams({ q: query });
        const response = await fetch("/api/search?" + params, {
          signal: controller.signal,
        });
        if (!response.ok) throw new Error("HTTP " + response.status);
        const data = await response.json();
        if (!Array.isArray(data.items) ||
            !data.items.every(item => typeof item === "string")) {
          throw new Error("검색 결과 형식이 올바르지 않습니다.");
        }
        if (mine !== latest || controller.signal.aborted) return;
        results.textContent = data.items.join("\n");
        state.textContent = data.items.length === 0 ? "검색 결과 없음" : "검색 완료";
      } catch (error) {
        if (mine !== latest) return;
        state.textContent = controller.signal.aborted
          ? "검색 요청이 중단됐습니다."
          : "검색 실패: " + error.message;
      } finally {
        if (mine === latest) active = null;
      }
    }
    

    새 검색을 시작할 때 이전 컨트롤러에 중단을 알리고 새 컨트롤러를 만듭니다. mine은 이번 요청의 번호이고 latest는 화면이 현재 기다리는 번호입니다. 응답이 와도 번호가 다르면 표시하지 않습니다.

    HTTP 오류나 잘못된 응답을 빈 검색 결과로 바꾸지도 않습니다. “검색 결과 없음”은 성공한 응답에 유효한 빈 배열이 있을 때만 표시합니다. 네트워크 실패와 실제 0건을 구분해야 사용자가 검색 조건을 잘못 바꾸지 않습니다.

    입력 이벤트에서는 search를 호출하고, 취소 버튼이나 화면 정리에서는 cancelSearch를 연결할 수 있습니다. 빠른 입력에 따른 요청 수 자체를 줄이려면 별도의 디바운스를 검토합니다. 취소 기능을 넣었다고 이미 보낸 요청의 서버 비용까지 없어지는 것은 아닙니다.

    abort와 작업 번호는 서로 다른 문제를 막습니다

    이전 요청을 abort하면 불필요한 응답 소비를 멈추는 데 도움이 됩니다. 하지만 화면에 어떤 결과를 반영할지는 앱이 정해야 합니다. 이미 응답을 받았거나 별도의 후처리가 진행되는 경로까지 모두 하나의 fetch 취소로 해결했다고 가정하면 안 됩니다.

    작업 번호 검사는 오래된 결과의 화면 반영을 막습니다. 이것만 넣으면 이전 요청과 계산이 계속 자원을 사용할 수 있으므로, 결과 무시와 작업 중단을 같은 기능으로 취급하지 않습니다.

    예시에서 “캐시”의 번호가 1, “쿠키”의 번호가 2라면 2번 결과를 표시한 뒤 1번이 도착해도 화면은 유지돼야 합니다. 사용자가 그 뒤 취소를 누르면 번호가 다시 바뀌므로 진행 중이던 결과가 나중에 화면을 채우지 않아야 합니다.

    검증할 때는 응답 순서를 일부러 뒤집고, 응답 헤더를 받은 뒤 본문 읽는 중에 취소하는 경우도 나눠 봅니다. 정상 순서에서 한 번 버튼이 작동했다는 확인만으로 경쟁 상황까지 처리됐다고 판단하지 않습니다.

    서버 작업 취소에는 별도의 요청과 상태가 필요합니다

    이제 검색이 아니라 서버에서 오래 걸리는 변환 작업을 시작했다고 생각해 보겠습니다. 시작 요청을 보낸 뒤 브라우저 fetch를 중단했더라도 서버는 작업을 접수했거나 이미 일부를 처리했을 수 있습니다. 앞서 본 API의 범위를 서버 작업에 적용해 보면, 브라우저의 중단 신호만으로 서버의 업무 상태까지 되돌렸다고 판단할 수 없다는 한계가 드러납니다.

    서버 작업을 중단해야 한다면 서버가 발급한 작업 ID를 기준으로 취소 요청을 보내고, 서버가 그 요청을 받아 어떤 상태로 전이했는지 다시 확인하는 계약이 필요합니다. 작업 ID를 아직 받지 못한 시작 요청이 끊겼다면 접수 여부를 조회할 수 있는 상관관계 식별자나 재시도 중복 방지 정책도 검토해야 합니다.

    예를 들어 화면 상태를 “취소 요청 보냄”, “서버가 중단 처리 중”, “취소 완료”, “이미 완료돼 취소 불가”, “서버 상태 확인 실패”로 나눌 수 있습니다. 이 명칭은 설명용 모델이며 모든 서비스가 제공하는 표준 상태 코드는 아닙니다.

    취소 요청의 HTTP 응답을 받았다고 실제 작업이 멈췄다는 뜻인지도 서버 계약에 따라 다릅니다. 단지 접수한 응답이라면 최종 상태를 다시 조회해야 합니다. 반대로 이미 완료된 작업을 확인했다면 클라이언트에서 “취소 완료”로 덮어써서는 안 됩니다.

    중요한 것은 모르는 상태를 실패나 성공으로 임의 확정하지 않는 것입니다. 시작 요청이나 취소 요청의 연결이 끊겼다면 “서버 처리 결과를 확인하지 못했습니다”라는 상태가 필요할 수 있습니다. 사용자가 같은 작업을 다시 눌러 중복 실행하는 문제와도 연결됩니다.

    긴 동기 계산은 취소 클릭 자체를 늦출 수 있습니다

    AbortSignal은 취소를 지원하는 작업이 받아 처리하는 신호입니다. 긴 동기 반복문에 컨트롤러를 옆에 만들어 둔다고 실행 중간에 자동으로 끼어들어 멈추지는 않습니다.

    메인 스레드에서 한 작업이 너무 오래 실행되면 버튼 클릭을 처리할 기회도 늦어집니다. JavaScript의 실행 모델에서는 현재 작업이 끝난 뒤 다음 작업을 처리하므로, 취소 버튼이 화면에 있다는 것만으로 즉시 반응성을 보장할 수 없습니다. 실행 완료와 이벤트 처리

    그런 계산은 작업을 나누거나 워커로 분리하고 중단 지점을 설계해야 합니다. 사용하는 라이브러리도 signal을 실제로 지원하는지 확인합니다. 함수에 signal이라는 인자를 넘길 수 있다는 사실과 내부 계산이 그 상태를 확인한다는 사실은 다릅니다.

    취소 완료라는 문구에는 확인한 범위를 담습니다

    읽기 전용 검색이라면 “결과 기다리기 중단”과 오래된 결과를 표시하지 않는 동작이 핵심일 수 있습니다. 서버 작업이라면 취소 요청 접수와 실제 중단 완료를 구분해야 합니다. 같은 버튼 이름을 쓰더라도 사용자에게 약속하는 결과는 다릅니다.

    구현을 마친 뒤에는 요청 시작 전, 헤더 수신 후, 본문 소비 중, 다음 요청으로 바뀐 뒤의 취소를 각각 확인합니다. 서버 작업이 포함됐다면 이미 완료된 경우와 상태 조회 실패도 검증합니다. 이 글에서는 그 실서버 동작을 확인하지 않았습니다.

    AbortController를 쓰는 것으로 취소 설계가 끝나지는 않습니다. 브라우저가 무엇을 멈췄고, 앱이 어떤 결과를 버렸으며, 서버가 어떤 상태인지까지 나눠야 “취소했다”는 말이 실제 동작과 일치합니다.

    728x90
    반응형
Designed by Tistory.