React _ Redux Toolkit의 thunk를 활용한 비동기 통신 이해하기

kyle kwon·2022년 11월 23일
3

React

목록 보기
11/15
post-thumbnail

Prologue

React로 프론트엔드 개발 시 Redux를 활용한 전역 상태 관리를 하게 됩니다. Redux의 전역 상태 관리의 프로세스는 다음과 같습니다.

  1. 해당 UI에 이벤트 발생
  2. Event Handler에서 action 객체가 Dispatch 됨
  3. Dispatch 전달한 action 객체와 state를 Reducer라는 함수에서 받아, 새로운 state를 반환
  4. UI에 반영

단순한 원시값 또는 참조값을 프론트엔드 로컬 환경에서 다룰 때는 위와 같은 프로세스만 거쳐도 무방합니다. 하지만, 백엔드와 데이터를 주고받는, 즉 API로부터 데이터 request를 보내고 response를 받는 비동기 통신을 하려면, 조금 다른 프로세스가 필요합니다.

Redux에서는 Reducer 함수에서 action 객체와 state 객체를 이용해 새로운 state를 반환하기 전에 추가적인 작업을 할 수 있도록, 미들웨어라는 개념을 도입합니다.

이 미들웨어에서 하는 추가적인 작업들은 예로 다음과 같습니다.

  1. 특정 조건을 걸어 액션을 무시하게 할 수 있습니다.
  2. action(객체)을 콘솔에서 출력할 수 있습니다.
  3. action(객체)가 dispatch 되었을 때, 이를 미들웨어에서 수정해서 Reducer에 전달할 수 있습니다.

주된 미들웨어의 용도는 async, 즉 비동기 작업을 처리할 때 많이 사용합니다.
위에서 이야기한 API를 연동한 비동기 통신이겠죠.
아래의 다이어그램과 같은 모습의 프로세스를 거칩니다.

비동기 작업과 관련된 대표적인 미들웨어 라이브러리로 redux-thunk, redux-saga가 있습니다.

이 중 redux-thunk가 redux toolkit을 사용할 경우 내장되어 있어, 손쉽게 사용할 수 있습니다.

Redux Toolkit에 내장된 thunk를 활용하는 방법에 대해서 알아보겠습니다.




01 thunk 생성

Redux Thunk의 장점이자 도입할 수 밖에 없는 이유

Redux Thunkaction Creator가 리턴하는 것을 객체가 아닌 함수를 사용할 수 있게 합니다. 함수를 리턴하게 되면, 해당 함수의 실행이 끝난 뒤에 값으로 action을 넘겨주기 때문에, redux thunk 없이 동기 통신을 하던 기존의 경우와 달리 action creator가 반환하는 객체로는 처리하지 못했던 비동기 작업redux thunk를 통해 가능하게 되었습니다.

createAsyncThunk

이 함수는 1) 액션 타입 문자열, 2) promise를 반환하는 비동기 함수, 그리고 3) 추가 옵션을 인자로 받는 함수로, thunk Action Creator를 반환합니다.

  1. 액션 타입 문자열
  2. 이행된 promise를 반환하는 비동기 (asynchronous) 함수
  3. 추가 옵션 (비동기 처리 전 취소, 비동기 실행 중 취소 등의 옵션)

즉, 비동기 작업을 처리하는 thunk 속성을 가진 action 객체를 만들어 준다고 생각하면 됩니다.

createAsyncThunk 활용 예시

예시를 들어 기본적인 구조를 살펴보겠습니다.

export const asyncUpFetch = createAsyncThunk('asyncThunk/asyncUpFetch', async () => {
  const response = await axios.get('https://jsonplaceholder.typicode.com/todos/1')
  const data = response.data
  console.log(data)
  return data
})

axios를 활용해 promise를 반환하는 비동기 함수를 두 번째 인자로 넣은 것이 보일 것입니다.

위에서 thunk Action Creator를 반환한다고 했는데, 3가지 형태의 다른 thunk action creator를 반환합니다.

다음과 같습니다.

  1. asyncUpFetch.pending: 'asyncThunk/asyncUpFetch/pending' action을 dispatch하는 action creator
  2. asyncUpFetch.fulfilled: 'asyncThunk/asyncUpFetch/fulfilled' action을 dispatch하는 action creator
  3. asyncUpFetch.rejected: 'asyncThunk/asyncUpFetch/rejected' action을 dispatch하는 action creator

위의 action들이 Dispatch되면, thunk는 아래의 프로세스를 내부적으로 거칩니다.

  1. 우선, pending action을 dispatch 합니다.

  2. payloadCreator callback(위의 비동기 함수 : async-await)을 호출하고, promise가 반환되기를 기다립니다.

  3. promise가 반환되면, promise의 상태에 따라 다음의 과정을 거칩니다.

    a. promise가 이행된 상태라면, action.payloadfulfilled action에 담아 dispatch 합니다.
    b. promise가 거부된 상태라면, rejected action을 dispatch 하되 rejectedValue(value) 함수의 반환값에 따라 action에 어떤 값을 넘길 지 결정합니다.
    c. dispatch된 action을 담고 있는 fulfilled(이행된) promise를 반환합니다.


