'리액트를 다루는 기술' 18장, 리덕스 미들웨어를 통한 비동기 작업 관리(1/2)

Jake_Young·2020년 9월 9일
0
post-thumbnail

🙄 미들웨어란?

미들웨어 코드의 기본 구조

  • 리덕스 미들웨어는 액션을 디스패치했을 때 리듀서에서 이를 처리하기에 앞서 사전에 지정된 작업들을 실행한다.
  • 미들웨어는 액션과 리듀서 사이의 중간자라고 볼 수 있다.
  • 미들웨어가 할 수 있는 일에는 액션을 단순히 콘솔에 기록하거나, 전달받은 액션 정보를 기반으로 액션을 아예 취소하거나 다른 종류의 액션을 추가로 디스패치할 수 있다.
// 미들웨어의 기본 구조(화살표 함수)
const middleware = store => next => action => {
  // 실제 실행할 작업
};
export default middleware

// 미들웨어의 기본 구조(일반 함수)
const middleware = function middleware(store){
  return function(next){
    return function(action){
      // 실제 실행할 작업
    }
  }
}
  • store는 리덕스 스토어 인스턴스를, action은 디스패치된 액션을 가리킨다.
  • 반면에 next 파라미터는 함수 형태이며, store.dispatch와 비슷한 역할을 한다.
  • 하지만 큰 차이점은 next(action)을 호출하면 그 다음에 처리할 미들웨어에게 액션을 넘겨주지만, 만약 그 다음 미들웨어가 없다면 리듀서에게 액션을 넘겨준다는 것이 다르다.
  • 미들웨어 내부에서 store.dispatch를 사용하면 첫 번째 미들웨어부터 다시 처리한다.
  • 미들웨어에서 next를 사용하지 않으면 액션이 리듀서에 전달되지 않는다. 액션이 무시된다.

로그 남기는 미들웨어 코드

// middleware.js
const middleware = store => next => action => {
  console.log('이전 상태', store.getState())
  console.log('액션', action)
  next(action)
  console.log('다음 상태', store.getState())
};
export default middleware

// index.js
import { createStore, applyMiddleware } from 'redux'
...
const store = createStore(rootReducer, applyMiddleware(middleware));
...

// redux-logger와 거의 비슷한 기능을 수행함

😎 비동기 작업을 처리하는 미들웨어

Redux-Thunk

  • 비동기 작업을 처리할 때 가장 많이 사용되는 미들웨어
  • 객체가 아닌 함수 형태의 액션을 디스패치할 수 있게 해줌
  • 리덕스의 창시자인 댄아브라모프(Dan Abramov)가 만들었음
  • Thunk란 특정 작업을 나중에 할 수 있도록 미루기 위해 함수 형태로 감싼 것을 의미
  • ReduxThunk를 사용하고 싶다면 index.js의 applyMiddleware의 인자로 넣어준다
const sampleThunk = () => (dispatch, getState) => {
  // 현재 상태를 참조할 수 있고,
  // 새 액션을 디스패치할 수도 있다.
}

Exercise

// index.js
import React from "react";
import ReactDOM from "react-dom";
import { createStore, applyMiddleware } from "redux";
import { Provider } from "react-redux";
import App from "./App";
import rootReducer from "./modules";
import { createLogger } from "redux-logger";
import ReduxThunk from "redux-thunk";

const rootElement = document.getElementById("root");
const logger = createLogger();
const store = createStore(rootReducer, applyMiddleware(logger, ReduxThunk));

ReactDOM.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>,
  rootElement
);

// app.js
import React from "react";
import "./styles.css";
import CounterContainer from "./containers/CounterContainer";
import SampleContainer from "./containers/SampleContainer";

export default function App() {
  return (
    <div className="App">
      <SampleContainer />
      <CounterContainer />
    </div>
  );
}

// /src/components/Counter.js
import React from "react";

const Counter = ({ onIncrease, onDecrease, number }) => {
  return (
    <div>
      <h1>{number}</h1>
      <button
        onClick={() => {
          onIncrease();
        }}
      >
        +1
      </button>
      <button
        onClick={() => {
          onDecrease();
        }}
      >
        -1
      </button>
    </div>
  );
};
export default Counter;

// /src/components/Sample.js
import React from "react";

const Sample = ({ loadingPost, loadingUsers, post, users }) => {
  return (
    <div>
      <section>
        <h1>Post</h1>
        {loadingPost && "loading..."}
        {!loadingPost && post && (
          <div>
            <h3>{post.title}</h3>
            <h3>{post.body}</h3>
          </div>
        )}
      </section>
      <hr />
      <section>
        <h1>users list</h1>
        {loadingUsers && "loading..."}
        {!loadingUsers && users && (
          <ul>
            {users.map((user) => (
              <li key={user.id}>
                {user.username} ({user.email})
              </li>
            ))}
          </ul>
        )}
      </section>
    </div>
  );
};

export default Sample;

// src/containers/CounterContainer.js
import React from "react";
import { connect } from "react-redux";
import { increaseAsync, decreaseAsync } from "../modules/counter";
import Counter from "../components/Counter";

