[Redux] Redux-thunk

mokyoungg·2020년 10월 7일
15

Redux

목록 보기
5/7

출처는 공식 문서와 Udemy의 Modern React with Redux [2020 Update] 강의 입니다.
공식 문서 : https://ko.redux.js.org/
Udemy 강의 : https://www.udemy.com/

공식 페이지 설명 (출처 : https://ko.redux.js.org/basics/reducers)
Reducer가 반드시 순수해야 한다는 점만 기억해두자.
인수가 주어지면, 다음 상태를 계산해서 반환하면 된다.예기치 못한 일은 없어야 한다.
사이드 이펙트도 없어야 한다. API 호출도 안된다. 변경도 안된다. 계산만 가능하다.

리듀서의 특징은 순수해야 한다는 점이다.
그럼 API 호출을 통해 데이터를 받아오려면 어떻게 해야하나?



1. middleware란 무엇인가?

미들웨어는 dispatch 함수를 결합해서 새 dispatch 함수를 반환하는 고차함수이다.
이들은 비동기 action을 acion으로 전환한다. action을 로깅하거나, 라우팅과 같은 부수효과를 일으키거나,
비동기 API 호출을 일련의 동기 action으로 바꾸는데 유용하다.

출처 : https://ko.redux.js.org/understanding/thinking-in-redux/glossary#%EB%94%94%EC%8A%A4%ED%8C%A8%EC%B9%98-%ED%95%A8%EC%88%98

Redux에 임의의 기능을 넣어 확장하는 방법으로는 미들웨어를 추천합니다.
미들웨어의 중요한 기능 중 하나는 조합이 가능하다는 점입니다.
가장 일반적인 미들웨어의 사용법은 많은 보일러플레이트나 Rx와 같은 라이브러리에 대한 의존성 없이도 비동기 액션을 지원하는 것입니다. 이는 비동기 액션을 보통의 액션처럼 보내게 해줌으로써 이루어집니다.
예를 들어, redux-thunk는 액션 생산자가 디스패치 함수를 통해 제어를 역전할 수 있게 합니다.
액션 생산자는 dispatch를 인수로 받아 비동기적으로 호출할 수 있습니다.
이러 함수들을 thunk라고 불립니다..(생략)

출처 : https://ko.redux.js.org/api/applymiddleware

미들웨어는 redux 라이브러리에서 적용된다.
미들웨어는 redux 저장소의 action을 변형하는 기능이다.
이를 통해 redux에 새로운 기능을 추가하며,
redux-thunk의 경우 네트워크 요청을 만드는데 도움이 되는 미들웨어이다.


2. redux-thunk는 무엇인가?

redux의 부족한 부분을 채워주는 기능을 미들웨어이다.
그 중 네트워크 요청의 기능에 도움을 주는 미들웨어 중에서 가장 많이 사용하는 것이 redux-thunk다.

Redux Thunk 미들웨어는 당신이 액션 대신 함수를 반환하는 액션 크리에이터를 쓸 수 있게 해준다.
Thunk는 action의 dispatch를 지연시키는데 사용될 수 있으며, 특정 조건이 충족되는 경우에만
dispatch할 수 있다. 내부 함수는 dispatch와 getState를 매개변수로 둔다.
출처 : https://www.npmjs.com/package/redux-thunk


3. redux-thunk를 사용하는 이유, redux는 왜 API 호출이 안되는가?

redux-thunk라는 미들웨어는 API 호출이 필요할 때 쓴다.
근데 Redux는 왜 API 호출을 하면 안되는가?

3-1. async / await 로 호출하면?(src/actions/index.js)

export const fetchData = async () => {
  //임의의 api 주소에 async await으로 (get) 요청을 하였다.
  const response = await APIadress.get('/data')
 
  return {
    type: 'FETCH_DATA',
    payload: response
  }
}

위와 같이 action을 작성하면 다음과 같은 에러 코드가 뜬다.

action must be plain objects. Use custom middleware for async actions

처음 이것을 보면 이해가 안 된다.
response는 plain objects라고 생각되기 때문이다.(console에 찍어보니까 객체 맞는데?)
하지만 사실 async await 은 엄청나게 큰 함수이다.
그 함수가 종료되고 return 되는 것이 내가 console에서 보는 plain objects 형태의 데이터다.
따라서, reducer로 dispatch 되는 최초의 action 형태는 plain이 아니라 async await의 함수의 형태이다.
그래서 오류가 난다.

3-2. promise 로 호출하면?(src/actions/index.js)

export const fetchData = async () => {
  //임의의 api 주소에 (get) 요청을 promise 형태로 하였다.
  const promise = await APIadress.get('/data')
 
  return {
    type: 'FETCH_DATA',
    payload: response
  }
}

redux가 실행될 때,
action creator를 호출하고 action이 dispatch 되어 reducer로 이동할 때.
이 모든 단계는 순식간에 실행된다고 한다.
Promise를 사용하면 순식간에 실행된 reducer에서 '반환된 데이터가 아직 없다'(객체에 아무것도 없다!)라고 판단한다.
따라서 Promise도 사용할 수 없다.

3-3. redux-thunk를 쓰면 어떻게 되는가?

Redux Thunk 미들웨어는 당신이 액션 대신 함수를 반환하는 액션 크리에이터를 쓸 수 있게 해준다.(2번에서 확인)

redux-thunk는 액션을 반환할 때 객체가 아니라 함수를 반환할 수 있게 해준다.
다른 말로 액션이 함수의 형태일 때 그 함수를 실행하고 또 실행하여 객체가 되었을 때 reducer로 dispatch 한다.

dispatch 이후에 Redux thunk가 실행된다.

action creator -> action -> dispatch -> Redux Thunk

Redux thunk에서 dispatch 된 액션이 함수인지 아닌지 판단한다.

Redux Thunk -> 함수인가? -> 아니요.(Plain Object) -> dispatch -> Reducer

Redux Thunk -> 함수인가? -> 예 -> 함수실행 -> dispatch -> 함수인가? -> 아니요(Plain Object) -> Reducer

함수일 때 Redux thunk는 다음과 같이 실행된다.(사실 이 부분은 무슨 말인지 잘 모르겠다.)

  • dispatch와 getState 함수를 호출
  • 함수가 실행
  • 요청이 끝날 때까지 대기
  • 요청이 끝나면 이를 dispatch(수동으로)

음... dispatch 라는 것은 action을 reducer로 보내는 것이다.
react-redux의 connect()의 도움으로 dispatch는 자동으로 이루어 진다.
그러나 action의 형태가 Plain Objects가 아닌 경우에는 에러가 뜨기 때문에,
Redux-thunk에서 함수를 처리하고 Plain Objects 형태가 나오면 이를 dispatch 한다.(자동이 아니라 수동으로)
그러니까... 아 이 부분은 내가 정리해서 따로 제출할게. 라고 생각하면 될 것 같다.

3-4. async await를 사용한 올바른 코드(src/actions/index.js)

export const fetchData = () => {
  // 이 액션 생성자(fetchData)는 함수를 반환한다.
  // 함수는 dispatch와 getState를 매개변수로 가진다.
  return async function(dispatch, getState) {
    const response = await APIadress.get('/data')
   
    // 요청이 완료되면 여기서 직접 dispatch 한다.
    dispatch ({ type: 'FETCH_DATE', payload: response })
  }
}

위의 코드를 리팩토링하면(이러한 형태의 코드로 많이 작성한다.)

export const fetchData = () => async dispatch => {
  const response = await APIadress.get('/data');
 
  dispatch({ type: 'FETCH_DATA', payload: response })
}


4. 일반적인 Redux의 Data Loading 순서

redux-thunk를 사용한 redux의 data loading은 다음과 같은 순서로 작동한다.

<컴포넌트 마운트에서(라이프사이클) 액션 생성자를 호출한다.>
1. 컴포넌트가 화면에 렌더링 됨
2. 컴포넌트에서 'ComoponentDidMount()' 라이프사이클 메서드를 호출
3. 'ComponentDidMount()' 에서 action creator를 호출함

<API 요청은 Action Creator에서 담당하며, Redux-thunk가 여기서 작동한다.>
4. Action creator가 코드를 실행하며 API 요청
5. API가 데이터를 응답
6. Action creator는 'payload'에 가져온 데이터를 할당하고 action을 반환한다.

<redux store에서 새로운 state가 생성된다.(dispatch 된 action의 payload(데이터))>
7. reducer는 action을 보고 'payload'에 할당된 데이터를 가져온다.
8. 새로운 state가 생성되었기 때문에 react 앱은 리렌더링 된다.



5. 실제로 사용해보기(코드 예시)

5-1. redux-thunk 설치

npm install --save redux-thunk

5-2. redux-thunk 사용 환경 세팅(src/index.js)

1
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";

2
// redux에서 applyMiddleware도 가져온다.
import { createStore, applyMiddleware } from "redux";
// redux-thunk에서 thunk를 가져온다.
import thunk from "redux-thunk";

3
import App from "./components/App";
import reducers from "./reducers";

4
// store라는 변수에 다음과 같이 작성한다.
const store = createStore(reducers, applyMiddleware(thunk));

5
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.querySelector("#root")
);

