본 글은 도서 '리액트를 다루는 기술', redux-thunk, redux-middleware를 참고하여 작성된 글입니다.
이전 글에 이어서 이번 글에서는 비동기 작업 처리를 도와주는 리덕스 미들웨어를 공부해보도록 하겠습니다.
🧰 redux-thunk, 그리고 thunk
redux-thunk는 리덕스 미들웨어에서 비동기 작업을 처리하는 데 사용하는 미들웨어로 비동기 작업을 다루는 미들웨어 중에서 가장 대표적인 리덕스 미들웨어입니다.
위키백과-썽크에서 서술된 펑크의 정의를 보면, "썽크는 주로 연산 결과가 필요할 때까지 연산을 지연시키는 용도로 사용되거나, "라고 언급되어 있습니다.
그렇다면, 연산 결과를 지연시키는 방법은 무엇이 있을까요? 가령, 1+2를 출력하고 싶다면, 아래와 같이 코드를 작성하면 됩니다.
console.log(1 + 2);
하지만, 썽크의 정의처럼 연산 결과가 필요할 때까지 연산을 지연시키고 싶다면 아래와 같이 함수로 코드를 감싼 후, 필요할 때 함수를 호출해주면 됩니다.
const foo = () => {
console.log(1+2);
}
🤨 그럼, redux-thunk는 어떻게 thunk의 개념을 적용해?
우리가 알고 있는 기존의 리덕스에서 액션 생성 함수는 액션을 객체 형태로 반환해주는 함수입니다. 하지만, 리덕스 썽크를 이용하면 액션 생성 함수는 객체가 아닌 함수를 반환할 수 있게 해 줍니다. 즉, 함수를 디스 패치할 수 있게 해 줍니다.
🙂 한번 사용해볼까?, 웹 요청 처리하기
이전 글에서 리덕스를 사용해 노트 앱을 만들어봤는데, 그 노트 앱 위에 현재 날씨를 요청해 데이터를 받아와 출력해주는 작업을 처리해보려고 합니다. 완성된 코드는 아래 링크에 있으니 참고하시길 바랍니다.
하나 씩 순서대로 만들어 보고 싶다면, 이전 글에 올려놓은 노트앱 프로젝트를 다운로드 혹은 클론 하시고, 터미널에서 파일 디렉터리로 이동해 아래와 같은 코드를 입력하면 프로젝트 환경에서 필요한 모듈이 설치가 됩니다.
~$ 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;
'Redux' 카테고리의 다른 글
[Redux] 🌤 Redux-saga를 사용해 간단한 날씨 앱 만들기 (0) | 2020.08.27 |
---|---|
[Redux] ⛓ 리덕스 미들웨어란 무엇인가? (2) (0) | 2020.05.30 |
[Redux] ⛓ 리덕스 미들웨어란 무엇인가? (1) (0) | 2020.05.30 |
[Redux] 🏃♂️ 리액트와 리덕스 함께 사용하기 (feat. ant-design) (2) (0) | 2020.05.01 |
[Redux] 🏃♂️ 리액트와 리덕스 함께 사용하기 (feat. ant-design) (1) (0) | 2020.05.01 |