최종적으로 항상 이행된(fulfilled) promise를 반환하는 이유는?

dispatch 결과가 사용되지 않는 경우에도 promise가 거부되는 상황을 방지하기 위해서 입니다.



02 thunk 활용

thunk를 생성했으므로, 다음과 같이 createSlice를 활용해 store에 저장될 slice를 만듭니다.

export const asyncThunkSlice = createSlice({
  name: 'asyncThunk',
  initialState: {
    value: 0,
    status: 'Welcome',
  },
  extraReducers: (builder) => {
    builder.addCase(asyncUpFetch.pending, (state, action) => {
      state.status = 'Loading'
    })
    builder.addCase(asyncUpFetch.fulfilled, (state, action) => {
      state.value = action.payload
      state.status = 'Complete'
    })
    builder.addCase(asyncUpFetch.rejected, (state, action) => {
      state.status = 'Failed'
    })
  },
})
  1. name에는 우리가 createAsyncThunk를 활용해 thunk를 생성할 때 지정했던 액션 타입 문자열의 앞부분을 똑같이 적어주어야 합니다.

  2. initialState는 이전에 slice를 만들 때와 마찬가지로, 초기 상태값을 만들어 줍니다.

  3. extraReducers라는 처음 보는 필드가 있습니다.
    a. 기존 slice 생성 시, reducers 필드에 action에 따른 reducer를 담아놓았던 상황과 달리, extraReducers라는 다른 필드가 등장했습니다.

    b. 통상적인 동기적인 작업을 하는 redux를 활용한 전역 상태 관리에서는 reducers 필드를 활용해서 action creator를 자동 생성할 수 있지만, 비동기 작업 시에는 자동으로 action creator를 생성하지 못하기 때문에, extraReducers라는 필드를 활용해야 합니다.


그러면, 이벤트 핸들러를 통해 dispatch 할 UI에 useSelectoruseDispatch를 통해 특정 요소에 dispatch(thunk 함수)를 붙여 비동기 통신을 하는지 살펴 보겠습니다.

// store.js
export const store = configureStore({
  reducer: {
    asyncThunk: asyncThunkSlice.reducer,
  },
})


// Counter.js

import { useSelector } from 'react-redux'
import { asyncUpFetch } from '../store/asyncThunkSlice'
import { useAppDispatch } from '../store/counterSlice'

function Counter() {
  const asyncValue = useSelector((state) => {
    return state.asyncThunk.value.title
  })
  const asyncStatus = useSelector((state) => state.asyncThunk.status)
  const dispatch = useAppDispatch()
  return (
    <div style={{ marginTop: '20px' }}>
      <button onClick={() => dispatch(asyncUpFetch())}>+ async Thunk</button>
      <div style={{ marginTop: '20px' }}>
        <span
          style={{
            marginRight: '0.25rem',
            padding: '0.25rem 0.5rem',
            width: '100px',
            border: '1px solid #e5e5e5',
          }}
        >
          {asyncValue}
        </span>
        <span>{asyncStatus}</span>
      </div>
    </div>
  )
}

export default Counter

button 요소에 onClick 리스너를 붙이고, 핸들러 함수에 callback의 형태로 dispatch(asyncUpFecth())를 넣어주었습니다.
즉, 클릭이 발생하면 dispatch가 thunk action 객체를 middleware로 보내고, middleware에서는 이행된 프로미스 형태, 함수를 리턴하고, 해당 함수의 실행이 끝나면 action 객체를 반환합니다. 그리고 이 action 객체와 현재 state를 받아 reducer에서 새로운 state를 반환하여 UI에 반영하게 됩니다.


01 <+ async thunk 버튼 누르기 전>

02 <+ async thunk 버튼 누른 후>

중간에 pending 된 상태를 잠깐 거쳐 complete, 즉 이행된 프로미스를 반환한 결과를 볼 수 있습니다. 아주 잠깐 사이에 보여지기 때문에 코드 실행 후 확인해보면 좋을 것 같습니다.




Conclusion

이렇게 redux thunk를 활용한 미들웨어의 속성 및 비동기 통신을 하는 방법에 대해서 알아보았습니다. 미들웨어라는 개념이 익숙하지 않아, 이해하는 데 많이 어려웠지만, 프로젝트에 적용해서 지속적으로 다루게 되면 이해도가 훨씬 높아지지 않을 까 기대합니다.

profile
FrontEnd Developer - 현재 블로그를 kyledot.netlify.app으로 이전하였습니다.

0개의 댓글