리덕스 미들웨어는 무엇인가? (2) redux-thunk

youthfulhps·2020년 7월 12일
0

Redux

목록 보기
5/5
post-thumbnail

이전글에 이어서 이번 글에서는 비동기작업 처리를 도와주는 리덕스 미들웨어를 공부해보도록 하겠습니다.

🧰 redux-thunk, 그리고 thunk

redux-thunk는 리덕스 미들웨어에서 비동기 작업을 처리하는데 사용하는 미들웨어로 비동기 작업을 다루는 미들웨어 중에서 가장 대표적인 리덕스 미들웨어입니다.

위키백과-썽크 에서 서술된 썽크의 정의를 보면, "썽크는 주로 연산 결과가 필요할 때까지 연산을 지연시키는 용도로 사용되거나, " 라고 언급되어 있습니다.

그렇다면, 연산 결과를 지연시키는 방법은 무엇이 있을까요? 가령, 1+2를 출력하고 싶다면, 아래와 같이 코드를 작성하면 됩니다.

console.log(1 + 2);

하지만, 썽크의 정의처럼 연산 결과가 필요할 때까지 연산을 지연시키고 싶다면 아래와 같이 함수로 코드를 감싼 후, 필요할 때 함수를 호출해주면 됩니다.

const foo = () => {
  console.log(1+2);
}

🤨 그럼, redux-thunk는 어떻게 thunk의 개념을 적용해?

우리가 알고 있는 기존의 리덕스에서 액션 생성 함수는 액션을 객체 형태로 반환해주는 함수입니다. 하지만, 리덕스 썽크를 이용하면 액션 생성 함수는 객체가 아닌 함수를 반환할 수 있게 해줍니다. 즉, 객체가 아닌 함수를 반환해줌으로서 필요할 때 함수를 호출할 수 있는 썽크의 형태가 되는 것입니다.

🙂 한번 사용해볼까?, 웹 요청 처리하기

아래의 프로젝트는 noteapp-react-redux-thunk에 공개되어 있습니다.

✔ axios를 통한 날씨 정보 요청하기

백분이 불여일견, redux-thunk를 직접 사용해보겠습니다. 비동기작업의 대표적인 예시로 웹 요청을 처리해볼건데 요청 작업은 axios를 사용하도록 하겠습니다.

~$ yarn add axios

우리가 사용하고 있는 프로젝트 noteapp 상단에 현재 날씨를 요청해 데이터를 받아와 출력해주는 작업을 처리해보려고 합니다.

날씨 정보는 OpenWhetherMap 에서 무료로 제공합니다. 회원가입을 하고, 회원가입 후 'Current Weather Data'를 Subscribe하신 후, API key를 획득하시면 됩니다. 아래와 같은 URL의 {API KEY}에 넣어주시면 됩니다.

http://api.openweathermap.org/data/2.5/weather?q=Seoul&?units=metric&APPID={API_KEY}

우선, 요청이 잘 되는지 콘솔에 로그를 남겨보도록 하겠습니다.프로젝트의 App.js의 코드를 아래와 같이 수정해보겠습니다.

import React, { useEffect } from "react";
import "./App.css";
import "antd/dist/antd.css";
import axios from "axios";     //추가

// import ClassContainer from "./containers/ClassContainer";
import FunctionalContainer from "./containers/FunctionalContainer";

const App = () => {
  useEffect(() => {           //추가
    axios
      .get(
        "http://api.openweathermap.org/data/2.5/weather?q=Seoul&units=metric&APPID={API_Key}"
      )
      .then((response) => {
        console.log(response.data);
      });
  }, []);

  return (
    <div>
      <FunctionalContainer />
    </div>
  );
};

export default App;

앱이 실행되면, axios를 통해 요청을 보냈고, 응답이 성공적으로 전달되었다면 응답의 데이터를 출력하게끔 해주었습니다. axios는 Promise 기반의 HTTP Client이기 때문에 then을 통해 간결히 응답을 가공할 수 있습니다.(여기서, {API Key}에 본인의 api key를 넣어주시면 되고, {} 또한 지워주셔야 합니다.)

아래와 같이 잘 출력되었나요?


✔ redux-thunk 모듈 설치 및 적용

우선, redux-thunk 모듈을 설치해주어야 합니다.

~$ yarn add redux-thunk

설치가 완료되었다면, index.js에서 logger를 적용했듯이, 같은 방법으로 redux-thunk를 적용해줍니다.

import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
import { Provider } from "react-redux";

import { createStore, applyMiddleware } from "redux";
import rootReducer from "./store/modules";

import { createLogger } from "redux-logger";
import reduxThunk from "redux-thunk"; //추가

const reduxLogger = createLogger();

const store = createStore(
  rootReducer,
  applyMiddleware(reduxLogger, reduxThunk)
); //수정

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

serviceWorker.unregister();

✔ redux-thunk 모듈 생성

store/module/weather.js

위의 디렉토리에 파일을 생성해주고 아래와 같이 작성해주겠습니다.

import { handleActions } from "redux-actions";

import axios from "axios";

const GET_WEATHER_PENDING = "GET_WEATHER_PENDING";
const GET_WEATHER_SUCCESS = "GET_WEATHER_SUCCESS";
const GET_WEATHER_FAILURE = "GET_WEATHER_FAILURE";

function getAPI() {
  return axios.get(
    "http://api.openweathermap.org/data/2.5/weather?q=Seoul&units=metric&APPID={API_Key}"
  );
}

export const getWeather = () => async (dispatch) => {
  dispatch({ type: GET_WEATHER_PENDING });

  try {
    const response = await getAPI();
    dispatch({
      type: GET_WEATHER_SUCCESS,
      payload: response.data,
    });
  } catch (err) {
    dispatch({
      type: GET_WEATHER_FAILURE,
      payload: err,
    });
    throw err;
  }
};