const CounterContainer = ({ number, increaseAsync, decreaseAsync }) => {
  return (
    <Counter
      number={number}
      onIncrease={increaseAsync}
      onDecrease={decreaseAsync}
    />
  );
};

export default connect(
  (state) => ({
    number: state.counter
  }),
  {
    increaseAsync,
    decreaseAsync
  }
)(CounterContainer);

// src/containers/SampleContainer.js
import React, { useEffect } from "react";
import { connect } from "react-redux";
import Sample from "../components/Sample";
import { getPost, getUsers } from "../modules/sample";

const SampleContainer = ({
  getPost,
  getUsers,
  post,
  users,
  loadingPost,
  loadingUsers
}) => {
  useEffect(() => {
    const fn = async () => {
      try {
        await getPost(1);
        await getUsers(1);
      } catch (e) {
        console.log(e);
      }
    };
    fn();
  }, [getPost, getUsers]);
  return (
    <Sample
      post={post}
      users={users}
      loadingPost={loadingPost}
      loadingUsers={loadingUsers}
    />
  );
};

export default connect(
  ({ sample, loading }) => ({
    post: sample.post,
    users: sample.users,
    loadingPost: loading["sample/GET_POST"],
    loadingUsers: loading["sample/GET_USERS"]
  }),
  {
    getPost,
    getUsers
  }
)(SampleContainer);

// src/lib/api.js
import axios from "axios";

export const getPost = (id) =>
  axios.get(`https://jsonplaceholder.typicode.com/posts/${id}`);

export const getUsers = (id) =>
  axios.get(`https://jsonplaceholder.typicode.com/users`);

// src/lib/createRequestThunk.js
import { startLoading, finishLoading } from "../modules/loading";

export default function createRequestThunk(type, request) {
  const SUCCESS = `${type}_SUCCESS`;
  const FAILURE = `${type}_FAILURE`;
  return (params) => async (dispatch) => {
    dispatch({ type });
    dispatch(startLoading(type));
    try {
      const response = await request(params);
      dispatch({ type: SUCCESS, payload: response.data });
      dispatch(finishLoading(type));
    } catch (e) {
      dispatch({ type: FAILURE, payload: e, error: true });
      dispatch(finishLoading(type));
      throw e;
    }
  };
}

// src/modules/index.js
import { combineReducers } from "redux";
import counter from "./counter";
import sample from "./sample";
import loading from "./loading";

const rootReducer = combineReducers({
  counter,
  loading,
  sample
});

export default rootReducer;

// src/modules/counter.js
import { createAction, handleActions } from "redux-actions";

const INCREASE = "counter/INCREASE";
const DECREASE = "counter/DECREASE";

export const increase = createAction(INCREASE);
export const decrease = createAction(DECREASE);

export const increaseAsync = () => (dispatch) => {
  setTimeout(() => {
    dispatch(increase());
  }, 1000);
};

export const decreaseAsync = () => (dispatch) => {
  setTimeout(() => {
    dispatch(decrease());
  }, 1000);
};

const initialState = 0;

const counter = handleActions(
  {
    [INCREASE]: (state) => state + 1,
    [DECREASE]: (state) => state - 1
  },
  initialState
);

export default counter;

// src/modules/loading.js
import { createAction, handleActions } from "redux-actions";

const START_LOADING = "loading/START_LOADING";
const FINISH_LOADING = "loading/FINISH_LOADING";

// 요청을 위한 액션 타입을 payload로 설정한다 (예: "sample/GET_POST")

export const startLoading = createAction(
  START_LOADING,
  (requestType) => requestType
);

export const finishLoading = createAction(
  FINISH_LOADING,
  (requestType) => requestType
);

const initialState = {};

const loading = handleActions(
  {
    [START_LOADING]: (state, action) => ({
      ...state,
      [action.payload]: true
    }),
    [FINISH_LOADING]: (state, action) => ({
      ...state,
      [action.payload]: false
    })
  },
  initialState
);

export default loading;

// src/modules/sample.js
import { handleActions } from "redux-actions";
import * as api from "../lib/api";
import createRequestThunk from "../lib/createRequestThunk";

const GET_POST = "sample/GET_POST";
const GET_POST_SUCCESS = "sample/GET_POST_SUCCESS";

const GET_USERS = "sample/GET_USER";
const GET_USERS_SUCCESS = "sample/GET_USER_SUCCESS";

export const getPost = createRequestThunk(GET_POST, api.getPost);
export const getUsers = createRequestThunk(GET_USERS, api.getUsers);

const initialState = {
  post: null,
  users: null
};

const sample = handleActions(
  {
    [GET_POST_SUCCESS]: (state, action) => ({
      ...state,
      post: action.payload
    }),
    [GET_USERS_SUCCESS]: (state, action) => ({
      ...state,
      users: action.payload
    })
  },
  initialState
);

export default sample;

Redux-Saga

  • 이건 다음 포스트에서..
profile
자바스크립트와 파이썬 그리고 컴퓨터와 네트워크

0개의 댓글