AbortController로 네트워크 요청 중단시켜보기

2022. 6. 27

여태까지 프론트엔드 개발자로서 업무하면서 비동기 작업을 통해 데이터를 다루었다. (물론 얼마 안되는 경력이지만.. 😅)

그런데 생각해보니 비동기 작업을 통해 데이터를 가져오는 것만 생각하고, 정작 요청에 대해서는 생각해본적이 없었다.

그래서 이번 기회에 요청작업에 대해 몇가지 알아보다가 AbortController 라는 객체를 알게 되었고, 이를 간단하게 사용해보며 학습한 내용을 공유해보려 한다.

예제 코드

AbortController? 🤔

서론에서 유추할 수 있듯이 AbortController는 웹 요청에 관련된 객체이다.

그 중에서도 요청 중단를 할 수 있는 친구인데 이를 통해 원하지 않는 요청을 중단한다거나, 필요가 없어진 요청을 중단하는 등 요청에 대한 응답이 더이상 필요 없어진 비동기 작업의 중단를 담당한다.

몇가지 상황을 예로 들자면 아래와 같다.

  1. 요청을 하였지만 응답이 너무 오래 걸린다.
  2. 요청을 한 후 사용자가 다른 작업을 위해 화면을 이동했다.
  3. 서비스의 특정 상황으로 인해 임의의 시간 이후 요청을 중단해야한다.

위 상황 말고도 정말 여러가지 상황으로 인해 요청을 중단해야할 수도 있을 것이다.

이런 중단을 통해 그만큼 네트워크 요청이 줄어들게 된다면 더 효율적인 서비스를 운영할 수 있지 않을까 생각된다.

AbortController는 fetch API를 통해 공식적으로 사용할 수 있으며 axios, react-query 등의 라이브러리에서도 사용법에 대한 안내를 해주고 있어 필요시 공식문서를 통해 사용해보면 된다.

먼저 AbortController를 통해 요청을 중단하는 간단한 예제부터 살펴보자

Javascript

예시코드

간단한 버튼 예시로서, 버튼을 클릭하면 비동기 요청을 하고 특정 시간까지 기다린 이후 자동으로 중단시키는 방법이다.

const FETCH_URL = 'https://jsonplaceholder.typicode.com/posts'

const $ = (selector) => document.querySelector(selector)

let abortController = null

document.addEventListener('DOMContentLoaded', () => {
  $('.fetch-button').addEventListener('click', getPostsData)
})

async function getPostsData() {
  // AbortController 할당
  abortController = new AbortController()
  // abortSignal 선언
  const { signal } = abortController

  const timer = setTimeout(() => {
    // 특정 ms 이후 요청을 수동으로 중단
    abortController.abort()
    console.log('abort')
  }, 100)

  try {
    // fetch API에 signal 선언
    const response = await fetch(FETCH_URL, { signal })
    const posts = await response.json()

    drawPosts(posts)
  } catch (error) {
    alert(error)
    exception()
  } finally {
    clearTimeout(timer)
    abortController = null
  }
}

function drawPosts(posts) {
  $('.list').innerHTML = posts
    .slice(0, 20)
    .map((post) => `<li>${post.title}</li>`)
    .join('')
}

function exception() {
  $('.list').innerHTML = '다시 요청해주세요'
}

코드가 길어보이지만 사실 AbortController에 대한 부분은 많이 없다. 😅

먼저 AbortController를 생성자를 통해 초기화한 부분이 보인다.

그리고 초기화한 abortController 변수에는 signal이라는 값이 존재하는데, 이를 통해 fetch API와 연결하여 특정 신호를 보내 요청을 중단시킬 수 있다.

AbortController는 한 가지 이상의 요청에 대해서 중단 요청을 할 수 있지만, 현재 예제에서는 하나의 요청만을 중단할 것이기 때문에 요청할 함수 내에서 매번 AbortSignal을 생성하여 signal을 생성시켰다. (요청 수 그리고 상황마다 다르게 처리할 수 있다.)

이제 중단을 위한 신호를 주기 위해 abortController.abort()를 통해 요청 이후 10ms 가 지나면 중단될 수 있도록 처리했다.

나머지 코드들은 HTML을 위한 코드기 때문에 크게 신경쓰지 않아도 된다. (필요시 예제 코드를 참고)

중단 확인

이제 요청 버튼을 클릭하면 실제로 잘 요청이 중단되는지 확인해보면 아래와 같은 에러가 출력된다.

AbortError: The user aborted a request.

여기서 알 수 있는 것은 바로 비동기 요청에 중단에 대한 응답이 catch에서 처리된다는 것이다. 😲

요청이 중단되었기 때문에 drawPosts를 통해 화면에 그려지는 것 없이 exception 함수의 결과가 출력된다.

abort error

이처럼 뭔가 특정한 요청을 했는데 일정 시간이 지나면 요청을 중단 후 무언가를 통해 사용자에게 전달 할 수 있다.

그러면 정작 우리는 대부분이 리액트에서 개발하게 되는데, 이런 방법을 리액트에서는 어떻게 사용해야할까?

React

예시 코드

import React, { useEffect, useRef, useState } from 'react'
import './App.css'
import { Link } from 'react-router-dom'

const FETCH_URL = 'https://jsonplaceholder.typicode.com/posts'

function App() {
  // abortController ref로 초기화
  const abortController = useRef(null)
  const [posts, setPosts] = useState([])

  async function getPostsData() {
    // AbortController 할당
    abortController.current = new AbortController()
    // abortSignal 선언
    const { signal } = abortController.current

    try {
      // fetch API에 signal 선언
      const response = await fetch(FETCH_URL, {
        signal,
      })
      const posts = await response.json()

      setPosts(posts)
    } catch (error) {
      alert(error)
    }
  }

  // 버튼 클릭시 요청 중단
  const handleCancelClick = () => {
    abortController.current && abortController.current.abort()
  }

  useEffect(() => {
    getPostsData()

    // 컴포넌트 이탈시 요청 중단
    return () => abortController.current && abortController.current.abort()
  }, [])

  return (
    <div className="App">
      <header className="App-header">
        <button onClick={handleCancelClick}>중단</button>
        <Link to="/getout">다른 화면으로 이동</Link>
        <ul>
          {posts.slice(0, 20).map((post) => (
            <li key={post.id}>{post.title}</li>
          ))}
        </ul>
      </header>
    </div>
  )
}

export default App

사실 리액트에서도 크게 다른 부분은 없다.

useRef 훅을 사용하여 리렌더링에 따른 이슈를 방지하고, 이전 예시와 동일하게 비동기 요청시 AbortController를 할당해주면서 signal을 fetch API에 선언한다.

하지만 여기서는 시간에 따른 요청 중단이 아닌 버튼 클릭 이벤트, 그리고 컴포넌트 이탈시 abort()를 통해 수동으로 중단하는 형태를 띄고있다.

버튼 클릭으로 인한 중단과, 따로 중단하지 않은 두가지 케이스의 짤이다.

중단하지 않은 결과

none abort

클릭시 요청 중단한 결과

abort click

네트워크 요청이 중단된 형태와 아닌 형태 두개가 잘 처리된 것을 확인할 수 있다. 🎉