const initialState = {
  pending: false,
  error: false,
  data: {
    area: "",
    temp: 0,
    weather: "",
  },
};

export default handleActions(
  {
    [GET_WEATHER_PENDING]: (state, action) => {
      return {
        ...state,
        pending: true,
        error: false,
      };
    },
    [GET_WEATHER_SUCCESS]: (state, action) => {
      const area = action.payload.name;
      const temp = action.payload.main.temp;
      const weather = action.payload.weather[0].main;

      return {
        ...state,
        pending: false,
        data: {
          area: area,
          temp: temp,
          weather: weather,
        },
      };
    },
    [GET_WEATHER_FAILURE]: (state, action) => {
      return {
        ...state,
        pending: false,
        error: true,
      };
    },
  },
  initialState
);

특징이라 하면, 하나의 요청에 요청 시작, 성공, 실패에 따른 세 가지의 액션 타입이 정의되어야 합니다.

리덕스는 동일한 입력에는 언제나 동일한 반환을 내는 순수한 함수로 작성되어야 하기 때문에 언제나 성공한 결과로 응답하지 않은 웹 요청과 같은 상황에서는 세 가지 액션으로 정의해주어야 원칙에 어긋나지 않습니다.

불러왔던 날씨 데이터에서 name(지역), temp(온도), weather(날씨)만 사용합니다.

모두 작성하셨다면, 루트리듀서에 추가해주세요!

import { combineReducers } from "redux";
import note from "./note";
import weather from './weather';   //추가

export default combineReducers({
  note,
  weather,   //추가
});

✔ Weather 컴포넌트 생성

src/components/Weather.js

import React from "react";
import { Button, Descriptions, Spin } from "antd";
import { LoadingOutlined, QuestionOutlined } from "@ant-design/icons";

const loadingIcon = <LoadingOutlined style={{ fontSize: 24 }} spin />;

const weather = ({ data, error, loading, getWeatherData }) => {
  return (
    <div
      style={{
        padding: "1rem 0.5rem",
        margin: "1rem 1rem",
        border: "1px solid rgba(0,0,0,0.1)",
      }}
    >
      <Button onClick={getWeatherData}>날씨 불러오기</Button>
      <Descriptions bordered title="Weather">
        <Descriptions.Item label="Area">
          {loading && <Spin indicator={loadingIcon} />}
          {error ? <QuestionOutlined /> : data.area}
        </Descriptions.Item>
        <Descriptions.Item label="Temp">
          {loading && <Spin indicator={loadingIcon} />}
          {error ? <QuestionOutlined /> : data.temp}
        </Descriptions.Item>
        <Descriptions.Item label="Weather">
          {loading && <Spin indicator={loadingIcon} />}
          {error ? <QuestionOutlined /> : data.weather}
        </Descriptions.Item>
      </Descriptions>
    </div>
  );
};

export default weather;

각각의 데이터(지역, 온도, 날씨)가 들어가는 곳에 조건문을 통해 유저에게 상태를 표시해주었습니다.

✔ WeatherContainer 컨테이너 생성

src/containers/WeatherContainer.js

import React from "react";
import { useSelector, useDispatch } from "react-redux";
import Weather from "../components/Weather";
import { getWeather } from "../store/modules/weather";

const WeatherContainer = () => {
  const dispatch = useDispatch();

  const { data, loading, error } = useSelector(({ weather }) => ({
    data: weather.data,
    loading: weather.loading,
    error: weather.error,
  }));

  const getWeatherData = () => {
    dispatch(getWeather());
  };

  return (
    <Weather
      data={data}
      error={error}
      loading={loading}
      getWeatherData={getWeatherData}
    />
  );
};

export default WeatherContainer;

✔ App.js 수정

import React from "react";
import "./App.css";
import "antd/dist/antd.css";

import FunctionalContainer from "./containers/FunctionalContainer";
import WeatherContainer from "./containers/WeatherContainer";

const App = () => {
  return (
    <div>
      <WeatherContainer />
      <FunctionalContainer />
    </div>
  );
};

export default App;

✔ 앱 실행

아래와 같이 잘 작동되시길 바랍니다!

1. 날씨 불러오기 버튼 클릭 전

2. 날씨 불러오기 버튼 클릭 후 (로딩 중)

(데이터가 보여지는 곳에 로딩 스핀이 돌고 있는 모습입니다.)

3. 날씨 불어오기 버튼 클릭 후 (성공)

👨‍💻 마무리

redux-thunk를 적용한 프로젝트를 정리하면서, thunk를 통해 정의한 getWeather함수에서 dispatch 관련 에러를 해결하기 위해 많은 시간을 보냈는데요.

두 번째 참고자료의 글에 서술되어 있는 '이 미들웨어를 사용하면 함수를 디스패치 할 수 있다고 했는데요, 함수를 디스패치 할 때에는, 해당 함수에서 dispatch 와 getState 를 파라미터로 받아와주어야 합니다. 이 함수를 만들어주는 함수를 우리는 thunk 라고 부릅니다.' 라는 글에서 정답을 얻었던 것 같습니다.

redux-thunk의 실제 코드는 아래와 같이 몇 줄 안되니 곱씹어보며 보다 더 이해해보려고 합니다. 감사합니다!

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => (next) => (action) => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

📝 참고자료

https://redux-advanced.vlpt.us/2/01.html

https://react.vlpt.us/redux-middleware/04-redux-thunk.html

리액트를 다루는 기술

profile
1day 1commit

0개의 댓글