🙄 미들웨어란?
미들웨어 코드의 기본 구조
- 리덕스 미들웨어는 액션을 디스패치했을 때 리듀서에서 이를 처리하기에 앞서 사전에 지정된 작업들을 실행한다.
- 미들웨어는 액션과 리듀서 사이의 중간자라고 볼 수 있다.
- 미들웨어가 할 수 있는 일에는 액션을 단순히 콘솔에 기록하거나, 전달받은 액션 정보를 기반으로 액션을 아예 취소하거나 다른 종류의 액션을 추가로 디스패치할 수 있다.
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를 사용하지 않으면 액션이 리듀서에 전달되지 않는다. 액션이 무시된다.
로그 남기는 미들웨어 코드
const middleware = store => next => action => {
console.log('이전 상태', store.getState())
console.log('액션', action)
next(action)
console.log('다음 상태', store.getState())
};
export default middleware
import { createStore, applyMiddleware } from 'redux'
...
const store = createStore(rootReducer, applyMiddleware(middleware));
...
😎 비동기 작업을 처리하는 미들웨어
Redux-Thunk
- 비동기 작업을 처리할 때 가장 많이 사용되는 미들웨어
- 객체가 아닌 함수 형태의 액션을 디스패치할 수 있게 해줌
- 리덕스의 창시자인 댄아브라모프(Dan Abramov)가 만들었음
- Thunk란 특정 작업을 나중에 할 수 있도록 미루기 위해 함수 형태로 감싼 것을 의미
- ReduxThunk를 사용하고 싶다면 index.js의 applyMiddleware의 인자로 넣어준다
const sampleThunk = () => (dispatch, getState) => {
}
Exercise
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
);
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>
);
}
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;
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;
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);
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);
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`);
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;
}
};
}
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;
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;
import { createAction, handleActions } from "redux-actions";
const START_LOADING = "loading/START_LOADING";
const FINISH_LOADING = "loading/FINISH_LOADING";
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;
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