리덕스 미들웨어
1. 미들웨어 만들기
2. thunk 미들웨어 사용하기
dispatch => 미들웨어 => reducer
const middleware = store => next => action => {
}
function middleware(store) {
return function(next){
return function(action) {
하고싶은 작업
}
}
}
npx create-react-app redux-middleware
npm install redux
npm install react-redux
yarn add redux-devtools-extension
npm install redux-thunk
yarn add react-router-dom
// npm으로 설치하든 yarn으로 설치하든 상관없으나 npm이 빠른 느낌이 든다
카운터 만들기
1. 리덕스 모듈
액션타입, 초기값, 액션생성함수, 리듀서
modules 폴더
1) counter.js - 카운터를 관리하는 리듀서
2) index.js - 여러개의 리덕스를 하나로 합치기 rootReducer
2. 리액트 프로젝트에 리덕스 사용하기
1) index.js 파일에서 store 생성하기
creaseStore(rootReducer)
2) <Provider store={store}><App/></Provider>
3. 컴포넌트 만들기
1) 프레젠테이셔널 컴포넌트 - props
2) 컨테이너 컴포넌트 - 스토어에 접근
4. 미들웨어 만들기
1) middleware 폴더생성 myLogger.js 만들기
2) 미들웨어 사용하기
index.js
- applyMiddleware 함수 불러오기
- createStore의 인수로 추가
- createStore(rootReducer, applyMiddleware(myLogger))
5. DevTools 사용하기
- composeWithDevTools 불러오기
createStore(rootReducer, composeWithDevTools(applyMiddleware(myLogger)))
6. redux-thunk
리덕스에서 비동기 작업 처리할 때 가장 많이 사용하는 미들웨어입니다. 액션객체가 아닌 함수를 디스패치 할 수 있습니다.
1) 라이브러리 설치(npm install redux-thunk)
2) 미들웨어에 추가
createStore(rootReducer, composeWithDevTools(applyMiddleware(ReduxThunk, myLogger)))
const thunk = store => next => action => {
typeof action === "function"
? action(store.dispatch, store.getState)
: next(action)
}
7. 카운트 딜레이하기
index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { applyMiddleware, legacy_createStore as createStore } from 'redux';
import rootReducer from './modules';
import { Provider } from 'react-redux';
import myLogger from './middlewares/myLogger';
import { composeWithDevTools } from 'redux-devtools-extension';
import ReduxThunk from 'redux-thunk'
import { BrowserRouter } from 'react-router-dom'
// 미들웨어 적용하기 applyMiddleware(미들웨어이름)
const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(ReduxThunk,myLogger)))
// rootReducer가 하나일 경우는 괄호안에 바로 넣기
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<BrowserRouter>
<Provider store={store}>
<App />
</Provider>
</BrowserRouter>
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
App.js
import './App.css';
import CounterContainer from './components/CounterContainer';
import PostListContainer from './components/PostListContainer';
import { Routes, Route } from 'react-router-dom';
import PostListPage from './page/PostListPage';
import PostPage from './page/PostPage';
function App() {
return (
<div className="App">
<CounterContainer />
<Routes>
<Route path="/" element={<PostListPage/>}/>
<Route path="/:id" element={<PostPage/>}/>
</Routes>
<PostListContainer />
</div>
);
}
export default App;
modules/counter.js
// 액션타입, 액션 생성함수, 초기값, 리듀서
// 초기값
const initialState = 0;
// 액션타입
const INCREASE = "INCREASE";
const DECREASE = "DECREASE";
// 액션생성함수 - 액션을 리턴해주는 함수
// dispatch({ type: INCREASE }) 둘이 dispatch(increase()) 같다
export const increase = () => ({ type: INCREASE })
export const decrease = () => ({ type: DECREASE })
export const increaseAsync = () => dispatch => {
setTimeout(() => dispatch(increase()), 1000)
}
export const decreaseAsync = () => dispatch => {
setTimeout(() => dispatch(decrease()), 1000)
}
// 리듀서
export default function counter(state = initialState, action) {
switch(action.type) {
case INCREASE:
return state + 1;
case DECREASE:
return state - 1;
default:
return state;
}
}
modules/posts.js
// 액션타입, 액션생성함수, 초기값, 리듀서
// 프로미스가 시작, 성공, 실패 했을 때 다른 액션을 디스패치해야한다.
// 각 프로미스마다 thunk함수를 만들어주어야 합니다.
// 리듀서에서 액션에 따라 로딩중, 결과, 에러상태를 변경해줘야합니다.
import * as postAPI from '../api/posts' // api/osts안의 함수 모두 불러오기
import { createPromiseThunk, reducerUtils } from '../lib/asyncUtils'
// 초기값
// 반복되는 초기값 {}를 initial()를 실행하여 리턴받음
const initialState = {
// posts: {
// loading: false,
// data: null,
// error: null
// },
// post: {
// loading: false,
// data: null,
// error: null
// }
posts: reducerUtils.initial(),
post: reducerUtils.initial()
}
// 액션타입
// 포스트 여러개 조회하기
const GET_POSTS = "GET_POSTS"; // 요청시작
const GET_POSTS_SUCCESS = "GET_POSTS_SUCCESS"; // 요청성공
const GET_POSTS_ERROR = "GET_POSTS_ERROR"; // 요청실패
// 포스트 하나 조회하기
const GET_POST = "GET_POST"; // 요청시작
const GET_POST_SUCCESS = "GET_POST_SUCCESS";
const GET_POST_ERROR = "GET_POST_ERROR";
// thunk 함수
export const getPosts = createPromiseThunk(GET_POSTS, postAPI.getPosts);
// () => async dispatch => {
// dispatch({ type: GET_POSTS }) // 요청을 시작
// try {
// const posts = await postAPI.getPosts(); // api 호출
// dispatch({ type:GET_POSTS_SUCCESS, posts }); // 성공
// // 성공 dispatch({ type: GET_POSTS_SUCCESS, posts: post}) // 윗줄과 같은 의미
// }
// catch (e) {
// dispatch({ type: GET_POSTS_ERROR, error: e})
// }
// }
// 하나만 조회하는 thunk 함수
export const getPost = createPromiseThunk(GET_POST, postAPI.getPostById);
// id => async dispatch => {
// dispatch({ type: GET_POST }) // 요청을 시작
// try {
// const post = await postAPI.getPostById(id); // api 호출
// dispatch({ type:GET_POST_SUCCESS, post }); // 성공
// // 성공 dispatch({ type: GET_POST_SUCCESS, post: post}) // 윗줄과 같은 의미
// }
// catch(e) {
// dispatch({ type: GET_POST_ERROR, error: e})
// }
// }
export default function posts(state = initialState, action) {
switch(action.type) {
case GET_POSTS:
return {
...state,
// posts: {
// loading: true,
// data: null,
// error: null
// }
posts: reducerUtils.loading()
}
case GET_POSTS_SUCCESS:
return {
...state,
// posts: {
// loading: false,
// data: action.posts,
// error: null
// }
posts: reducerUtils.success(action.payload)
}
case GET_POSTS_ERROR:
return {
...state,
// posts: {
// loading: false,
// data: null,
// error: action.error
// }
posts: reducerUtils.success(action.error)
}
case GET_POST:
return {
...state,
// post: {
// loading: true,
// data: null,
// error: null
// }
post: reducerUtils.loading()
}
case GET_POST_SUCCESS:
return {
...state,
// post: {
// loading: false,
// data: action.post,
// error: null
// }
post: reducerUtils.success(action.payload)
}
case GET_POST_ERROR:
return {
...state,
// post: {
// loading: false,
// data: null,
// error: action.error
// }
post: reducerUtils.success(action.error)
}
default:
return state
}
}
modules/posts_test.js
// 액션타입, 액션생성함수, 초기값, 리듀서
// 프로미스가 시작, 성공, 실패 했을 때 다른 액션을 디스패치해야한다.
// 각 프로미스마다 thunk함수를 만들어주어야 합니다.
// 리듀서에서 액션에 따라 로딩중, 결과, 에러상태를 변경해줘야합니다.
import * as postAPI from '../api/posts' // api/osts안의 함수 모두 불러오기
// 초기값
const initialState = {
posts: {
loading: false,
data: null,
error: null
},
post: {
loading: false,
data: null,
error: null
}
}
// 액션타입
// 포스트 여러개 조회하기
const GET_POSTS = "GET_POSTS"; // 요청시작
const GET_POSTS_SUCCESS = "GET_POSTS_SUCCESS"; // 요청성공
const GET_POSTS_ERROR = "GET_POSTS_ERROR"; // 요청실패
// 포스트 하나 조회하기
const GET_POST = "GET_POST"; // 요청시작
const GET_POST_SUCCESS = "GET_POST_SUCCESS";
const GET_POST_ERROR = "GET_POST_ERROR";
// thunk 함수
export const getPosts = () => async dispatch => {
dispatch({ type: GET_POSTS }) // 요청을 시작
try {
const posts = await postAPI.getPosts(); // api 호출
dispatch({ type:GET_POSTS_SUCCESS, posts }); // 성공
// 성공 dispatch({ type: GET_POSTS_SUCCESS, posts: post}) // 윗줄과 같은 의미
}
catch (e) {
dispatch({ type: GET_POSTS_ERROR, error: e})
}
}
// 하나만 조회하는 thunk 함수
export const getPost = id => async dispatch => {
dispatch({ type: GET_POST }) // 요청을 시작
try {
const post = await postAPI.getPostById(id); // api 호출
dispatch({ type:GET_POST_SUCCESS, post }); // 성공
// 성공 dispatch({ type: GET_POST_SUCCESS, post: post}) // 윗줄과 같은 의미
}
catch(e) {
dispatch({ type: GET_POST_ERROR, error: e})
}
}
export default function posts_test(state = initialState, action) {
switch(action.type) {
case GET_POSTS:
return {
...state,
posts: {
loading: true,
data: null,
error: null
}
}
case GET_POSTS_SUCCESS:
return {
...state,
posts: {
loading: false,
data: action.posts,
error: null
}
}
case GET_POSTS_ERROR:
return {
...state,
posts: {
loading: false,
data: null,
error: action.error
}
}
case GET_POST:
return {
...state,
post: {
loading: true,
data: null,
error: null
}
}
case GET_POST_SUCCESS:
return {
...state,
post: {
loading: false,
data: action.post,
error: null
}
}
case GET_POST_ERROR:
return {
...state,
post: {
loading: false,
data: null,
error: action.error
}
}
default:
return state
}
}
modules/index.js
import { combineReducers } from "redux";
import counter from './counter'
import posts from './posts'
const rootReducer = combineReducers( {counter, posts} )
export default rootReducer;
components/Counter.js
import React from 'react';
const Counter = ({ number, onIncrease, onDecrease }) => {
return (
<div>
<h1>{number}</h1>
<button onClick={ onIncrease }>+1</button>
<button onClick={ onDecrease }>-1</button>
</div>
);
};
export default Counter;
components/CounterContainer.js
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { decreaseAsync, increaseAsync } from '../modules/counter';
// useSelector은 상태값 조회
import Counter from './Counter';
const CounterContainer = () => {
const number = useSelector(state=> state.counter)
const dispatch = useDispatch();
const onIncrease = () => {
dispatch(increaseAsync());
}
const onDecrease = () => {
dispatch(decreaseAsync());
}
return (
<Counter
number={number}
onDecrease={onDecrease}
onIncrease={onIncrease}/>
);
};
export default CounterContainer;
components/PostList.js
import React from 'react';
import { Link } from 'react-router-dom'
const PostList = ( { posts }) => {
return (
<ul>
{posts.map(post=>(
<li key={post.id}>
<Link to={`/${post.id}`}>{post.title}</Link>
</li>
))}
</ul>
);
};
export default PostList;
components/PostListContainer.js
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { getPosts } from '../modules/posts';
import PostList from './PostList';
const PostListContainer = () => {
const { data, loading, error } = useSelector( state=> state.posts.posts)
const dispatch = useDispatch();
// 컴포넌트 마운트 후 포스트 목록 요청하기
useEffect(()=> {
dispatch(getPosts())
}, [dispatch])
if(loading) return <div>로딩중 ..</div>
if(error) return <div>에러 발생</div>
if(!data) return null
return (
<PostList posts={data}/>
);
};
export default PostListContainer;
components/Post.js
import React from 'react';
const Post = ({ post }) => {
return (
<div>
<h1>{post.title}</h1>
<p>{post.body}</p>
</div>
);
};
export default Post;
components/PostContainer.js
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { getPost } from '../modules/posts'
import Post from './Post';
const PostContainer = ({ postId }) => {
const {data, loading, error } = useSelector(state => state.posts.post);
const dispatch = useDispatch();
useEffect(()=>{
dispatch(getPost(postId))
},[dispatch, postId])
if(loading) return <div>로딩중 ..</div>
if(error) return <div>에러발생</div>
if(!data) return null;
return (
<Post post={data}/>
);
};
export default PostContainer;
components/PostListPage.js
import React from 'react';
import PostContainer from '../components/PostContainer';
const PostListPage = () => {
return (
<PostContainer />
);
};
export default PostListPage;
components/PostPage.js
import React from 'react';
import PostContainer from '../components/PostContainer';
import { useParams } from 'react-router-dom';
const PostPage = () => {
const {id} = useParams();
return (
<PostContainer postId={parseInt(id)}/>
);
};
export default PostPage;
middlewares/myLogger.js
// 전달받은 액션을 출력하고 다음으로 넘기기
const myLogger = store => next => action => {
console.log(action); // 액션을 출력
const result = next(action); // 다음 미들웨어(또는 리듀서)에게 액션을 전달함
console.log('\t', store.getState())
return result; // 여기서 반환하는 값은 dispatch(액션)의 결과물이 됩니다. 기본: undefined
};
export default myLogger;
api/posts.js
// n 밀리세컨드동안 기다리는 프로미스를 만들어주는 함수
const sleep = n => new Promise(resolve => setTimeout(resolve, n));
//
const posts = [
{
id: 1,
title: "리덕스 미들웨어를 공부합시다.",
body: "리덕스 미들웨어를 만들어 봅시다."
},
{
id: 2,
title: "redux-thunk를 사용해봅시다.",
body: "redux-thunk를 사용해서 비동기 작업을 처리해봅시다."
},
{
id: 3,
title: "redux-saga도 공부해봅시다.",
body: "나중엔 redux-saga를 사용해서 비동기 작업을 처리해보세요."
}
]
// 포스트 목록을 가져오는 비동기 함수
export const getPosts = async () => {
await sleep(500); // 0.5초 쉬기
return posts;
}
// ID로 포스트를 조회하는 비동기 함수
export const getPostById = async id => {
await sleep(500); // 0.5초 쉬기
return posts.find(post => post.id === id); // id로 찾아서 반환
}
lib/asyncUtils.js
// thunk 함수
// GET_POSTS
export const createPromiseThunk = (type, promiseCreator) => {
const [ SUCCESS, ERROR ] = [`${type}_SUCCESS`, `${type}_ERROR`];
return param => async dispatch => {
dispatch({ type, param }) // 요청을 시작
try {
const payload = await promiseCreator(param); // api 호출
dispatch({ type: SUCCESS, payload }); // 성공
}
catch(e) {
dispatch({ type: ERROR, payload: e, error: true}) // 실패
}
}
}
// 리듀서에서 사용할 수 있는 유틸함수
export const reducerUtils = {
initial: (initialData = null) => ({
loading: false,
data: initialData,
error: null
}),
loading: (prevState = null) => ({
loading: true,
data: prevState,
error: null
}),
success: payload => ({
loading: false,
data: payload,
error: null
}),
error: error => ({
loading:false,
data: null,
error: error
})
}
'Stack > React' 카테고리의 다른 글
[React] 고객관리 사이트 (Client) redux로 구현하기 (0) | 2022.07.21 |
---|---|
[React] json-server을 이용하여 데이터 받아오기 (0) | 2022.07.15 |
[React] Redux - 리덕스 (0) | 2022.07.12 |
[React] HTML / 자바스크립트로 Redux 맛보기 (0) | 2022.07.11 |
[React] git clone 한 뒤, 오류 없이 npm start 하는 방법 (0) | 2022.07.09 |