어제 오늘 내일

[React] AboutController로 비동기 처리하기 본문

IT/React

[React] AboutController로 비동기 처리하기

hi.anna 2026. 8. 2. 19:09

React 애플리케이션을 개발하다 보면 useEffect 내부에서 API 호출 등 비동기 작업을 수행하는 경우가 매우 많습니다.

하지만 컴포넌트가 빠르게 재렌더링되거나 유저가 다른 페이지로 이동(언마운트)했을 때,

이미 보낸 API 요청이 뒤늦게 응답을 반환하여 메모리 누수나 레이스 컨디션(Race Condition)을 유발하곤 합니다.

이러한 문제를 깔끔하게 해결해 주는 표준 Web API가 바로 AbortController입니다.

이 포스팅에서는 AbortController의 기본 개념부터 React에서 활용하는 3가지 실전 패턴

(언마운트 대처, 실시간 검색 처리, 커스텀 훅 작성)을 알아보겠습니다.


1. AbortController란?

AbortController는 하나 이상의 DOM 요청(예: fetch API)을 원하는 시점에 취소(Abort)할 수 있게 해주는 JavaScript 표준 Web API입니다.

  • const controller = new AbortController();로 인스턴스를 생성합니다.
  • controller.signalfetch 옵션으로 전달합니다.
  • 요청 중단이 필요한 시점에 controller.abort()를 호출합니다.

2. 패턴 1: 컴포넌트 언마운트 시 요청 취소하기

유저가 데이터를 불러오는 도중에 페이지를 이탈하면, 응답이 돌아와도 상태를 업데이트할 컴포넌트가 사라진 상태입니다. useEffect의 클린업 함수(Cleanup Function)에서 controller.abort()를 호출하면 불필요한 네트워크 리소스 소비와 상태 업데이트 에러를 방지할 수 있습니다.

import React, { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    // 1. 새로운 AbortController 인스턴스 생성
    const controller = new AbortController();
    // 2. 컨트롤러로부터 signal 객체를 추출
    const signal = controller.signal;

    async function fetchUserData() {
      setLoading(true);
      setError(null);

      try {
        // 3. fetch 옵션에 signal 전달
        const response = await fetch(`https://api.example.com/users/${userId}`, {
          signal: signal // 또는 단축 속성명으로 signal 만 작성 가능
        });

        if (!response.ok) {
          throw new Error('데이터를 불러오는데 실패했습니다.');
        }

        const data = await response.json();

        // 4. 요청이 정상 완료되었을 때만 상태 업데이트
        setUser(data);
      } catch (err) {
        // 5. 취소 요청으로 발생한 에러(AbortError)인지 구분 처리
        if (err.name === 'AbortError') {
          // abort() 호출에 의한 에러는 의도된 취소이므로 콘솔 로그만 남기거나 무시
          console.log('API 요청이 성공적으로 취소되었습니다.');
        } else {
          // 일반적인 네트워크/서버 에러는 사용자에게 표시
          setError(err.message);
        }
      } finally {
        setLoading(false);
      }
    }

    fetchUserData();

    // 6. useEffect 클린업 함수: 컴포넌트가 언마운트되거나 userId가 변경될 때 실행
    return () => {
      // 진행 중인 API 요청 중단
      controller.abort();
    };
  }, [userId]); // userId가 바뀔 때마다 기존 요청을 취소하고 새 요청 시작

  if (loading) return <div>로딩 중...</div>;
  if (error) return <div>에러 발생: {error}</div>;

  return (
    <div>
      <h2>{user?.name}님의 프로필</h2>
      <p>이메일: {user?.email}</p>
    </div>
  );
}

export default UserProfile;

3. 패턴 2: 레이스 컨디션(Race Condition) 방지 - 실시간 검색

유저가 검색창에 글자를 빠르게 칠 때(r, re, rea, react), 각 글자마다 API 요청이 발송됩니다. 만약 네트워크 상황에 의해 이전 요청(re)이 마지막 요청(react)보다 나중에 응답되면, 잘못된 이전 검색 결과가 화면에 노출되는 레이스 컨디션이 발생합니다.

AbortController를 사용하면 새로운 입력이 들어오는 순간 이전 요청을 바로 취소하여 항상 최신 응답만 적용되도록 보장합니다.

