본문 바로가기

온라인 강의(유데미, 인프런 등)/React 완벽 가이드(유데미)

[react & RTK] React Toolkit 비동기 작업 처리하기(createAsyncThunk)

반응형

Redux

reducer에는 순수하고 Side-effects(데이터 요청(fetch) 등의 비동기 작업, 브라우저 캐시, 로컬스토리지, setTimeout 등)이 없으며 동기적인 기능이 들어있어야 한다. 따라서 비동기적인 로직은 컴포넌트 내부(ex. useEffect()) 또는 action creators 안에서만 사용해야 한다. 

하지만 우리가 주로 만드는 애플리케이션은 다양한 API 요청들을 수반하여 Side-effects라는 것은 반드시 존재하기에 현재 저장소의 상태를 디스패치하거나 확인하여 비동기 논리가 저장소와 상호작용하도록 해야 하는데, 이를 가능하게 해주는 것이 Redux의 미들웨어이다.

Redux에는 여러 종류의 비동기 미들웨어가 있으며 각각 다른 구문을 사용하여 논리를 작성할 수 있다. 가장 일반적인 비동기 미들웨어로 redux-thunk, redux-saga, redux-observable가 있다.

 

미들웨어

Redux에서 dispatch 시 action이 reducer로 전달 되고, reducer는 state를 업데이트 시킨다. 미들웨어를 사용하면 이 과정 사이에 우리가 하고 싶은 작업들을 넣어 실행시킬 수 있다.

예를 들어, counter 프로그램에서 더하기 버튼을 클릭했을 때 바로 더하지 않고 3초를 기다렸다가 더해지도록 구현하려면 미들웨어를 사용해야한다. dispatch가 되자마자 즉시 action이 reducer에게 전달되어 새로운 state를 반환해버리기 때문이다. 여기서 “3초를 기다리는 작업" 작업을 미들웨어가 해주는 것이다.

보통 우리가 리덕스 미들웨어를 사용하는 이유는 서버와의 통신을 위함이 대부분이고, 그 중에서도 많이 사용되고 있는 리덕스 미들웨어가 Redux-thunk 이다.

 

Thunk

액션 객체 대신 함수를 반환하는 액션 생성자를 호출할 수 있는 미들웨어이다. Redux Toolkit의 configureStore기능은 기본적으로 Thunk 미들웨어를 자동으로 설정한다.

thunk 함수를 사용하면 dispatch 시 객체가 아닌 함수를 dispatch 할 수 있게 해준다. 즉 dispatch(객체) 가 아니라 dispatch(함수)를 할 수 있게 되는 것이다. 이를 통해 state 업데이트 전, 과정의 중간에서 실행되기 원하는 작업을 함수안에 넣어 해당 작업이 실행될 수 있도록 한다. 

dispatch(함수) → **함수실행** → 함수안에서 dispatch(객체)

 

Redux Tool kit 에서의 비동기 작업

Redux Toolkit은 내부적으로 thunk를 내장하고 있어서, 다른 미들웨어를 사용하지 않고도 비동기 처리를 할 수 있다.

물론 Redux Toolkit의 비동기 처리 기능을 사용하지 않고, 컴포넌트 내부의 useEffect()에서 API 호출을 하는 것도 가능하다. 실제로 지금까지 그렇게 해왔다. 다만, Redux Toolkit의 비동기 기능을 사용하면, 컴포넌트 외부에서 비동기 처리를 할 수 있기 때문에 관심사 분리가 가능하다는 장점이 있다(컴포넌트 관련 코드에서 API 요청 부분을 간단히 할 수 있음).

createAsyncThunk createSlice를 사용하여 Redux Toolkit만으로 비동기 처리를 쉽게 할 수 있으며, redux-saga에서만 사용할 수 있던 기능(이미 호출한 API 요청 취소하기 등)까지 사용할 수 있다.

 

createAsyncThunk

createAsyncThunk의 첫번째 파라미터에는 action의 타입을 넣고, 두번째 파라미터에는 처리할 비동기 함수와 그 안에 리턴할 payload를 넣어준다.

createAsyncThunk(typePrefix: string, payloadCreator: AsyncThunkPayloadCreator,
options?: AsyncThunkOptions): AsyncThunk

 

예시를 살펴보자.

import { createAsyncThunk } from '@reduxjs/toolkit'

export const login = createAsyncThunk(
  // action type
  "auth/login",
  async (loginData, { rejectWithValue }) => {
    try{
    // 처리할 비동기 로직
      const response = await axios.post(`${API_URL}/v1/login`, loginData);
      // action의 payload
      return response.data.data;
    }catch(err){
      return rejectWithValue(error.response.data);
    }
  }
);

