React-saga를 통해 axios 호출 취소하기
Front-End/React

React-saga를 통해 axios 호출 취소하기

 

 

회사 프로젝트에서 특정 수치를 조회하기 위해 사용하는 API가 있다. 이 API는 상황에 따라서 호출 시간이 오래 걸리고, 조회 항목이 많아질수록 호출 시간이 길어진다. 만약 사용자가 수치를 조회하는 중 화면을 나간다면, 수치 조회 API는 여전히 Pending 중이기 때문에 그 영향으로 새로 렌더링 되는 화면의 로딩이 길어지게 된다.

수치 조회 API의 응답을 기다리는 중 현재 페이지가 unmount된다면, 해당 API 호출을 취소하여 불필요한 데이터 호출을 막고, UX를 개선하려 한다.

 

 

 

현재 프로젝트에서 비동기 통신 라이브러리는 axios 사용, 비동기 로직은 react-saga 로 관리 중이다. 따라서 이 글에서는 이 두 라이브러리를 사용하여 API 호출을 취소하는 방법을 다룬다.

 

 

 

 

 

Axios의 취소 토큰

참고

 

axios에서는 CancelToken.source 을 사용하여 취소 토큰을 만들 수 있다.

const cancelSource = axios.CancelToken.source();
// GET방식
axios.get('/user/12345', {
  cancelToken: cancelSource.token
})

// POST방식
axios.post('/user/12345', {
  ...params
}, {
  cancelToken: cancelSource.token
})

 

만든 취소 토큰을 axios 요청에 옵션으로 넣어주고

 

cancelSource.cancel();

cancel 메서드 호출로 해당 요청을 취소시키는 방식이다.

 

❗️같은 취소 토큰으로 여러 요청을 취소할 수도 있다.

 

 

 

 

 

만약 API 호출 로직이 컴포넌트 내부에 있었다면, useEffect 안에서 취소 토큰을 생성한 뒤, API 호출 시 해당 토큰을 넣고, clean up 함수 내에서 cancel()을 실행시키면 된다.

useEffect(() => {
    const cancelSource = axios.CancelToken.source();
    const fetch = async() => {
        api호출 ~~
    }

    fetch();
    return () => {
        cancelSource.cancel();
    }
},[])

대충 이런 식으로?

 

 

 

 

하지만 비동기 호출을 saga에서 한다면 저 로직을 어떻게 변형시켜야 할까?

 

 

 

 

React-saga에서의 axios cencel

 

대략적으로 로직을 그려본다면,

  1. 취소 토큰(CancelToken)을 생성한 뒤, API 호출 시 해당 토큰을 넣는 과정은 saga 함수에서 진행.
  2. useEffect를 통해 해당 컴포넌트가 unmount 된다면 API호출 취소 액션을 발생 시키고
  3. saga는 해당 액션을 모니터링하고 있다가 액션 발생시 cancel() 실행한다.

의 순서로 진행될 것이다.

 

 

 

기존의 saga 구성

// saga.js

export const getDataAction = createAction("getDataAction");

function* getDataFlow(action) {
  const { params } = action.payload;

  yield put(getDataRequest());

  try {
    const response = yield call(getData, params);
        yield put(getRequestSuccess(response.data));
    } catch (error) {
        yield put(getRequestFailure(error));
    }
}

function* dataSaga(){
    yield takeLatest(getDataAction, getDataFlow);
}

기존의 사가 구조를 간략하게 재구성하여 나타냈다.

getDataAction 이 발생하면 getDataFlow가 실행된다.

 

const response = yield call(getData, params) 에서 getData 는 axios를 호출 메서드다.

// apis.js

export const getData = async(params) => {
    const response = await axios.post('/somethings', params);
    return response.data;
}

 

 

 

 

API호출 시 취소 토큰 넣어주기

 

이 기존 로직에 1. 취소 토큰을 생성한 뒤 API 호출시 해당 토큰을 넣는 과정을 추가하면 코드는 다음과 같다.

