Stack/React

[React] Redux Middleware - 리덕스 미들웨어

7ingout 2022. 7. 14. 12:36

리덕스 미들웨어

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
    })
}