createAsyncThunk는 thunk action creator를 반환한다. 위의 경우를 예로 들면, 다음 세 가지 thunk action creator가 반환된다.

  • login.pending: 'auth/login/pending' 액션을 디스패치하는 thunk action creator
  • login.fulfilled: 'auth/login/fulfilled' 액션을 디스패치하는 thunk action creator
  • login.rejected: 'auth/login/rejected' 액션을 디스패치하는 thunk action creator

아래는 비동기 함수가 fulfilled 되었을 때, action의 payload로 state를 변경해주는 로직이다.

// extraReducer에 비동기 함수의 pending, fulfilled, rejected를 처리할 내용을 넣어준다.

const authSlice = createSlice({
  name: "auth",
  initialState,
  reducers: {
    logout: (state, action) => {},
  },
  extraReducers: builder => {
    builder.addCase(login.pending, (state) =>{
      state.status = "loading";
    })
    builder.addCase(login.fulfilled, (state, { payload }) => {
      state.status = "success";
      state.isAuth = true;
      state.user = payload;
    });
    builder.addCase(login.rejected, (state, action) => {
      state.status = "failed";
      state.error = action.payload;
    });
  },
});

위와 같이 extraReducer에 파라미터인 builder를 통해 addCase를 사용하여 case(pending 상태에 대한 처리, fulfilled에 대한 처리 등)를 등록해준다. promise 결과가 pending이면 대기 중인 상태인 것이고, fulfilled면 성공, rejected면 거절이므로 상황에 맞게끔 반환되는 값을 state에 지정해주면 된다.

오류 처리

createAsyncThunk는 항상 이행된 프로미스만 반환하기에 비동기 처리가 실패했을 경우, 서버에서 이에 대한 오류 내역을 보내도 해당 내역을 사용하고 싶다면 따로 그에 대한 처리를 해줘야한다.

따라서 이를 해결하기 위해서는 createAsyncThunk내에서 오류를 발견하고, 이를 반환해야 한다. 오류를 반환하기 위해선 rejectWithValue를 사용한다. 에러가 발생하면 rejectWithValue의 파라미터 값으로 보내지게 되고, 이를 return하여 에러를 발생시킨다.

  extraReducers: builder => {
    // The `builder` callback form is used here because it provides correctly typed reducers from the action creators
    builder.addCase(login.fulfilled, (state, { payload }) => {
    state.status = "success";
      state.isAuth = true;
      state.user = payload;
    });
    builder.addCase(login.rejected, (state, action) => {
    state.status = "failed";
      if (action.payload) {
        state.error = action.payload;
      } else {
        state.error = action.error.message;
      }
   }
);

builder.addCase(login.rejected) 에서 action.payload가 존재한다는 뜻은 서버에서 보내준 에러 메세지를 받았다는 뜻이므로, 이를 state.error에 저장시킨다. 반대로 action.payload가 존재하지 않는다는 것은 서버에서 따로 에러처리를 해준게 없다는 뜻이기에, 기존에 처리하던 방식대로, action.error를 가져와 사용하게 된다.

사용

이렇게 만든 thunk와 slice는 다음과 같이 컴포넌트에서 사용할 수 있다.

dispatch(login(데이터));

useDispatch를 통해 createAsyncThunk를 통해 만든 Thunk 함수를 호출하고, useSelecter를 통해 state값을 동기화 시켜준다.

 

참고한 블로그)

https://velog.io/@wkahd01/redux-toolkit%EC%97%90%EC%84%9C-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0

https://velog.io/@raejoonee/createAsyncThunk

https://narup.tistory.com/257

https://subtlething.tistory.com/85

 

우연히 강의에서 redux toolkit에 관한 내용을 듣고 이거저거 서치해보다가 createAsyncThunk를 알게되어 몇 시간의 구글링 끝에 간단하게나마 내용 정리를 해보았다. 리덕스 너 왜이렇게 어려운거니ㅠㅠ 오늘 내용 중 오류 처리 부분은 특히나 더 살에 와닿지가 않는다... 구글링을 정말 많이 했지만 상태관리 라이브러리 자체에 익숙치 않아서 그런가 모든 내용이 다 생소하다!!! 리덕스 으으아아아아아악!!!!! ㅠㅠ 리코일은 또 언제 공부하는데 ㅠㅠ 리덕스도 이렇게 할게 많다고!!!! 

상태관리 라이브러리를 사용하는 API 통신을 해본 적도 없었을 뿐더러 상태관리 라이브러리를 사용하여 API 통신을 처리하는 관심사 분리를 것을 적용해본 적도 없었고, 리덕스와 리덕스 툴킷에 대해 공부하는 과정에 있어 상기의 내용이 아직 완벽하게 이해가 가지는 않지만 다시 이 글을 찾게되면 지금보다는 리덕스 툴킷에 익숙해져있기를! 제발!!!

 

반응형