function* getDataFlow(action) {
  const { params } = action.payload;
  yield put(getDataRequest());

  try {
        const cancelSource = axios.CancelToken,source();

    const response = yield call(getData, params, {cancelToken: cancelSource.token});
        yield put(getRequestSuccess(response.data));
    } catch (error) {
        yield put(getRequestFailure(error));
    }
}

 

 

 

unmount시 API취소 액션 생성

// saga.js

export const getDataAction = createAction("getDataAction");
export const cancelDataAction = createAction("cancelDataAction");

...
// (API를 호출하는)Component.jsx

...
useEffect(() => () => {
    dispatch(cancelDataAction());
},[])

API 호출 취소를 요청할 액션 객체 cancelDataAction 을 생성한 뒤,

컴포넌트가 unmount시 해당 액션이 발생하도록 추가해주었다.

 

이제 다시 getDataFlow 로 돌아가 cancelDataAction 을 모니터링하는 코드를 추가해주자.

 

 

 

취소 액션 모니터링 추가

 

여기서 유의해야 할 점은 api 호출을 위해 사용한 call()봉쇄 effect라는 점이다.

일단 call effect가 호출되면 그 결과가 나올 때까지 다음으로 넘어갈 수 없으므로

결과가 나오기 전 pending 상태에서는 cancelDataAction을 캐치할 수 없다.

 

 

const response = yield call(getData, params, {cancelToken: cancelSource.token});
yield take(cancelDataAction); // call effect가 완료될때까지 취소 액션을 캐치하지 못한다.

우리는 API 호출 중 cancelDataAction이 발생하면, 그 즉시 API 호출을 취소하기를 원하는데,

위처럼 구현한다면 우리가 원하는 결과를 얻을 수 없다.

 

 

이런 경우 사용하는 것이 race() Effect이다.

race는 이름 그대로 여러 이펙트를 경주시켜 가장 먼저 완료된 effect만을 실행하고 경주에서 진 이펙트들을 자동으로 취소시킨다.

const {response, cancel} = yield race({
      response: call(getData, params , {cancelToken: cancelSource.token}),
      cancel: take(cancelCountAction)
    });

사용방법은 위와 같다.

 

 

만약 call effect가 무사히 resolve 되면 response에 API 응답 데이터가 담기고, resolve 되기 전에 취소 요청이 들어오면, cancel에 액션 객체가 담긴다.

 

 

 

if(cancel && cancel.type){
      yield call(cancelSource.cancel);
}else{
      yield put(getDataSuccess(getRequestSuccess(response.data)));
}

아래 조건문을 추가하여 만약 cancel에 액션 객체가 담겼다면 cancel을 실행하고,

아니라면 호출이 성공했다는 액션을 보내준다.

 

 

 

 

이제 정상적으로 동작하는지 확인해보자.

API 호출이 완료되지 않은 상태에서 현재 페이지를 나가면,

pending 상태였던 데이터가 canceled되는 것을 확인할 수 있다.

 

 

 

 

// saga.js

export const getDataAction = createAction("getDataAction");
export const cancelDataAction = createAction("cancelDataAction");

function* getDataFlow(action) {
  const { params } = action.payload;
  yield put(getDataRequest());

  try {
        const cancelSource = axios.CancelToken,source();
        const {response, cancel} = yield race({
              response: call(getData, params , {cancelToken: cancelSource.token}),
              cancel: take(cancelCountAction)
            });

        if(cancel && cancel.type){
              yield call(cancelSource.cancel);
        }else{
              yield put(getDataSuccess(getRequestSuccess(response.data)));
        }
    } catch (error) {
        yield put(getRequestFailure(error));
    }
}

function* dataSaga(){
    yield takeLatest(getDataAction, getDataFlow);
}

전체적인 saga 코드는 다음과 같다.

 

 

 

 

 

 

📌

https://stackoverflow.com/questions/50078589/cancel-of-requests-through-saga

 

Cancel of requests through saga

We provide a drop down option at the top..let's say it has options A B C. Whenever user changes the drop down option, a saga gets triggered which makes around 10 different webapi calls.( A map of c...

stackoverflow.com

 

https://redux-saga.js.org/docs/api/#raceeffects

 

API Reference | Redux-Saga

Middleware API

redux-saga.js.org