본문 바로가기

Redux

[Redux] 🌤 Redux-thunk를 사용해 간단한 날씨 앱 만들기

본 글은 도서 '리액트를 다루는 기술', redux-thunk, redux-middleware를 참고하여 작성된 글입니다.

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

🧰 redux-thunk, 그리고 thunk

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

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

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

 

console.log(1 + 2);

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

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

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

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

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

이전 글에서 리덕스를 사용해 노트 앱을 만들어봤는데, 그 노트 앱 위에 현재 날씨를 요청해 데이터를 받아와 출력해주는 작업을 처리해보려고 합니다. 완성된 코드는 아래 링크에 있으니 참고하시길 바랍니다.

 

 

youthfulhps/noteapp-react-redux-thunk

리덕스 성크를 공부하기 위한 간단한 노트앱입니다. Contribute to youthfulhps/noteapp-react-redux-thunk development by creating an account on GitHub.

github.com

 

하나 씩 순서대로 만들어 보고 싶다면, 이전 글에 올려놓은 노트앱 프로젝트를 다운로드 혹은 클론 하시고, 터미널에서 파일 디렉터리로 이동해 아래와 같은 코드를 입력하면 프로젝트 환경에서 필요한 모듈이 설치가 됩니다.

~$ yarn

 

설치가 완료되었다면, 아래와 같이 입력하여 프로젝트를 실행시켜 잘 작동하는지 확인해 주세요.

 

 

~$ yarn start

 

모두 잘 되셨다면, 이제 날씨를 출력하는 앱을 그 노트 앱 위에 만들어 보도록 하겠습니다.

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

우선, 필요한 날씨 정보를 요청할 때 axios를 사용하기 때문에 axios를 설치해 주세요.

~$ yarn add axios

 

날씨 정보는 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;

아래와 같이 콘솔에 잘 출력된 것을 확인할 수 있습니다.

✔ 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
);

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

 

또한, 위 코드에서 성공 시에 action.payload로 전달되는 값은 이전에 콘솔에서 출력했던 값 들일 텐데, 그중, 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;

각각의 데이터(지역, 온도, 날씨)가 들어가는 곳에 조건문을 통해 유저에게 상태를 표시해주도록 하였는데요. 위에서 정의한 weather 모듈을 보면, GET_WEATHER_PENDING 액션 상태일 때, loading 값을 true로 전달해 주었습니다. 이를 통해 현재 상태가 요청을 날렸지만, 아직 성공, 실패의 결과를 얻지 못한 상태 즉, 로딩 상태를 유저에게 알려주기 위해 로딩 시 스핀 아이콘이 화면에 출력될 수 있도록 조건 랜더링을 해주었습니다.

✔ WeatherContainer 컨테이너 생성

src/containers/WeatherContainer.js 파일을 생성하고, 아래와 같이 코드를 작성해 주세요. 똑똑한 컴포넌트이기 때문에 상태 값을 가져와 UI를 담당하는 weather 컴포넌트에게 전달해주는 역할을 하고 있습니다.

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;