이전글에 이어서 이번 글에서는 비동기작업 처리를 도와주는 리덕스 미들웨어를 공부해보도록 하겠습니다.
redux-thunk는 리덕스 미들웨어에서 비동기 작업을 처리하는데 사용하는 미들웨어로 비동기 작업을 다루는 미들웨어 중에서 가장 대표적인 리덕스 미들웨어입니다.
위키백과-썽크 에서 서술된 썽크의 정의를 보면, "썽크는 주로 연산 결과가 필요할 때까지 연산을 지연시키는 용도로 사용되거나, " 라고 언급되어 있습니다.
그렇다면, 연산 결과를 지연시키는 방법은 무엇이 있을까요? 가령, 1+2를 출력하고 싶다면, 아래와 같이 코드를 작성하면 됩니다.
console.log(1 + 2);
하지만, 썽크의 정의처럼 연산 결과가 필요할 때까지 연산을 지연시키고 싶다면 아래와 같이 함수로 코드를 감싼 후, 필요할 때 함수를 호출해주면 됩니다.
const foo = () => {
console.log(1+2);
}
우리가 알고 있는 기존의 리덕스에서 액션 생성 함수는 액션을 객체 형태로 반환해주는 함수입니다. 하지만, 리덕스 썽크를 이용하면 액션 생성 함수는 객체가 아닌 함수를 반환할 수 있게 해줍니다. 즉, 객체가 아닌 함수를 반환해줌으로서 필요할 때 함수를 호출할 수 있는 썽크의 형태가 되는 것입니다.
아래의 프로젝트는 noteapp-react-redux-thunk에 공개되어 있습니다.
백분이 불여일견, 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 모듈을 설치해주어야 합니다.
~$ 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();
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, //추가
});
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;
각각의 데이터(지역, 온도, 날씨)가 들어가는 곳에 조건문을 통해 유저에게 상태를 표시해주었습니다.
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;
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;
아래와 같이 잘 작동되시길 바랍니다!
(데이터가 보여지는 곳에 로딩 스핀이 돌고 있는 모습입니다.)
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
리액트를 다루는 기술