5-3. action으로 API 호출 (src/actions/index.js)

export const fetchData = () => async (dispatch) => {
  const response = await APIadress.get('/data');

// 요청이 완료되면 여기서 직접 dispatch 한다.
  dispatch({ type: 'FETCH_DATA', payload: response })
}

5-3. reducer에서 action 받기 (src/reducers/index.js)

1 
//redux에서 combineReducers 를 가져옴
import { combineReducers } from 'redux'

2
// switch 구문을 사용한 reducer
// 최초의 state는 빈 배열이다.([])
const datasReducer (state=[], action) => {
  switch(action.type){
    // action의 type의 값이 'FETCH_DATA' 인가?
    case 'FETCH_DATA':
      // 참(true)이면 그 액션의 payload를 반환한다.
      return action.payload;
    // 해당되는 액션 type이 없으면
    default:
      // 기존의 state를 반환한다.
      return state;
  }
}

3
// createStore를 위한 reducer로 넘어간다.
export default combineReducers({
  datas: datasReducer,
})

받은 데이터를 활용하여 reducer의 state(배열형태)를 업데이트하려면
[...state, action.payload] 맞나.
이 부분에 대해선 더 공부를 해야겠다.

profile
생경하다.

1개의 댓글

comment-user-thumbnail
2021년 10월 15일

오... 감사합니다!! 이해가 잘되네요.

답글 달기