import React, { useState, useEffect } from 'react';

function SearchAutoComplete() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);

  useEffect(() => {
    // 검색어가 비어있으면 API 요청을 보내지 않고 결과 초기화
    if (!query.trim()) {
      setResults([]);
      return;
    }

    // 새로운 컨트롤러 생성
    const controller = new AbortController();

    async function searchApi() {
      try {
        const response = await fetch(
          `https://api.example.com/search?q=${encodeURIComponent(query)}`,
          { signal: controller.signal } // signal 연결
        );
        const data = await response.json();
        setResults(data.items);
      } catch (err) {
        // 의도적인 취소는 에러로 다루지 않음
        if (err.name !== 'AbortError') {
          console.error('검색 중 에러 발생:', err);
        }
      }
    }

    searchApi();

    // query가 변경되면 다음 effect가 실행되기 전 클린업이 작동하여 '이전 요청'을 취소함
    return () => {
      controller.abort();
    };
  }, [query]);

  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="검색어를 입력하세요..."
      />
      <ul>
        {results.map((item) => (
          <li key={item.id}>{item.title}</li>
        ))}
      </ul>
    </div>
  );
}

export default SearchAutoComplete;

4. 패턴 3: Axios에서의 AbortController 활용

많은 React 프로젝트에서 fetch 대신 Axios를 사용합니다. Axios v0.22.0부터는 기존의 CancelToken(현재는 deprecated) 대신 표준 AbortController signal을 지원합니다.

import React, { useEffect, useState } from 'react';
import axios from 'axios';

function AxiosExample() {
  const [data, setData] = useState(null);

  useEffect(() => {
    const controller = new AbortController();

    axios
      .get('https://api.example.com/data', {
        // Axios 요청 설정에 signal 객체 전달
        signal: controller.signal
      })
      .then((response) => {
        setData(response.data);
      })
      .catch((error) => {
        // Axios에서는 axios.isCancel(error) 또는 error.name으로 취소 여부 확인
        if (axios.isCancel(error)) {
          console.log('Axios 요청이 취소되었습니다:', error.message);
        } else {
          console.error('Axios 에러:', error);
        }
      });

    return () => {
      // 클린업 시 중단
      controller.abort();
    };
  }, []);

  return <div>{data ? JSON.stringify(data) : '불러오는 중...'}</div>;
}

5. 패턴 4: 재사용 가능한 커스텀 훅 useFetch 만들기

매번 useEffect 안에서 AbortController를 선언하고 클린업하는 코드가 번거롭다면, 커스텀 훅(Custom Hook)으로 로직을 분리하여 깔끔하게 재사용할 수 있습니다.

import { useState, useEffect } from 'react';

/**
 * URL 기반으로 데이터를 가져오며, 
 * URL 변경이나 언마운트 시 자동 요청 취소를 지원하는 커스텀 훅
 */
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    // URL이 유효하지 않으면 실행 방지
    if (!url) return;

    const controller = new AbortController();

    const fetchData = async () => {
      setLoading(true);
      setError(null);

      try {
        const response = await fetch(url, { signal: controller.signal });
        if (!response.ok) {
          throw new Error(`HTTP 에러 발생: ${response.status}`);
        }
        const result = await response.json();
        setData(result);
      } catch (err) {
        // 취소가 아닐 때만 에러 상태 업데이트
        if (err.name !== 'AbortError') {
          setError(err.message);
        }
      } finally {
        setLoading(false);
      }
    };

    fetchData();

    // 클린업 함수에서 자동으로 요청 중단 처리
    return () => {
      controller.abort();
    };
  }, [url]);

  return { data, loading, error };
}

// --- 커스텀 훅 사용 예시 ---
function CustomHookDemo({ productId }) {
  // 사용처에서는 abort 신경 쓸 필요 없이 단 한 줄로 안전한 비동기 통신 구현
  const { data, loading, error } = useFetch(`https://api.example.com/products/${productId}`);

  if (loading) return <p>상품 정보를 불러오는 중...</p>;
  if (error) return <p>에러 발생: {error}</p>;

  return (
    <div>
      <h3>{data?.name}</h3>
      <p>가격: {data?.price}원</p>
    </div>
  );
}

export default CustomHookDemo;

 

반응형
Comments